diff --git a/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequest.test.ts b/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequest.test.ts index ef3ee357a0a..3f54a94a33d 100644 --- a/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequest.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequest.test.ts @@ -16,7 +16,7 @@ import { exchangeAuthNTokens, getCookieValuesFromRequest, getRedirectOrDefault, - resolveCodeAndStateFromUrl, + parseSignInCallbackUrl, resolveRedirectSignInUrl, } from '../../../src/auth/utils'; import { CreateAuthRoutesHandlersInput } from '../../../src/auth/types'; @@ -25,6 +25,11 @@ import { STATE_COOKIE_NAME, } from '../../../src/auth/constant'; +import { + ERROR_CLIENT_COOKIE_COMBINATIONS, + ERROR_URL_PARAMS_COMBINATIONS, +} from './signInCallbackErrorCombinations'; + jest.mock('../../../src/auth/utils'); const mockAppendSetCookieHeaders = jest.mocked(appendSetCookieHeaders); @@ -43,7 +48,7 @@ const mockCreateTokenCookiesSetOptions = jest.mocked( ); const mockExchangeAuthNTokens = jest.mocked(exchangeAuthNTokens); const mockGetCookieValuesFromRequest = jest.mocked(getCookieValuesFromRequest); -const mockResolveCodeAndStateFromUrl = jest.mocked(resolveCodeAndStateFromUrl); +const mockParseSignInCallbackUrl = jest.mocked(parseSignInCallbackUrl); const mockResolveRedirectSignInUrl = jest.mocked(resolveRedirectSignInUrl); const mockGetRedirectOrDefault = jest.mocked(getRedirectOrDefault); @@ -72,19 +77,25 @@ describe('handleSignInCallbackRequest', () => { mockCreateTokenCookiesSetOptions.mockClear(); mockExchangeAuthNTokens.mockClear(); mockGetCookieValuesFromRequest.mockClear(); - mockResolveCodeAndStateFromUrl.mockClear(); + mockParseSignInCallbackUrl.mockClear(); mockResolveRedirectSignInUrl.mockClear(); }); - test.each([ - [null, 'state'], - ['state', null], - ])( - 'returns a 400 response when request.url contains query params: code=%s, state=%s', - async (code, state) => { - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + test.each(ERROR_URL_PARAMS_COMBINATIONS)( + 'returns a $expectedStatus response when request.url contains query params: code=$code, state=$state, error=$error, error_description=$errorDescription', + async ({ + code, + state, + error, + errorDescription, + expectedStatus, + expectedRedirect, + }) => { + mockParseSignInCallbackUrl.mockReturnValueOnce({ code, state, + error, + errorDescription, }); const url = 'https://example.com/api/auth/sign-in-callback'; const request = new NextRequest(new URL(url)); @@ -98,33 +109,30 @@ describe('handleSignInCallbackRequest', () => { origin: mockOrigin, }); - expect(response.status).toBe(400); - expect(mockResolveCodeAndStateFromUrl).toHaveBeenCalledWith(url); + expect(response.status).toBe(expectedStatus); + expect(mockParseSignInCallbackUrl).toHaveBeenCalledWith(url); + + if (expectedStatus === 302) { + expect(response.headers.get('Location')).toBe(expectedRedirect); + } }, ); - test.each([ - ['client state cookie is missing', undefined, 'state', 'pkce'], - [ - 'client cookie state a different value from the state query parameter', - 'state_different', - 'state', - 'pkce', - ], - ['client pkce cookie is missing', 'state', 'state', undefined], - ])( - `returns a 400 response when %s`, - async (_, clientState, state, clientPkce) => { - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + test.each(ERROR_CLIENT_COOKIE_COMBINATIONS)( + `returns a $expectedStatus response when client cookies are: state=$state, pkce=$pkce and expected state value is 'state_b'`, + async ({ state, pkce, expectedStatus, expectedRedirect }) => { + mockParseSignInCallbackUrl.mockReturnValueOnce({ code: 'not_important_for_this_test', - state, + state: 'not_important_for_this_test', + error: null, + errorDescription: null, }); mockGetCookieValuesFromRequest.mockReturnValueOnce({ - [STATE_COOKIE_NAME]: clientState, - [PKCE_COOKIE_NAME]: clientPkce, + [STATE_COOKIE_NAME]: state, + [PKCE_COOKIE_NAME]: pkce, }); - const url = `https://example.com/api/auth/sign-in-callback?state=${state}&code=not_important_for_this_test`; + const url = `https://example.com/api/auth/sign-in-callback?state=state_b&code=not_important_for_this_test`; const request = new NextRequest(new URL(url)); const response = await handleSignInCallbackRequest({ @@ -136,12 +144,19 @@ describe('handleSignInCallbackRequest', () => { origin: mockOrigin, }); - expect(response.status).toBe(400); - expect(mockResolveCodeAndStateFromUrl).toHaveBeenCalledWith(url); + expect(response.status).toBe(expectedStatus); + expect(mockParseSignInCallbackUrl).toHaveBeenCalledWith(url); expect(mockGetCookieValuesFromRequest).toHaveBeenCalledWith(request, [ PKCE_COOKIE_NAME, STATE_COOKIE_NAME, ]); + + if (expectedStatus === 302) { + expect(mockGetRedirectOrDefault).toHaveBeenCalledWith( + mockHandlerInput.redirectOnSignOutComplete, + ); + expect(response.headers.get('Location')).toBe(expectedRedirect); + } }, ); @@ -151,9 +166,11 @@ describe('handleSignInCallbackRequest', () => { const mockSignInCallbackUrl = 'https://example.com/api/auth/sign-in-callback'; const mockError = 'invalid_grant'; - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + mockParseSignInCallbackUrl.mockReturnValueOnce({ code: mockCode, state: 'not_important_for_this_test', + error: null, + errorDescription: null, }); mockGetCookieValuesFromRequest.mockReturnValueOnce({ [STATE_COOKIE_NAME]: 'not_important_for_this_test', @@ -245,9 +262,11 @@ describe('handleSignInCallbackRequest', () => { mockCreateAuthFlowProofCookiesRemoveOptions.mockReturnValueOnce( mockCreateAuthFlowProofCookiesRemoveOptionsResult, ); - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + mockParseSignInCallbackUrl.mockReturnValueOnce({ code: mockCode, state: 'not_important_for_this_test', + error: null, + errorDescription: null, }); mockGetCookieValuesFromRequest.mockReturnValueOnce({ [STATE_COOKIE_NAME]: 'not_important_for_this_test', diff --git a/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequestForPagesRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequestForPagesRouter.test.ts index 5fd5612ae0c..c59c580c474 100644 --- a/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequestForPagesRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handlers/handleSignInCallbackRequestForPagesRouter.test.ts @@ -16,7 +16,7 @@ import { exchangeAuthNTokens, getCookieValuesFromNextApiRequest, getRedirectOrDefault, - resolveCodeAndStateFromUrl, + parseSignInCallbackUrl, resolveRedirectSignInUrl, } from '../../../src/auth/utils'; import { CreateAuthRoutesHandlersInput } from '../../../src/auth/types'; @@ -26,6 +26,11 @@ import { } from '../../../src/auth/constant'; import { createMockNextApiResponse } from '../testUtils'; +import { + ERROR_CLIENT_COOKIE_COMBINATIONS, + ERROR_URL_PARAMS_COMBINATIONS, +} from './signInCallbackErrorCombinations'; + jest.mock('../../../src/auth/utils'); const mockAppendSetCookieHeadersToNextApiResponse = jest.mocked( @@ -48,7 +53,7 @@ const mockExchangeAuthNTokens = jest.mocked(exchangeAuthNTokens); const mockGetCookieValuesFromNextApiRequest = jest.mocked( getCookieValuesFromNextApiRequest, ); -const mockResolveCodeAndStateFromUrl = jest.mocked(resolveCodeAndStateFromUrl); +const mockParseSignInCallbackUrl = jest.mocked(parseSignInCallbackUrl); const mockResolveRedirectSignInUrl = jest.mocked(resolveRedirectSignInUrl); const mockGetRedirectOrDefault = jest.mocked(getRedirectOrDefault); @@ -66,6 +71,7 @@ describe('handleSignInCallbackRequest', () => { mockResponseEnd, mockResponseStatus, mockResponseSend, + mockResponseRedirect, mockResponse, } = createMockNextApiResponse(); @@ -84,7 +90,7 @@ describe('handleSignInCallbackRequest', () => { mockCreateTokenCookiesSetOptions.mockClear(); mockExchangeAuthNTokens.mockClear(); mockGetCookieValuesFromNextApiRequest.mockClear(); - mockResolveCodeAndStateFromUrl.mockClear(); + mockParseSignInCallbackUrl.mockClear(); mockResolveRedirectSignInUrl.mockClear(); mockGetRedirectOrDefault.mockClear(); @@ -92,17 +98,24 @@ describe('handleSignInCallbackRequest', () => { mockResponseEnd.mockClear(); mockResponseStatus.mockClear(); mockResponseSend.mockClear(); + mockResponseRedirect.mockClear(); }); - test.each([ - [null, 'state'], - ['state', null], - ])( - 'returns a 400 response when request.url contains query params: code=%s, state=%s', - async (code, state) => { - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + test.each(ERROR_URL_PARAMS_COMBINATIONS)( + 'returns a $expectedStatus response when request.url contains query params: code=$code, state=$state, error=$error, error_description=$errorDescription', + async ({ + code, + state, + error, + errorDescription, + expectedStatus, + expectedRedirect, + }) => { + mockParseSignInCallbackUrl.mockReturnValueOnce({ code, state, + error, + errorDescription, }); const url = '/api/auth/sign-in-callback'; const mockRequest = { @@ -120,36 +133,40 @@ describe('handleSignInCallbackRequest', () => { origin: mockOrigin, }); - expect(mockResponseStatus).toHaveBeenCalledWith(400); - expect(mockResponseEnd).toHaveBeenCalled(); - expect(mockResolveCodeAndStateFromUrl).toHaveBeenCalledWith(url); + if (expectedStatus === 400) { + expect(mockResponseStatus).toHaveBeenCalledWith(expectedStatus); + expect(mockResponseEnd).toHaveBeenCalled(); + } else { + expect(mockGetRedirectOrDefault).toHaveBeenCalledWith( + mockHandlerInput.redirectOnSignOutComplete, + ); + expect(mockResponseRedirect).toHaveBeenCalledWith( + expectedStatus, + expectedRedirect, + ); + } + expect(mockParseSignInCallbackUrl).toHaveBeenCalledWith(url); }, ); - test.each([ - ['client state cookie is missing', undefined, 'state', 'pkce'], - [ - 'client cookie state a different value from the state query parameter', - 'state_different', - 'state', - 'pkce', - ], - ['client pkce cookie is missing', 'state', 'state', undefined], - ])( - `returns a 400 response when %s`, - async (_, clientState, state, clientPkce) => { - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + test.each(ERROR_CLIENT_COOKIE_COMBINATIONS)( + `returns a $expectedStatus response when client cookies are: state=$state, pkce=$pkce and expected state value is 'state_b'`, + async ({ state, pkce, expectedStatus, expectedRedirect }) => { + mockParseSignInCallbackUrl.mockReturnValueOnce({ code: 'not_important_for_this_test', - state, + state: 'not_important_for_this_test', + error: null, + errorDescription: null, }); mockGetCookieValuesFromNextApiRequest.mockReturnValueOnce({ - [STATE_COOKIE_NAME]: clientState, - [PKCE_COOKIE_NAME]: clientPkce, + [STATE_COOKIE_NAME]: state, + [PKCE_COOKIE_NAME]: pkce, }); + const expectedState = 'state_b'; - const url = `/api/auth/sign-in-callback?state=${state}&code=not_important_for_this_test`; + const url = `/api/auth/sign-in-callback?state=${expectedState}&code=not_important_for_this_test`; const mockRequest = { - query: { state }, + query: { state: expectedState }, url, } as unknown as NextApiRequest; @@ -163,9 +180,19 @@ describe('handleSignInCallbackRequest', () => { origin: mockOrigin, }); - expect(mockResponseStatus).toHaveBeenCalledWith(400); - expect(mockResponseEnd).toHaveBeenCalled(); - expect(mockResolveCodeAndStateFromUrl).toHaveBeenCalledWith(url); + if (expectedStatus === 400) { + expect(mockResponseStatus).toHaveBeenCalledWith(expectedStatus); + expect(mockResponseEnd).toHaveBeenCalled(); + } else { + expect(mockGetRedirectOrDefault).toHaveBeenCalledWith( + mockHandlerInput.redirectOnSignOutComplete, + ); + expect(mockResponseRedirect).toHaveBeenCalledWith( + expectedStatus, + expectedRedirect, + ); + } + expect(mockParseSignInCallbackUrl).toHaveBeenCalledWith(url); expect(mockGetCookieValuesFromNextApiRequest).toHaveBeenCalledWith( mockRequest, [PKCE_COOKIE_NAME, STATE_COOKIE_NAME], @@ -183,9 +210,11 @@ describe('handleSignInCallbackRequest', () => { query: {}, url: '/api/auth/sign-in-callback', } as unknown as NextApiRequest; - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + mockParseSignInCallbackUrl.mockReturnValueOnce({ code: mockCode, state: 'not_important_for_this_test', + error: null, + errorDescription: null, }); mockGetCookieValuesFromNextApiRequest.mockReturnValueOnce({ [STATE_COOKIE_NAME]: 'not_important_for_this_test', @@ -277,9 +306,11 @@ describe('handleSignInCallbackRequest', () => { mockCreateAuthFlowProofCookiesRemoveOptions.mockReturnValueOnce( mockCreateAuthFlowProofCookiesRemoveOptionsResult, ); - mockResolveCodeAndStateFromUrl.mockReturnValueOnce({ + mockParseSignInCallbackUrl.mockReturnValueOnce({ code: mockCode, state: 'not_important_for_this_test', + error: null, + errorDescription: null, }); mockGetCookieValuesFromNextApiRequest.mockReturnValueOnce({ [STATE_COOKIE_NAME]: 'not_important_for_this_test', diff --git a/packages/adapter-nextjs/__tests__/auth/handlers/signInCallbackErrorCombinations.ts b/packages/adapter-nextjs/__tests__/auth/handlers/signInCallbackErrorCombinations.ts new file mode 100644 index 00000000000..d8e545c38da --- /dev/null +++ b/packages/adapter-nextjs/__tests__/auth/handlers/signInCallbackErrorCombinations.ts @@ -0,0 +1,60 @@ +import { SIGN_IN_TIMEOUT_ERROR } from '../../../src/auth/constant'; + +export const ERROR_URL_PARAMS_COMBINATIONS = [ + { + code: null, + state: 'state', + error: null, + errorDescription: null, + expectedStatus: 400, + }, + { + code: 'code', + state: null, + error: null, + errorDescription: null, + expectedStatus: 400, + }, + { + code: null, + state: null, + error: null, + errorDescription: 'errorDescription', + expectedStatus: 302, + expectedRedirect: '/sign-in?error=errorDescription', + }, + { + code: null, + state: null, + error: 'error', + errorDescription: null, + expectedStatus: 302, + expectedRedirect: '/sign-in?error=error', + }, +]; + +export const ERROR_CLIENT_COOKIE_COMBINATIONS = [ + { + state: 'state_a', + pkce: 'pkce', + expectedStatus: 400, + }, + { + state: undefined, + pkce: undefined, + expectedStatus: 302, + expectedRedirect: `/sign-in?error=${SIGN_IN_TIMEOUT_ERROR}`, + }, + { + state: undefined, + pkce: 'pkce', + expectedStatus: 302, + expectedRedirect: `/sign-in?error=${SIGN_IN_TIMEOUT_ERROR}`, + }, + { + state: 'state', + pkce: undefined, + expectedStatus: 302, + expectedRedirect: `/sign-in?error=${SIGN_IN_TIMEOUT_ERROR}`, + }, +]; diff --git a/packages/adapter-nextjs/__tests__/auth/utils/parseSignInCallbackUrl.test.ts b/packages/adapter-nextjs/__tests__/auth/utils/parseSignInCallbackUrl.test.ts new file mode 100644 index 00000000000..157b9667f03 --- /dev/null +++ b/packages/adapter-nextjs/__tests__/auth/utils/parseSignInCallbackUrl.test.ts @@ -0,0 +1,16 @@ +import { parseSignInCallbackUrl } from '../../../src/auth/utils/parseSignInCallbackUrl'; + +describe('parseSignInCallbackUrl', () => { + it('returns the code and state from the url', () => { + const url = + 'https://example.com?code=123&state=456&error=789&error_description=abc'; + const result = parseSignInCallbackUrl(url); + + expect(result).toEqual({ + code: '123', + state: '456', + error: '789', + errorDescription: 'abc', + }); + }); +}); diff --git a/packages/adapter-nextjs/__tests__/auth/utils/resolveCodeAndStateFromUrl.test.ts b/packages/adapter-nextjs/__tests__/auth/utils/resolveCodeAndStateFromUrl.test.ts deleted file mode 100644 index 6b3194107ae..00000000000 --- a/packages/adapter-nextjs/__tests__/auth/utils/resolveCodeAndStateFromUrl.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { resolveCodeAndStateFromUrl } from '../../../src/auth/utils/resolveCodeAndStateFromUrl'; - -describe('resolveCodeAndStateFromUrl', () => { - it('returns the code and state from the url', () => { - const url = 'https://example.com?code=123&state=456'; - const result = resolveCodeAndStateFromUrl(url); - - expect(result).toEqual({ - code: '123', - state: '456', - }); - }); -}); diff --git a/packages/adapter-nextjs/src/auth/constant.ts b/packages/adapter-nextjs/src/auth/constant.ts index d269b06034b..500ee331e90 100644 --- a/packages/adapter-nextjs/src/auth/constant.ts +++ b/packages/adapter-nextjs/src/auth/constant.ts @@ -22,6 +22,16 @@ export const PKCE_COOKIE_NAME = 'com.amplify.server_auth.pkce'; export const STATE_COOKIE_NAME = 'com.amplify.server_auth.state'; export const IS_SIGNING_OUT_COOKIE_NAME = 'com.amplify.server_auth.isSigningOut'; -export const AUTH_FLOW_PROOF_MAX_AGE = 10 * 60; // 10 mins in seconds -export const REMOVE_COOKIE_MAX_AGE = -1; // -1 to remove the cookie immediately (0 ==> session cookie) + +// The 5 minutes is from the Cognito Social Identity Provider settings, see: +// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-social-idp.html +export const AUTH_FLOW_PROOF_MAX_AGE = 5 * 60; + +// -1 to remove the cookie immediately (0 ==> session cookie as observed) +export const REMOVE_COOKIE_MAX_AGE = -1; + +// With server-side auth flow, we don't support the less secure implicit flow export const OAUTH_GRANT_TYPE = 'authorization_code'; + +export const SIGN_IN_TIMEOUT_ERROR = + 'Sign in has to be completed within 5 minutes.'; diff --git a/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequest.ts b/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequest.ts index 69533724ed6..4804506ad0c 100644 --- a/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequest.ts +++ b/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequest.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { PKCE_COOKIE_NAME, STATE_COOKIE_NAME } from '../constant'; +import { + PKCE_COOKIE_NAME, + SIGN_IN_TIMEOUT_ERROR, + STATE_COOKIE_NAME, +} from '../constant'; import { appendSetCookieHeaders, createAuthFlowProofCookiesRemoveOptions, @@ -12,7 +16,7 @@ import { exchangeAuthNTokens, getCookieValuesFromRequest, getRedirectOrDefault, - resolveCodeAndStateFromUrl, + parseSignInCallbackUrl, resolveRedirectSignInUrl, } from '../utils'; @@ -26,14 +30,38 @@ export const handleSignInCallbackRequest: HandleSignInCallbackRequest = async ({ setCookieOptions, origin, }) => { - const { code, state } = resolveCodeAndStateFromUrl(request.url); + const { code, state, error, errorDescription } = parseSignInCallbackUrl( + request.url, + ); + + if (errorDescription || error) { + return new Response(null, { + status: 302, + headers: new Headers({ + location: `${getRedirectOrDefault(handlerInput.redirectOnSignOutComplete)}?error=${errorDescription || error}`, + }), + }); + } + if (!code || !state) { return new Response(null, { status: 400 }); } const { [PKCE_COOKIE_NAME]: clientPkce, [STATE_COOKIE_NAME]: clientState } = getCookieValuesFromRequest(request, [PKCE_COOKIE_NAME, STATE_COOKIE_NAME]); - if (!clientState || clientState !== state || !clientPkce) { + + // The state and pkce cookies are removed from cookie store after 5 minutes + if (!clientState || !clientPkce) { + return new Response(null, { + status: 302, + headers: new Headers({ + location: `${getRedirectOrDefault(handlerInput.redirectOnSignOutComplete)}?error=${SIGN_IN_TIMEOUT_ERROR}`, + }), + }); + } + + // Most likely the cookie has been tampered + if (clientState !== state) { return new Response(null, { status: 400 }); } diff --git a/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequestForPagesRouter.ts b/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequestForPagesRouter.ts index 843f931be21..376fa78ec23 100644 --- a/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequestForPagesRouter.ts +++ b/packages/adapter-nextjs/src/auth/handlers/handleSignInCallbackRequestForPagesRouter.ts @@ -1,7 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { PKCE_COOKIE_NAME, STATE_COOKIE_NAME } from '../constant'; +import { + PKCE_COOKIE_NAME, + SIGN_IN_TIMEOUT_ERROR, + STATE_COOKIE_NAME, +} from '../constant'; import { appendSetCookieHeadersToNextApiResponse, createAuthFlowProofCookiesRemoveOptions, @@ -12,7 +16,7 @@ import { exchangeAuthNTokens, getCookieValuesFromNextApiRequest, getRedirectOrDefault, - resolveCodeAndStateFromUrl, + parseSignInCallbackUrl, resolveRedirectSignInUrl, } from '../utils'; @@ -28,7 +32,19 @@ export const handleSignInCallbackRequestForPagesRouter: HandleSignInCallbackRequ setCookieOptions, origin, }) => { - const { code, state } = resolveCodeAndStateFromUrl(request.url!); + const { code, state, error, errorDescription } = parseSignInCallbackUrl( + request.url!, + ); + + if (errorDescription || error) { + response.redirect( + 302, + `${getRedirectOrDefault(handlerInput.redirectOnSignOutComplete)}?error=${errorDescription || error}`, + ); + + return; + } + if (!code || !state) { response.status(400).end(); @@ -41,7 +57,18 @@ export const handleSignInCallbackRequestForPagesRouter: HandleSignInCallbackRequ STATE_COOKIE_NAME, ]); - if (!clientState || clientState !== state || !clientPkce) { + // The state and pkce cookies are removed from cookie store after 5 minutes + if (!clientState || !clientPkce) { + response.redirect( + 302, + `${getRedirectOrDefault(handlerInput.redirectOnSignOutComplete)}?error=${SIGN_IN_TIMEOUT_ERROR}`, + ); + + return; + } + + // Most likely the cookie has been tampered + if (clientState !== state) { response.status(400).end(); return; diff --git a/packages/adapter-nextjs/src/auth/utils/index.ts b/packages/adapter-nextjs/src/auth/utils/index.ts index 31a57a12f5b..72c0f19f074 100644 --- a/packages/adapter-nextjs/src/auth/utils/index.ts +++ b/packages/adapter-nextjs/src/auth/utils/index.ts @@ -36,7 +36,7 @@ export { } from './hasActiveUserSession'; export { isSupportedAuthApiRoutePath } from './isSupportedAuthApiRoutePath'; export { isValidOrigin, isSSLOrigin } from './origin'; -export { resolveCodeAndStateFromUrl } from './resolveCodeAndStateFromUrl'; +export { parseSignInCallbackUrl } from './parseSignInCallbackUrl'; export { resolveIdentityProviderFromUrl } from './resolveIdentityProviderFromUrl'; export { resolveRedirectSignInUrl, diff --git a/packages/adapter-nextjs/src/auth/utils/resolveCodeAndStateFromUrl.ts b/packages/adapter-nextjs/src/auth/utils/parseSignInCallbackUrl.ts similarity index 62% rename from packages/adapter-nextjs/src/auth/utils/resolveCodeAndStateFromUrl.ts rename to packages/adapter-nextjs/src/auth/utils/parseSignInCallbackUrl.ts index 3f6f7f20916..f3918f22157 100644 --- a/packages/adapter-nextjs/src/auth/utils/resolveCodeAndStateFromUrl.ts +++ b/packages/adapter-nextjs/src/auth/utils/parseSignInCallbackUrl.ts @@ -3,12 +3,16 @@ import { getSearchParamValueFromUrl } from './getSearchParamValueFromUrl'; -export const resolveCodeAndStateFromUrl = ( +export const parseSignInCallbackUrl = ( urlStr: string, ): { code: string | null; state: string | null; + error: string | null; + errorDescription: string | null; } => ({ state: getSearchParamValueFromUrl(urlStr, 'state'), code: getSearchParamValueFromUrl(urlStr, 'code'), + error: getSearchParamValueFromUrl(urlStr, 'error'), + errorDescription: getSearchParamValueFromUrl(urlStr, 'error_description'), });