From c8345c5544a188e829590a9f0d63cb91f6ce3055 Mon Sep 17 00:00:00 2001 From: Hui Zhao <zhohz@amazon.com> Date: Mon, 23 Sep 2024 14:29:21 -0700 Subject: [PATCH 1/2] feat(adapter-nextjs): add user has signed in check before initiating sign-in and sign-up --- .../createAuthRouteHandlersFactory.test.ts | 9 ++ ...dleAuthApiRouteRequestForAppRouter.test.ts | 59 ++++++++ ...eAuthApiRouteRequestForPagesRouter.test.ts | 56 ++++++++ .../auth/utils/hasUserSignedIn.test.ts | 128 ++++++++++++++++++ .../__tests__/createServerRunner.test.ts | 1 + .../auth/createAuthRouteHandlersFactory.ts | 3 + .../handleAuthApiRouteRequestForAppRouter.ts | 40 +++++- ...handleAuthApiRouteRequestForPagesRouter.ts | 33 ++++- packages/adapter-nextjs/src/auth/types.ts | 3 + .../src/auth/utils/hasUserSignedIn.ts | 56 ++++++++ .../adapter-nextjs/src/auth/utils/index.ts | 4 + .../adapter-nextjs/src/createServerRunner.ts | 11 +- 12 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts create mode 100644 packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts diff --git a/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts b/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts index e744fb0ca82..504807e04ec 100644 --- a/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/createAuthRouteHandlersFactory.test.ts @@ -63,6 +63,8 @@ const mockIsNextRequest = jest.mocked(isNextRequest); const mockIsAuthRoutesHandlersContext = jest.mocked( isAuthRoutesHandlersContext, ); +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction<NextServer.RunOperationWithContext>; describe('createAuthRoutesHandlersFactory', () => { const AMPLIFY_APP_ORIGIN = 'https://example.com'; @@ -73,6 +75,7 @@ describe('createAuthRoutesHandlersFactory', () => { config: mockAmplifyConfig, runtimeOptions: mockRuntimeOptions, amplifyAppOrigin: undefined, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }), ).toThrow('Could not find the AMPLIFY_APP_ORIGIN environment variable.'); }); @@ -82,6 +85,7 @@ describe('createAuthRoutesHandlersFactory', () => { config: mockAmplifyConfig, runtimeOptions: mockRuntimeOptions, amplifyAppOrigin: AMPLIFY_APP_ORIGIN, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockAssertTokenProviderConfig).toHaveBeenCalledWith( @@ -98,6 +102,7 @@ describe('createAuthRoutesHandlersFactory', () => { config: mockAmplifyConfig, runtimeOptions: mockRuntimeOptions, amplifyAppOrigin: AMPLIFY_APP_ORIGIN, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }; const testCreateAuthRoutesHandlersInput: CreateAuthRoutesHandlersInput = { customState: 'random-state', @@ -138,6 +143,7 @@ describe('createAuthRoutesHandlersFactory', () => { setCookieOptions: mockRuntimeOptions.cookies, origin: 'https://example.com', userPoolClientId: 'def', + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); }); @@ -159,6 +165,7 @@ describe('createAuthRoutesHandlersFactory', () => { setCookieOptions: mockRuntimeOptions.cookies, origin: 'https://example.com', userPoolClientId: 'def', + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); }); @@ -180,6 +187,7 @@ describe('createAuthRoutesHandlersFactory', () => { config: mockAmplifyConfig, runtimeOptions: undefined, amplifyAppOrigin: AMPLIFY_APP_ORIGIN, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); const handlerWithDefaultParamValues = createAuthRoutesHandlers(/* undefined */); @@ -202,6 +210,7 @@ describe('createAuthRoutesHandlersFactory', () => { setCookieOptions: {}, origin: 'https://example.com', userPoolClientId: 'def', + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); }); }); diff --git a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts index 58fdb69e5e4..a21143a965b 100644 --- a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts @@ -12,8 +12,14 @@ import { handleSignOutCallbackRequest, handleSignOutRequest, } from '../../src/auth/handlers'; +import { NextServer } from '../../src'; +import { + hasUserSignedInWithAppRouter, + isSupportedAuthApiRoutePath, +} from '../../src/auth/utils'; jest.mock('../../src/auth/handlers'); +jest.mock('../../src/auth/utils'); const mockHandleSignInSignUpRequest = jest.mocked(handleSignInSignUpRequest); const mockHandleSignOutRequest = jest.mocked(handleSignOutRequest); @@ -23,6 +29,14 @@ const mockHandleSignInCallbackRequest = jest.mocked( const mockHandleSignOutCallbackRequest = jest.mocked( handleSignOutCallbackRequest, ); +const mockHasUserSignedInWithAppRouter = jest.mocked( + hasUserSignedInWithAppRouter, +); +const mockIsSupportedAuthApiRoutePath = jest.mocked( + isSupportedAuthApiRoutePath, +); +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction<NextServer.RunOperationWithContext>; describe('handleAuthApiRouteRequestForAppRouter', () => { const testOrigin = 'https://example.com'; @@ -40,6 +54,11 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { }; const _ = handleAuthApiRouteRequestForAppRouter; + beforeAll(() => { + mockHasUserSignedInWithAppRouter.mockResolvedValue(false); + mockIsSupportedAuthApiRoutePath.mockReturnValue(true); + }); + it('returns a 405 response when input.request has an unsupported method', async () => { const request = new NextRequest( new URL('https://example.com/api/auth/sign-in'), @@ -55,6 +74,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response.status).toBe(405); @@ -75,6 +95,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response.status).toBe(400); @@ -87,6 +108,9 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { method: 'GET', }, ); + + mockIsSupportedAuthApiRoutePath.mockReturnValueOnce(false); + const response = await handleAuthApiRouteRequestForAppRouter({ request, handlerContext: { params: { slug: 'exchange-token' } }, @@ -95,6 +119,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response.status).toBe(404); @@ -124,6 +149,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); @@ -139,6 +165,36 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { }, ); + test.each([['sign-in'], ['sign-up']])( + `calls hasUserSignedInWithAppRouter with correct params when handlerContext.params.slug is %s, and when it returns true, the handler returns a 302 response`, + async slug => { + mockHasUserSignedInWithAppRouter.mockResolvedValueOnce(true); + const mockRequest = new NextRequest( + new URL('https://example.com/api/auth/sign-in'), + { + method: 'GET', + }, + ); + + const response = await handleAuthApiRouteRequestForAppRouter({ + request: mockRequest, + handlerContext: { params: { slug } }, + handlerInput: { + ...testHandlerInput, + redirectOnSignInComplete: undefined, + }, + userPoolClientId: 'userPoolClientId', + oAuthConfig: testOAuthConfig, + setCookieOptions: {}, + origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toBe('/'); + }, + ); + it('calls handleSignOutRequest with correct params when handlerContext.params.slug is sign-out', async () => { const mockRequest = new NextRequest( new URL('https://example.com/api/auth/sign-out'), @@ -158,6 +214,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); @@ -188,6 +245,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); @@ -220,6 +278,7 @@ describe('handleAuthApiRouteRequestForAppRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(response).toBe(mockResponse); diff --git a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts index 3561c8e8153..1e6603f71f5 100644 --- a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts @@ -9,10 +9,16 @@ import { handleSignOutCallbackRequestForPagesRouter, handleSignOutRequestForPagesRouter, } from '../../src/auth/handlers'; +import { NextServer } from '../../src'; +import { + hasUserSignedInWithPagesRouter, + isSupportedAuthApiRoutePath, +} from '../../src/auth/utils'; import { createMockNextApiResponse } from './testUtils'; jest.mock('../../src/auth/handlers'); +jest.mock('../../src/auth/utils'); const mockHandleSignInSignUpRequestForPagesRouter = jest.mocked( handleSignInSignUpRequestForPagesRouter, @@ -26,6 +32,14 @@ const mockHandleSignInCallbackRequestForPagesRouter = jest.mocked( const mockHandleSignOutCallbackRequestForPagesRouter = jest.mocked( handleSignOutCallbackRequestForPagesRouter, ); +const mockIsSupportedAuthApiRoutePath = jest.mocked( + isSupportedAuthApiRoutePath, +); +const mockHasUserSignedInWithPagesRouter = jest.mocked( + hasUserSignedInWithPagesRouter, +); +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction<NextServer.RunOperationWithContext>; describe('handleAuthApiRouteRequestForPagesRouter', () => { const testOrigin = 'https://example.com'; @@ -50,6 +64,11 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { mockResponse, } = createMockNextApiResponse(); + beforeAll(() => { + mockHasUserSignedInWithPagesRouter.mockResolvedValue(false); + mockIsSupportedAuthApiRoutePath.mockReturnValue(true); + }); + afterEach(() => { mockResponseAppendHeader.mockClear(); mockResponseEnd.mockClear(); @@ -69,6 +88,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: testSetCookieOptions, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockResponseStatus).toHaveBeenCalledWith(405); @@ -86,6 +106,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: testSetCookieOptions, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockResponseStatus).toHaveBeenCalledWith(400); @@ -98,6 +119,8 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { query: { slug: 'exchange-token' }, } as any; + mockIsSupportedAuthApiRoutePath.mockReturnValueOnce(false); + handleAuthApiRouteRequestForPagesRouter({ request: mockRequest, response: mockResponse, @@ -106,6 +129,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: testSetCookieOptions, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockResponseStatus).toHaveBeenCalledWith(404); @@ -132,6 +156,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignInSignUpRequestForPagesRouter).toHaveBeenCalledWith({ @@ -147,6 +172,34 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { }, ); + test.each([['sign-in'], ['sign-up']])( + `calls hasUserSignedInWithPagesRouter with correct params when handlerContext.params.slug is %s, and when it returns true, the handler returns a 302 response`, + async slug => { + mockHasUserSignedInWithPagesRouter.mockResolvedValueOnce(true); + const mockRequest = { + url: 'https://example.com/api/auth/sign-in', + method: 'GET', + query: { slug }, + } as unknown as NextApiRequest; + + await handleAuthApiRouteRequestForPagesRouter({ + request: mockRequest, + response: mockResponse, + handlerInput: { + ...testHandlerInput, + redirectOnSignInComplete: undefined, + }, + userPoolClientId: 'userPoolClientId', + oAuthConfig: testOAuthConfig, + setCookieOptions: {}, + origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(mockResponseRedirect).toHaveBeenCalledWith(302, '/'); + }, + ); + it('calls handleSignOutRequest with correct params when handlerContext.params.slug is sign-out', async () => { const mockRequest = { url: 'https://example.com/api/auth/sign-in', @@ -162,6 +215,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignOutRequestForPagesRouter).toHaveBeenCalledWith({ @@ -188,6 +242,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignInCallbackRequestForPagesRouter).toHaveBeenCalledWith({ @@ -216,6 +271,7 @@ describe('handleAuthApiRouteRequestForPagesRouter', () => { oAuthConfig: testOAuthConfig, setCookieOptions: {}, origin: testOrigin, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); expect(mockHandleSignOutCallbackRequestForPagesRouter).toHaveBeenCalledWith( diff --git a/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts b/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts new file mode 100644 index 00000000000..e1e6dd32575 --- /dev/null +++ b/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts @@ -0,0 +1,128 @@ +/** + * @jest-environment node + */ +import { getCurrentUser } from 'aws-amplify/auth/server'; +import { NextRequest } from 'next/server'; +import { AuthUser } from '@aws-amplify/auth/cognito'; +import { NextApiRequest } from 'next'; + +import { + hasUserSignedInWithAppRouter, + hasUserSignedInWithPagesRouter, +} from '../../../src/auth/utils/hasUserSignedIn'; +import { NextServer } from '../../../src/types'; +import { createMockNextApiResponse } from '../testUtils'; + +jest.mock('aws-amplify/auth/server'); + +const mockRunWithAmplifyServerContext = + jest.fn() as jest.MockedFunction<NextServer.RunOperationWithContext>; +const mockGetCurrentUser = jest.mocked(getCurrentUser); + +describe('hasUserSignedIn', () => { + const mockContextSpec = { token: { value: Symbol('mock') } }; + const mockCurrentUserResult: AuthUser = { + userId: 'mockUserId', + username: 'mockUsername', + }; + + beforeAll(() => { + mockRunWithAmplifyServerContext.mockImplementation( + async ({ nextServerContext: _, operation }) => { + return operation(mockContextSpec); + }, + ); + mockGetCurrentUser.mockResolvedValue(mockCurrentUserResult); + }); + + afterEach(() => { + mockRunWithAmplifyServerContext.mockClear(); + mockGetCurrentUser.mockClear(); + }); + + describe('hasUserSignedInWithAppRouter', () => { + const mockRequest = new NextRequest('https://example.com/api/auth/sign-in'); + + it('invokes server getCurrentUser() with expected parameter within the injected runWithAmplifyServerContext function', async () => { + await hasUserSignedInWithAppRouter({ + request: mockRequest, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(mockRunWithAmplifyServerContext).toHaveBeenCalledWith({ + nextServerContext: { + request: mockRequest, + response: expect.any(Response), + }, + operation: expect.any(Function), + }); + expect(mockGetCurrentUser).toHaveBeenCalledWith(mockContextSpec); + }); + + it('returns true when getCurrentUser() resolves (returned auth tokens)', async () => { + const result = await hasUserSignedInWithAppRouter({ + request: mockRequest, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(true); + }); + + it('returns false when getCurrentUser() rejects (no auth tokens)', async () => { + mockGetCurrentUser.mockRejectedValueOnce(new Error('No current user')); + + const result = await hasUserSignedInWithAppRouter({ + request: mockRequest, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(false); + }); + }); + + describe('hasUserSignedInWithPagesRouter', () => { + const mockRequest = { + url: 'https://example.com/api/auth/sign-in', + } as unknown as NextApiRequest; + const { mockResponse } = createMockNextApiResponse(); + + it('invokes server getCurrentUser() with expected parameter within the injected runWithAmplifyServerContext function', async () => { + await hasUserSignedInWithPagesRouter({ + request: mockRequest, + response: mockResponse, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(mockRunWithAmplifyServerContext).toHaveBeenCalledWith({ + nextServerContext: { + request: mockRequest, + response: mockResponse, + }, + operation: expect.any(Function), + }); + expect(mockGetCurrentUser).toHaveBeenCalledWith(mockContextSpec); + }); + + it('returns true when getCurrentUser() resolves (returned auth tokens)', async () => { + const result = await hasUserSignedInWithPagesRouter({ + request: mockRequest, + response: mockResponse, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(true); + }); + + it('returns false when getCurrentUser() rejects (no auth tokens)', async () => { + mockGetCurrentUser.mockRejectedValueOnce(new Error('No current user')); + + const result = await hasUserSignedInWithPagesRouter({ + request: mockRequest, + response: mockResponse, + runWithAmplifyServerContext: mockRunWithAmplifyServerContext, + }); + + expect(result).toBe(false); + }); + }); +}); diff --git a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts index a433389015f..99c120a2d2f 100644 --- a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts +++ b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts @@ -110,6 +110,7 @@ describe('createServerRunner', () => { config: mockAmplifyConfig, runtimeOptions: undefined, amplifyAppOrigin: AMPLIFY_APP_ORIGIN, + runWithAmplifyServerContext: expect.any(Function), }); expect(result).toMatchObject({ createAuthRouteHandlers: expect.any(Function), diff --git a/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts b/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts index a3de55c9701..582450280b3 100644 --- a/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts +++ b/packages/adapter-nextjs/src/auth/createAuthRouteHandlersFactory.ts @@ -28,6 +28,7 @@ export const createAuthRouteHandlersFactory = ({ config: resourcesConfig, runtimeOptions = {}, amplifyAppOrigin, + runWithAmplifyServerContext, }: CreateAuthRouteHandlersFactoryInput): CreateAuthRouteHandlers => { if (!amplifyAppOrigin) throw new AmplifyServerContextError({ @@ -61,6 +62,7 @@ export const createAuthRouteHandlersFactory = ({ oAuthConfig, setCookieOptions, origin: amplifyAppOrigin, + runWithAmplifyServerContext, }); // In the Pages Router, the final response is handled by contextOrResponse @@ -80,6 +82,7 @@ export const createAuthRouteHandlersFactory = ({ oAuthConfig, setCookieOptions, origin: amplifyAppOrigin, + runWithAmplifyServerContext, }); } diff --git a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts index af24955b71d..a37371a020a 100644 --- a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts +++ b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { HandleAuthApiRouteRequestForAppRouter } from './types'; -import { isSupportedAuthApiRoutePath } from './utils'; +import { + hasUserSignedInWithAppRouter, + isSupportedAuthApiRoutePath, +} from './utils'; import { handleSignInCallbackRequest, handleSignInSignUpRequest, @@ -19,6 +22,7 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor oAuthConfig, origin, setCookieOptions, + runWithAmplifyServerContext, }) => { if (request.method !== 'GET') { return new Response(null, { status: 405 }); @@ -35,7 +39,21 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor } switch (slug) { - case 'sign-up': + case 'sign-up': { + const hasUserSignedIn = await hasUserSignedInWithAppRouter({ + request, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + return new Response(null, { + status: 302, + headers: new Headers({ + Location: handlerInput.redirectOnSignInComplete ?? '/', + }), + }); + } + return handleSignInSignUpRequest({ request, userPoolClientId, @@ -45,7 +63,22 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor setCookieOptions, type: 'signUp', }); - case 'sign-in': + } + case 'sign-in': { + const hasUserSignedIn = await hasUserSignedInWithAppRouter({ + request, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + return new Response(null, { + status: 302, + headers: new Headers({ + Location: handlerInput.redirectOnSignInComplete ?? '/', + }), + }); + } + return handleSignInSignUpRequest({ request, userPoolClientId, @@ -55,6 +88,7 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor setCookieOptions, type: 'signIn', }); + } case 'sign-out': return handleSignOutRequest({ userPoolClientId, diff --git a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts index 6fe7db9c9c9..4bb3a3db7b2 100644 --- a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts +++ b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { HandleAuthApiRouteRequestForPagesRouter } from './types'; -import { isSupportedAuthApiRoutePath } from './utils'; +import { + hasUserSignedInWithPagesRouter, + isSupportedAuthApiRoutePath, +} from './utils'; import { handleSignInCallbackRequestForPagesRouter, handleSignInSignUpRequestForPagesRouter, @@ -19,6 +22,7 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF handlerInput, origin, setCookieOptions, + runWithAmplifyServerContext, }) => { if (request.method !== 'GET') { response.status(405).end(); @@ -41,7 +45,19 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF } switch (slug) { - case 'sign-up': + case 'sign-up': { + const hasUserSignedIn = await hasUserSignedInWithPagesRouter({ + request, + response, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + response.redirect(302, handlerInput.redirectOnSignInComplete ?? '/'); + + return; + } + handleSignInSignUpRequestForPagesRouter({ request, response, @@ -53,7 +69,20 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF type: 'signUp', }); break; + } case 'sign-in': { + const hasUserSignedIn = await hasUserSignedInWithPagesRouter({ + request, + response, + runWithAmplifyServerContext, + }); + + if (hasUserSignedIn) { + response.redirect(302, handlerInput.redirectOnSignInComplete ?? '/'); + + return; + } + handleSignInSignUpRequestForPagesRouter({ request, response, diff --git a/packages/adapter-nextjs/src/auth/types.ts b/packages/adapter-nextjs/src/auth/types.ts index 5d1edddc0d2..400c70edc31 100644 --- a/packages/adapter-nextjs/src/auth/types.ts +++ b/packages/adapter-nextjs/src/auth/types.ts @@ -74,6 +74,7 @@ export interface CreateAuthRouteHandlersFactoryInput { config: ResourcesConfig; runtimeOptions: NextServer.CreateServerRunnerRuntimeOptions | undefined; amplifyAppOrigin?: string; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; } export type CreateOAuthRouteHandlersFactory = ( @@ -92,12 +93,14 @@ interface HandleAuthApiRouteRequestForAppRouterInput extends HandleAuthApiRouteRequestInputBase { request: NextRequest; handlerContext: AuthRoutesHandlerContext; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; } interface HandleAuthApiRouteRequestForPagesRouterInput extends HandleAuthApiRouteRequestInputBase { request: NextApiRequest; response: NextApiResponse; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; } export type HandleAuthApiRouteRequestForAppRouter = ( diff --git a/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts b/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts new file mode 100644 index 00000000000..f3f7b3fe377 --- /dev/null +++ b/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { NextRequest } from 'next/server'; +import { getCurrentUser } from 'aws-amplify/auth/server'; +import { NextApiRequest, NextApiResponse } from 'next'; + +import { NextServer } from '../../types'; + +export const hasUserSignedInWithAppRouter = async ({ + request, + runWithAmplifyServerContext, +}: { + request: NextRequest; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; +}): Promise<boolean> => { + const dummyResponse = new Response(); + + try { + await runWithAmplifyServerContext({ + nextServerContext: { request, response: dummyResponse }, + operation(contextSpec) { + return getCurrentUser(contextSpec); + }, + }); + + return true; + } catch (_) { + // `getCurrentUser()` throws if there is no valid token + return false; + } +}; + +export const hasUserSignedInWithPagesRouter = async ({ + request, + response, + runWithAmplifyServerContext, +}: { + request: NextApiRequest; + response: NextApiResponse; + runWithAmplifyServerContext: NextServer.RunOperationWithContext; +}): Promise<boolean> => { + try { + await runWithAmplifyServerContext({ + nextServerContext: { request, response }, + operation(contextSpec) { + return getCurrentUser(contextSpec); + }, + }); + + return true; + } catch (_) { + // `getCurrentUser()` throws if there is no valid token + return false; + } +}; diff --git a/packages/adapter-nextjs/src/auth/utils/index.ts b/packages/adapter-nextjs/src/auth/utils/index.ts index a82d25f3dca..7d98d534a74 100644 --- a/packages/adapter-nextjs/src/auth/utils/index.ts +++ b/packages/adapter-nextjs/src/auth/utils/index.ts @@ -29,6 +29,10 @@ export { isNextApiResponse, isNextRequest, } from './predicates'; +export { + hasUserSignedInWithAppRouter, + hasUserSignedInWithPagesRouter, +} from './hasUserSignedIn'; export { isSupportedAuthApiRoutePath } from './isSupportedAuthApiRoutePath'; export { resolveCodeAndStateFromUrl } from './resolveCodeAndStateFromUrl'; export { resolveIdentityProviderFromUrl } from './resolveIdentityProviderFromUrl'; diff --git a/packages/adapter-nextjs/src/createServerRunner.ts b/packages/adapter-nextjs/src/createServerRunner.ts index d97c47067a4..a9ed7359b28 100644 --- a/packages/adapter-nextjs/src/createServerRunner.ts +++ b/packages/adapter-nextjs/src/createServerRunner.ts @@ -33,15 +33,18 @@ export const createServerRunner: NextServer.CreateServerRunner = ({ const amplifyConfig = parseAmplifyConfig(config); const amplifyAppOrigin = process.env.AMPLIFY_APP_ORIGIN; + const runWithAmplifyServerContext = createRunWithAmplifyServerContext({ + config: amplifyConfig, + runtimeOptions, + }); + return { - runWithAmplifyServerContext: createRunWithAmplifyServerContext({ - config: amplifyConfig, - runtimeOptions, - }), + runWithAmplifyServerContext, createAuthRouteHandlers: createAuthRouteHandlersFactory({ config: amplifyConfig, runtimeOptions, amplifyAppOrigin, + runWithAmplifyServerContext, }), }; }; From 3ec76df6c66eb90be752d24e797ea03ad2326002 Mon Sep 17 00:00:00 2001 From: Hui Zhao <zhohz@amazon.com> Date: Mon, 23 Dec 2024 15:26:21 -0800 Subject: [PATCH 2/2] chore(adapter-nextjs): rename hasUserSignedIn to hasActiveUserSession --- ...ndleAuthApiRouteRequestForAppRouter.test.ts | 4 ++-- ...leAuthApiRouteRequestForPagesRouter.test.ts | 4 ++-- ...In.test.ts => hasActiveUserSession.test.ts} | 18 +++++++++--------- .../handleAuthApiRouteRequestForAppRouter.ts | 10 +++++----- .../handleAuthApiRouteRequestForPagesRouter.ts | 10 +++++----- ...UserSignedIn.ts => hasActiveUserSession.ts} | 4 ++-- .../adapter-nextjs/src/auth/utils/index.ts | 6 +++--- 7 files changed, 28 insertions(+), 28 deletions(-) rename packages/adapter-nextjs/__tests__/auth/utils/{hasUserSignedIn.test.ts => hasActiveUserSession.test.ts} (88%) rename packages/adapter-nextjs/src/auth/utils/{hasUserSignedIn.ts => hasActiveUserSession.ts} (91%) diff --git a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts index a21143a965b..ccb48a358bf 100644 --- a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForAppRouter.test.ts @@ -14,7 +14,7 @@ import { } from '../../src/auth/handlers'; import { NextServer } from '../../src'; import { - hasUserSignedInWithAppRouter, + hasActiveUserSessionWithAppRouter, isSupportedAuthApiRoutePath, } from '../../src/auth/utils'; @@ -30,7 +30,7 @@ const mockHandleSignOutCallbackRequest = jest.mocked( handleSignOutCallbackRequest, ); const mockHasUserSignedInWithAppRouter = jest.mocked( - hasUserSignedInWithAppRouter, + hasActiveUserSessionWithAppRouter, ); const mockIsSupportedAuthApiRoutePath = jest.mocked( isSupportedAuthApiRoutePath, diff --git a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts index 1e6603f71f5..bc701c92c07 100644 --- a/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/handleAuthApiRouteRequestForPagesRouter.test.ts @@ -11,7 +11,7 @@ import { } from '../../src/auth/handlers'; import { NextServer } from '../../src'; import { - hasUserSignedInWithPagesRouter, + hasActiveUserSessionWithPagesRouter, isSupportedAuthApiRoutePath, } from '../../src/auth/utils'; @@ -36,7 +36,7 @@ const mockIsSupportedAuthApiRoutePath = jest.mocked( isSupportedAuthApiRoutePath, ); const mockHasUserSignedInWithPagesRouter = jest.mocked( - hasUserSignedInWithPagesRouter, + hasActiveUserSessionWithPagesRouter, ); const mockRunWithAmplifyServerContext = jest.fn() as jest.MockedFunction<NextServer.RunOperationWithContext>; diff --git a/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts b/packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts similarity index 88% rename from packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts rename to packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts index e1e6dd32575..23799e1dcf2 100644 --- a/packages/adapter-nextjs/__tests__/auth/utils/hasUserSignedIn.test.ts +++ b/packages/adapter-nextjs/__tests__/auth/utils/hasActiveUserSession.test.ts @@ -7,9 +7,9 @@ import { AuthUser } from '@aws-amplify/auth/cognito'; import { NextApiRequest } from 'next'; import { - hasUserSignedInWithAppRouter, - hasUserSignedInWithPagesRouter, -} from '../../../src/auth/utils/hasUserSignedIn'; + hasActiveUserSessionWithAppRouter, + hasActiveUserSessionWithPagesRouter, +} from '../../../src/auth/utils/hasActiveUserSession'; import { NextServer } from '../../../src/types'; import { createMockNextApiResponse } from '../testUtils'; @@ -44,7 +44,7 @@ describe('hasUserSignedIn', () => { const mockRequest = new NextRequest('https://example.com/api/auth/sign-in'); it('invokes server getCurrentUser() with expected parameter within the injected runWithAmplifyServerContext function', async () => { - await hasUserSignedInWithAppRouter({ + await hasActiveUserSessionWithAppRouter({ request: mockRequest, runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); @@ -60,7 +60,7 @@ describe('hasUserSignedIn', () => { }); it('returns true when getCurrentUser() resolves (returned auth tokens)', async () => { - const result = await hasUserSignedInWithAppRouter({ + const result = await hasActiveUserSessionWithAppRouter({ request: mockRequest, runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); @@ -71,7 +71,7 @@ describe('hasUserSignedIn', () => { it('returns false when getCurrentUser() rejects (no auth tokens)', async () => { mockGetCurrentUser.mockRejectedValueOnce(new Error('No current user')); - const result = await hasUserSignedInWithAppRouter({ + const result = await hasActiveUserSessionWithAppRouter({ request: mockRequest, runWithAmplifyServerContext: mockRunWithAmplifyServerContext, }); @@ -87,7 +87,7 @@ describe('hasUserSignedIn', () => { const { mockResponse } = createMockNextApiResponse(); it('invokes server getCurrentUser() with expected parameter within the injected runWithAmplifyServerContext function', async () => { - await hasUserSignedInWithPagesRouter({ + await hasActiveUserSessionWithPagesRouter({ request: mockRequest, response: mockResponse, runWithAmplifyServerContext: mockRunWithAmplifyServerContext, @@ -104,7 +104,7 @@ describe('hasUserSignedIn', () => { }); it('returns true when getCurrentUser() resolves (returned auth tokens)', async () => { - const result = await hasUserSignedInWithPagesRouter({ + const result = await hasActiveUserSessionWithPagesRouter({ request: mockRequest, response: mockResponse, runWithAmplifyServerContext: mockRunWithAmplifyServerContext, @@ -116,7 +116,7 @@ describe('hasUserSignedIn', () => { it('returns false when getCurrentUser() rejects (no auth tokens)', async () => { mockGetCurrentUser.mockRejectedValueOnce(new Error('No current user')); - const result = await hasUserSignedInWithPagesRouter({ + const result = await hasActiveUserSessionWithPagesRouter({ request: mockRequest, response: mockResponse, runWithAmplifyServerContext: mockRunWithAmplifyServerContext, diff --git a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts index a37371a020a..ebcf44e7b1a 100644 --- a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts +++ b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForAppRouter.ts @@ -3,7 +3,7 @@ import { HandleAuthApiRouteRequestForAppRouter } from './types'; import { - hasUserSignedInWithAppRouter, + hasActiveUserSessionWithAppRouter, isSupportedAuthApiRoutePath, } from './utils'; import { @@ -40,12 +40,12 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor switch (slug) { case 'sign-up': { - const hasUserSignedIn = await hasUserSignedInWithAppRouter({ + const hasActiveUserSession = await hasActiveUserSessionWithAppRouter({ request, runWithAmplifyServerContext, }); - if (hasUserSignedIn) { + if (hasActiveUserSession) { return new Response(null, { status: 302, headers: new Headers({ @@ -65,12 +65,12 @@ export const handleAuthApiRouteRequestForAppRouter: HandleAuthApiRouteRequestFor }); } case 'sign-in': { - const hasUserSignedIn = await hasUserSignedInWithAppRouter({ + const hasActiveUserSession = await hasActiveUserSessionWithAppRouter({ request, runWithAmplifyServerContext, }); - if (hasUserSignedIn) { + if (hasActiveUserSession) { return new Response(null, { status: 302, headers: new Headers({ diff --git a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts index 4bb3a3db7b2..2a80a8b5ab0 100644 --- a/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts +++ b/packages/adapter-nextjs/src/auth/handleAuthApiRouteRequestForPagesRouter.ts @@ -3,7 +3,7 @@ import { HandleAuthApiRouteRequestForPagesRouter } from './types'; import { - hasUserSignedInWithPagesRouter, + hasActiveUserSessionWithPagesRouter, isSupportedAuthApiRoutePath, } from './utils'; import { @@ -46,13 +46,13 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF switch (slug) { case 'sign-up': { - const hasUserSignedIn = await hasUserSignedInWithPagesRouter({ + const hasActiveUserSession = await hasActiveUserSessionWithPagesRouter({ request, response, runWithAmplifyServerContext, }); - if (hasUserSignedIn) { + if (hasActiveUserSession) { response.redirect(302, handlerInput.redirectOnSignInComplete ?? '/'); return; @@ -71,13 +71,13 @@ export const handleAuthApiRouteRequestForPagesRouter: HandleAuthApiRouteRequestF break; } case 'sign-in': { - const hasUserSignedIn = await hasUserSignedInWithPagesRouter({ + const hasActiveUserSession = await hasActiveUserSessionWithPagesRouter({ request, response, runWithAmplifyServerContext, }); - if (hasUserSignedIn) { + if (hasActiveUserSession) { response.redirect(302, handlerInput.redirectOnSignInComplete ?? '/'); return; diff --git a/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts b/packages/adapter-nextjs/src/auth/utils/hasActiveUserSession.ts similarity index 91% rename from packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts rename to packages/adapter-nextjs/src/auth/utils/hasActiveUserSession.ts index f3f7b3fe377..b9e6bd0f8cf 100644 --- a/packages/adapter-nextjs/src/auth/utils/hasUserSignedIn.ts +++ b/packages/adapter-nextjs/src/auth/utils/hasActiveUserSession.ts @@ -7,7 +7,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { NextServer } from '../../types'; -export const hasUserSignedInWithAppRouter = async ({ +export const hasActiveUserSessionWithAppRouter = async ({ request, runWithAmplifyServerContext, }: { @@ -31,7 +31,7 @@ export const hasUserSignedInWithAppRouter = async ({ } }; -export const hasUserSignedInWithPagesRouter = async ({ +export const hasActiveUserSessionWithPagesRouter = async ({ request, response, runWithAmplifyServerContext, diff --git a/packages/adapter-nextjs/src/auth/utils/index.ts b/packages/adapter-nextjs/src/auth/utils/index.ts index 7d98d534a74..ffff7b38103 100644 --- a/packages/adapter-nextjs/src/auth/utils/index.ts +++ b/packages/adapter-nextjs/src/auth/utils/index.ts @@ -30,9 +30,9 @@ export { isNextRequest, } from './predicates'; export { - hasUserSignedInWithAppRouter, - hasUserSignedInWithPagesRouter, -} from './hasUserSignedIn'; + hasActiveUserSessionWithAppRouter, + hasActiveUserSessionWithPagesRouter, +} from './hasActiveUserSession'; export { isSupportedAuthApiRoutePath } from './isSupportedAuthApiRoutePath'; export { resolveCodeAndStateFromUrl } from './resolveCodeAndStateFromUrl'; export { resolveIdentityProviderFromUrl } from './resolveIdentityProviderFromUrl';