Skip to content

Commit b412884

Browse files
authored
CCM-8744: force refresh access tokens in middleware (#337)
1 parent 31e95ce commit b412884

File tree

16 files changed

+246
-216
lines changed

16 files changed

+246
-216
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"devDependencies": [
6868
"jest.config.ts",
6969
"jest.setup.ts",
70-
"**/__tests__/**"
70+
"**/__tests__/**",
71+
"**/*.dev.[jt]s?(x)"
7172
]
7273
}
7374
],

frontend/next.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ const nextConfig = (phase) => {
4242
permanent: false,
4343
basePath: false,
4444
},
45+
{
46+
source: `${basePath}/auth/signout`,
47+
destination: '/auth/signout',
48+
basePath: false,
49+
permanent: false,
50+
},
4551
];
4652
},
4753

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@testing-library/react": "^16.2.0",
4545
"@testing-library/user-event": "^14.6.1",
4646
"@types/jest": "^29.5.14",
47+
"@types/js-cookie": "^3.0.6",
4748
"@types/jsonwebtoken": "^9.0.8",
4849
"@types/markdown-it": "^13.0.1",
4950
"@types/node": "^22.8.1",
@@ -52,6 +53,7 @@
5253
"constructs": "^10.3.0",
5354
"eslint-config-next": "^15.1.7",
5455
"jest-mock-extended": "^3.0.7",
56+
"js-cookie": "^3.0.5",
5557
"mochawesome": "^7.1.3",
5658
"pa11y-ci": "^3.1.0",
5759
"pa11y-ci-reporter-html": "^7.0.0",

frontend/src/__tests__/components/molecules/NHSNotifyFormWrapper.test.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import {
44
csrfServerAction,
55
} from '@molecules/NHSNotifyFormWrapper/NHSNotifyFormWrapper';
66
import { render } from '@testing-library/react';
7-
import { verifyCsrfTokenFull } from '@utils/csrf-utils';
7+
import { redirect } from 'next/navigation';
8+
import { verifyFormCsrfToken } from '@utils/csrf-utils';
9+
10+
jest.mock('next/navigation');
811

912
jest.mock('@utils/csrf-utils', () => ({
10-
verifyCsrfTokenFull: jest.fn(),
13+
verifyFormCsrfToken: jest.fn(),
1114
}));
1215

1316
test('Renders back button', () => {
@@ -27,8 +30,9 @@ describe('csrfServerAction', () => {
2730
expect(action).toEqual('/action');
2831
});
2932

30-
test('server action', async () => {
31-
const mockAction = jest.fn(() => 'response');
33+
test('server action with valid csrf check', async () => {
34+
jest.mocked(verifyFormCsrfToken).mockResolvedValueOnce(true);
35+
const mockAction = jest.fn();
3236
const action = csrfServerAction(mockAction);
3337

3438
if (typeof action === 'string') {
@@ -38,7 +42,25 @@ describe('csrfServerAction', () => {
3842
const mockFormData = mockDeep<FormData>();
3943
await action(mockFormData);
4044

41-
expect(verifyCsrfTokenFull).toHaveBeenCalledWith(mockFormData);
45+
expect(verifyFormCsrfToken).toHaveBeenCalledWith(mockFormData);
4246
expect(mockAction).toHaveBeenCalledWith(mockFormData);
4347
});
48+
49+
test('server action with failed csrf check', async () => {
50+
jest.mocked(verifyFormCsrfToken).mockResolvedValueOnce(false);
51+
52+
const mockAction = jest.fn();
53+
const action = csrfServerAction(mockAction);
54+
55+
if (typeof action === 'string') {
56+
throw new TypeError('Expected server action');
57+
}
58+
59+
const mockFormData = mockDeep<FormData>();
60+
await action(mockFormData);
61+
62+
expect(verifyFormCsrfToken).toHaveBeenCalledWith(mockFormData);
63+
expect(redirect).toHaveBeenCalledWith('/auth/signout');
64+
expect(mockAction).not.toHaveBeenCalled();
65+
});
4466
});

frontend/src/__tests__/middleware.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ afterAll(() => {
2020
});
2121

2222
describe('middleware function', () => {
23-
it('If route is not registered in midleware, respond with 404', async () => {
23+
it('If route is not registered in middleware, respond with 404', async () => {
2424
const url = new URL('https://url.com/manage-templates/does-not-exist');
2525
const request = new NextRequest(url);
2626
const response = await middleware(request);
@@ -31,13 +31,18 @@ describe('middleware function', () => {
3131
it('if request path is protected, and no access token is obtained, redirect to auth page', async () => {
3232
const url = new URL('https://url.com/manage-templates');
3333
const request = new NextRequest(url);
34+
request.cookies.set('csrf_token', 'some-csrf-value');
35+
3436
const response = await middleware(request);
3537

38+
expect(getTokenMock).toHaveBeenCalledWith({ forceRefresh: true });
39+
3640
expect(response.status).toBe(307);
3741
expect(response.headers.get('location')).toBe(
3842
'https://url.com/auth?redirect=%2Ftemplates%2Fmanage-templates'
3943
);
4044
expect(response.headers.get('Content-Type')).toBe('text/html');
45+
expect(response.cookies.get('csrf_token')?.value).toEqual('');
4146
});
4247

4348
it('if request path is protected, and access token is obtained, respond with CSP', async () => {

frontend/src/__tests__/utils/amplify-utils.test.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* @jest-environment node
33
*/
4-
import { getAccessTokenServer } from '@utils/amplify-utils';
4+
import { sign } from 'jsonwebtoken';
55
import { fetchAuthSession } from 'aws-amplify/auth/server';
6+
import { getAccessTokenServer, getSessionId } from '../../utils/amplify-utils';
67

78
jest.mock('aws-amplify/auth/server');
89
jest.mock('@aws-amplify/adapter-nextjs/api');
@@ -19,24 +20,22 @@ const fetchAuthSessionMock = jest.mocked(fetchAuthSession);
1920

2021
describe('amplify-utils', () => {
2122
test('getAccessTokenServer - should return the auth token', async () => {
22-
fetchAuthSessionMock.mockResolvedValue({
23+
fetchAuthSessionMock.mockResolvedValueOnce({
2324
tokens: {
2425
accessToken: {
25-
toString: () => 'mockSub',
26-
payload: {
27-
sub: 'mockSub',
28-
},
26+
toString: () => 'mockToken',
27+
payload: {},
2928
},
3029
},
3130
});
3231

3332
const result = await getAccessTokenServer();
3433

35-
expect(result).toEqual('mockSub');
34+
expect(result).toEqual('mockToken');
3635
});
3736

3837
test('getAccessTokenServer - should return undefined when no auth session', async () => {
39-
fetchAuthSessionMock.mockResolvedValue({});
38+
fetchAuthSessionMock.mockResolvedValueOnce({});
4039

4140
const result = await getAccessTokenServer();
4241

@@ -52,4 +51,46 @@ describe('amplify-utils', () => {
5251

5352
expect(result).toBeUndefined();
5453
});
54+
55+
describe('getSessionId', () => {
56+
test('returns void when access token not found', async () => {
57+
fetchAuthSessionMock.mockResolvedValueOnce({});
58+
59+
await expect(getSessionId()).resolves.toBeUndefined();
60+
});
61+
62+
test('errors when session ID not found', async () => {
63+
fetchAuthSessionMock.mockResolvedValueOnce({
64+
tokens: {
65+
accessToken: {
66+
toString: () => sign({}, 'key'),
67+
payload: {},
68+
},
69+
},
70+
});
71+
72+
await expect(getSessionId()).resolves.toBeUndefined();
73+
});
74+
75+
test('returns session id', async () => {
76+
fetchAuthSessionMock.mockResolvedValueOnce({
77+
tokens: {
78+
accessToken: {
79+
toString: () =>
80+
sign(
81+
{
82+
origin_jti: 'jti',
83+
},
84+
'key'
85+
),
86+
payload: {},
87+
},
88+
},
89+
});
90+
91+
const sessionId = await getSessionId();
92+
93+
expect(sessionId).toEqual('jti');
94+
});
95+
});
5596
});

0 commit comments

Comments
 (0)