Skip to content

Commit b940a23

Browse files
authored
fix(adapter-nextjs): server-side sign out not working with Firefox and Safari (aws-amplify#14246)
1 parent 677f466 commit b940a23

20 files changed

+365
-76
lines changed

packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => {
295295
oAuthConfig: testOAuthConfig,
296296
setCookieOptions: {},
297297
userPoolClientId: 'userPoolClientId',
298+
origin: testOrigin,
298299
});
299300
});
300301
});

packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => {
289289
oAuthConfig: testOAuthConfig,
290290
setCookieOptions: {},
291291
userPoolClientId: 'userPoolClientId',
292+
origin: testOrigin,
292293
},
293294
);
294295
});

packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequest.test.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
appendSetCookieHeaders,
88
createAuthFlowProofCookiesRemoveOptions,
99
createErrorSearchParamsString,
10-
createOnSignInCompleteRedirectIntermediate,
10+
createRedirectionIntermediary,
1111
createSignInFlowProofCookies,
1212
createTokenCookies,
1313
createTokenCookiesSetOptions,
@@ -37,8 +37,8 @@ const mockAppendSetCookieHeaders = jest.mocked(appendSetCookieHeaders);
3737
const mockCreateAuthFlowProofCookiesRemoveOptions = jest.mocked(
3838
createAuthFlowProofCookiesRemoveOptions,
3939
);
40-
const mockCreateOnSignInCompleteRedirectIntermediate = jest.mocked(
41-
createOnSignInCompleteRedirectIntermediate,
40+
const mockCreateRedirectionIntermediary = jest.mocked(
41+
createRedirectionIntermediary,
4242
);
4343
const mockCreateSignInFlowProofCookies = jest.mocked(
4444
createSignInFlowProofCookies,
@@ -78,7 +78,7 @@ describe('handleSignInCallbackRequest', () => {
7878
afterEach(() => {
7979
mockAppendSetCookieHeaders.mockClear();
8080
mockCreateAuthFlowProofCookiesRemoveOptions.mockClear();
81-
mockCreateOnSignInCompleteRedirectIntermediate.mockClear();
81+
mockCreateRedirectionIntermediary.mockClear();
8282
mockCreateSignInFlowProofCookies.mockClear();
8383
mockCreateTokenCookies.mockClear();
8484
mockCreateTokenCookiesSetOptions.mockClear();
@@ -300,8 +300,8 @@ describe('handleSignInCallbackRequest', () => {
300300
headers.append('Set-cookie', 'mock-cookie-1');
301301
headers.append('Set-cookie', 'mock-cookie-2');
302302
});
303-
mockCreateOnSignInCompleteRedirectIntermediate.mockImplementationOnce(
304-
({ redirectOnSignInComplete }) =>
303+
mockCreateRedirectionIntermediary.mockImplementationOnce(
304+
({ redirectTo: redirectOnSignInComplete }) =>
305305
`<html>redirect to ${redirectOnSignInComplete}</html>`,
306306
);
307307

@@ -352,10 +352,8 @@ describe('handleSignInCallbackRequest', () => {
352352
mockCreateSignInFlowProofCookiesResult,
353353
mockCreateAuthFlowProofCookiesRemoveOptionsResult,
354354
);
355-
expect(
356-
mockCreateOnSignInCompleteRedirectIntermediate,
357-
).toHaveBeenCalledWith({
358-
redirectOnSignInComplete: expectedFinalRedirect,
355+
expect(mockCreateRedirectionIntermediary).toHaveBeenCalledWith({
356+
redirectTo: expectedFinalRedirect,
359357
});
360358
expect(mockGetRedirectOrDefault).toHaveBeenCalledWith(
361359
handlerInput.redirectOnSignInComplete,

packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequestForPagesRouter.test.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
appendSetCookieHeadersToNextApiResponse,
88
createAuthFlowProofCookiesRemoveOptions,
99
createErrorSearchParamsString,
10-
createOnSignInCompleteRedirectIntermediate,
10+
createRedirectionIntermediary,
1111
createSignInFlowProofCookies,
1212
createTokenCookies,
1313
createTokenCookiesSetOptions,
@@ -40,8 +40,8 @@ const mockAppendSetCookieHeadersToNextApiResponse = jest.mocked(
4040
const mockCreateAuthFlowProofCookiesRemoveOptions = jest.mocked(
4141
createAuthFlowProofCookiesRemoveOptions,
4242
);
43-
const mockCreateOnSignInCompleteRedirectIntermediate = jest.mocked(
44-
createOnSignInCompleteRedirectIntermediate,
43+
const mockCreateRedirectionIntermediary = jest.mocked(
44+
createRedirectionIntermediary,
4545
);
4646
const mockCreateSignInFlowProofCookies = jest.mocked(
4747
createSignInFlowProofCookies,
@@ -91,7 +91,7 @@ describe('handleSignInCallbackRequest', () => {
9191
afterEach(() => {
9292
mockAppendSetCookieHeadersToNextApiResponse.mockClear();
9393
mockCreateAuthFlowProofCookiesRemoveOptions.mockClear();
94-
mockCreateOnSignInCompleteRedirectIntermediate.mockClear();
94+
mockCreateRedirectionIntermediary.mockClear();
9595
mockCreateSignInFlowProofCookies.mockClear();
9696
mockCreateTokenCookies.mockClear();
9797
mockCreateTokenCookiesSetOptions.mockClear();
@@ -345,8 +345,8 @@ describe('handleSignInCallbackRequest', () => {
345345
response.appendHeader('Set-cookie', 'mock-cookie-2');
346346
},
347347
);
348-
mockCreateOnSignInCompleteRedirectIntermediate.mockImplementationOnce(
349-
({ redirectOnSignInComplete }) =>
348+
mockCreateRedirectionIntermediary.mockImplementationOnce(
349+
({ redirectTo: redirectOnSignInComplete }) =>
350350
`<html>redirect to ${redirectOnSignInComplete}</html>`,
351351
);
352352

@@ -396,10 +396,8 @@ describe('handleSignInCallbackRequest', () => {
396396
mockSetCookieOptions,
397397
);
398398

399-
expect(
400-
mockCreateOnSignInCompleteRedirectIntermediate,
401-
).toHaveBeenCalledWith({
402-
redirectOnSignInComplete: expectedFinalRedirect,
399+
expect(mockCreateRedirectionIntermediary).toHaveBeenCalledWith({
400+
redirectTo: expectedFinalRedirect,
403401
});
404402
expect(getRedirectOrDefault).toHaveBeenCalledWith(
405403
handlerInput.redirectOnSignInComplete,

packages/adapter-nextjs/__tests__/auth/handlers/handleSignOutCallbackRequest.test.ts

+105-1
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import {
55
createKeysForAuthStorage,
66
} from 'aws-amplify/adapter-core';
77

8-
import { IS_SIGNING_OUT_COOKIE_NAME } from '../../../src/auth/constant';
8+
import {
9+
IS_SIGNING_OUT_COOKIE_NAME,
10+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
11+
} from '../../../src/auth/constant';
912
import { handleSignOutCallbackRequest } from '../../../src/auth/handlers/handleSignOutCallbackRequest';
1013
import { CreateAuthRoutesHandlersInput } from '../../../src/auth/types';
1114
import {
1215
appendSetCookieHeaders,
16+
createAuthFlowProofCookiesRemoveOptions,
17+
createRedirectionIntermediary,
1318
createTokenCookiesRemoveOptions,
1419
createTokenRemoveCookies,
1520
getCookieValuesFromRequest,
1621
getRedirectOrDefault,
22+
resolveRedirectSignOutUrl,
1723
revokeAuthNTokens,
1824
} from '../../../src/auth/utils';
1925

@@ -32,6 +38,13 @@ const mockGetCookieValuesFromRequest = jest.mocked(getCookieValuesFromRequest);
3238
const mockRevokeAuthNTokens = jest.mocked(revokeAuthNTokens);
3339
const mockCreateKeysForAuthStorage = jest.mocked(createKeysForAuthStorage);
3440
const mockGetRedirectOrDefault = jest.mocked(getRedirectOrDefault);
41+
const mockCreateAuthFlowProofCookiesRemoveOptions = jest.mocked(
42+
createAuthFlowProofCookiesRemoveOptions,
43+
);
44+
const mockCreateRedirectionIntermediary = jest.mocked(
45+
createRedirectionIntermediary,
46+
);
47+
const mockResolveRedirectSignOutUrl = jest.mocked(resolveRedirectSignOutUrl);
3548

3649
describe('handleSignOutCallbackRequest', () => {
3750
const mockRequest = new Request(
@@ -57,6 +70,9 @@ describe('handleSignOutCallbackRequest', () => {
5770
mockGetCookieValuesFromRequest.mockClear();
5871
mockRevokeAuthNTokens.mockClear();
5972
mockGetRedirectOrDefault.mockClear();
73+
mockCreateAuthFlowProofCookiesRemoveOptions.mockClear();
74+
mockCreateRedirectionIntermediary.mockClear();
75+
mockResolveRedirectSignOutUrl.mockClear();
6076
});
6177

6278
it(`returns a 400 response when the request does not have the "${IS_SIGNING_OUT_COOKIE_NAME}" cookie`, async () => {
@@ -68,6 +84,7 @@ describe('handleSignOutCallbackRequest', () => {
6884
userPoolClientId: mockUserPoolClientId,
6985
oAuthConfig: mockOAuthConfig,
7086
setCookieOptions: mockSetCookieOptions,
87+
origin: 'https://example.com',
7188
});
7289

7390
// verify the response
@@ -76,9 +93,88 @@ describe('handleSignOutCallbackRequest', () => {
7693
// verify the calls to dependencies
7794
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
7895
IS_SIGNING_OUT_COOKIE_NAME,
96+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
7997
]);
8098
});
8199

100+
it(`returns a 200 response with the intermediate redirect HTML when the request has the "${IS_SIGNING_OUT_COOKIE_NAME}" and "${IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME}" cookies`, async () => {
101+
mockGetCookieValuesFromRequest.mockReturnValueOnce({
102+
[IS_SIGNING_OUT_COOKIE_NAME]: 'true',
103+
[IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME]: 'true',
104+
});
105+
const mockCreateTokenRemoveCookiesResult = [
106+
{
107+
name: IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
108+
value: '',
109+
},
110+
];
111+
mockCreateTokenRemoveCookies.mockReturnValueOnce(
112+
mockCreateTokenRemoveCookiesResult,
113+
);
114+
const mockCreateTokenCookiesRemoveOptionsResult = {
115+
path: '/',
116+
maxAge: -1,
117+
domain: mockSetCookieOptions.domain,
118+
};
119+
mockCreateAuthFlowProofCookiesRemoveOptions.mockReturnValueOnce(
120+
mockCreateTokenCookiesRemoveOptionsResult,
121+
);
122+
const mockResolveRedirectSignOutUrlResult =
123+
'https://example.com/sign-out-callback';
124+
mockResolveRedirectSignOutUrl.mockReturnValueOnce(
125+
mockResolveRedirectSignOutUrlResult,
126+
);
127+
const mockCreateOnSignInCompleteRedirectIntermediateResult =
128+
'<html><head><meta http-equiv="refresh" content="0;url=/"></head></html>';
129+
mockCreateRedirectionIntermediary.mockReturnValueOnce(
130+
mockCreateOnSignInCompleteRedirectIntermediateResult,
131+
);
132+
mockAppendSetCookieHeaders.mockImplementationOnce(headers => {
133+
headers.append(
134+
'Set-Cookie',
135+
'mock_cookie1=; Domain=.example.com; Path=/',
136+
);
137+
headers.append(
138+
'Set-Cookie',
139+
'mock_cookie2=; Domain=.example.com; Path=/',
140+
);
141+
});
142+
143+
const response = await handleSignOutCallbackRequest({
144+
request: mockRequest,
145+
handlerInput: mockHandlerInput,
146+
userPoolClientId: mockUserPoolClientId,
147+
oAuthConfig: mockOAuthConfig,
148+
setCookieOptions: mockSetCookieOptions,
149+
origin: 'https://example.com',
150+
});
151+
152+
// verify the response
153+
expect(response.status).toBe(200);
154+
expect(response.headers.get('Content-Type')).toBe('text/html');
155+
expect(response.headers.get('Set-Cookie')).toBe(
156+
'mock_cookie1=; Domain=.example.com; Path=/, mock_cookie2=; Domain=.example.com; Path=/',
157+
);
158+
expect(await response.text()).toBe(
159+
mockCreateOnSignInCompleteRedirectIntermediateResult,
160+
);
161+
162+
// verify the calls to dependencies
163+
expect(mockCreateTokenRemoveCookies).toHaveBeenCalledWith([
164+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
165+
]);
166+
expect(mockCreateAuthFlowProofCookiesRemoveOptions).toHaveBeenCalledWith(
167+
mockSetCookieOptions,
168+
);
169+
expect(mockResolveRedirectSignOutUrl).toHaveBeenCalledWith(
170+
'https://example.com',
171+
mockOAuthConfig,
172+
);
173+
expect(mockCreateRedirectionIntermediary).toHaveBeenCalledWith({
174+
redirectTo: mockResolveRedirectSignOutUrlResult,
175+
});
176+
});
177+
82178
it('returns a 302 response to redirect to handlerInput.redirectOnSignOutComplete when the request cookies do not have a username', async () => {
83179
mockGetCookieValuesFromRequest
84180
.mockReturnValueOnce({
@@ -92,6 +188,7 @@ describe('handleSignOutCallbackRequest', () => {
92188
userPoolClientId: mockUserPoolClientId,
93189
oAuthConfig: mockOAuthConfig,
94190
setCookieOptions: mockSetCookieOptions,
191+
origin: 'https://example.com',
95192
});
96193

97194
// verify the response
@@ -101,6 +198,7 @@ describe('handleSignOutCallbackRequest', () => {
101198
// verify the calls to dependencies
102199
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
103200
IS_SIGNING_OUT_COOKIE_NAME,
201+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
104202
]);
105203
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
106204
`${AUTH_KEY_PREFIX}.${mockUserPoolClientId}.LastAuthUser`,
@@ -126,6 +224,7 @@ describe('handleSignOutCallbackRequest', () => {
126224
userPoolClientId: mockUserPoolClientId,
127225
oAuthConfig: mockOAuthConfig,
128226
setCookieOptions: mockSetCookieOptions,
227+
origin: 'https://example.com',
129228
});
130229

131230
// verify the response
@@ -136,6 +235,7 @@ describe('handleSignOutCallbackRequest', () => {
136235
// verify the calls to dependencies
137236
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
138237
IS_SIGNING_OUT_COOKIE_NAME,
238+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
139239
]);
140240
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
141241
`${AUTH_KEY_PREFIX}.${mockUserPoolClientId}.LastAuthUser`,
@@ -167,6 +267,7 @@ describe('handleSignOutCallbackRequest', () => {
167267
userPoolClientId: mockUserPoolClientId,
168268
oAuthConfig: mockOAuthConfig,
169269
setCookieOptions: mockSetCookieOptions,
270+
origin: 'https://example.com',
170271
});
171272

172273
// verify the response
@@ -176,6 +277,7 @@ describe('handleSignOutCallbackRequest', () => {
176277
// verify the calls to dependencies
177278
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
178279
IS_SIGNING_OUT_COOKIE_NAME,
280+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
179281
]);
180282
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
181283
`${AUTH_KEY_PREFIX}.${mockUserPoolClientId}.LastAuthUser`,
@@ -257,6 +359,7 @@ describe('handleSignOutCallbackRequest', () => {
257359
userPoolClientId: mockUserPoolClientId,
258360
oAuthConfig: mockOAuthConfig,
259361
setCookieOptions: mockSetCookieOptions,
362+
origin: 'https://example.com',
260363
});
261364

262365
// verify the calls to dependencies
@@ -269,6 +372,7 @@ describe('handleSignOutCallbackRequest', () => {
269372
// verify the calls to dependencies
270373
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
271374
IS_SIGNING_OUT_COOKIE_NAME,
375+
IS_SIGNING_OUT_REDIRECTING_COOKIE_NAME,
272376
]);
273377
expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(mockRequest, [
274378
`${AUTH_KEY_PREFIX}.${mockUserPoolClientId}.LastAuthUser`,

0 commit comments

Comments
 (0)