From 21a4f1e0e497d9a7f165dd2777129d26c9bba9f1 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:04:53 -0300 Subject: [PATCH 1/9] Introduce ALS store for request data within middleware runtime --- packages/nextjs/src/server/clerkClient.ts | 65 +++++++- packages/nextjs/src/server/clerkMiddleware.ts | 146 +++++++++--------- 2 files changed, 140 insertions(+), 71 deletions(-) diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 41b6559b5a3..7ef41d9f3c7 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -1,5 +1,10 @@ +import type { ClerkClient } from '@clerk/backend'; import { createClerkClient } from '@clerk/backend'; +import type { AuthenticateRequestOptions } from '@clerk/backend/internal'; +import { constants } from '@clerk/backend/internal'; +import { deprecated } from '@clerk/shared/deprecated'; +import { buildRequestLike } from '../app-router/server/utils'; import { API_URL, API_VERSION, @@ -12,8 +17,9 @@ import { TELEMETRY_DEBUG, TELEMETRY_DISABLED, } from './constants'; +import { decryptClerkRequestData, getHeader } from './utils'; -const clerkClient = createClerkClient({ +const clerkClientDefaultOptions = { secretKey: SECRET_KEY, publishableKey: PUBLISHABLE_KEY, apiUrl: API_URL, @@ -27,6 +33,63 @@ const clerkClient = createClerkClient({ disabled: TELEMETRY_DISABLED, debug: TELEMETRY_DEBUG, }, +}; + +export const clerkClientStorage = new AsyncLocalStorage>(); + +const createClerkClientWithOptions: typeof createClerkClient = options => + createClerkClient({ ...clerkClientDefaultOptions, ...options }); + +const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); + +/** + * @deprecated + * This singleton is deprecated and will be removed in a future release. Please use `clerkClient()` as a function instead. + */ +const clerkClientSingletonProxy = new Proxy(clerkClientSingleton, { + get(target, prop, receiver) { + deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); + + return Reflect.get(target, prop, receiver); + }, }); +/** + * Constructs a BAPI client that accesses request data within the runtime. + * Necessary if middleware dynamic keys are used. + */ +const clerkClientForRequest = () => { + let requestData: Partial | undefined; + + /** + * For BAPI client usage inside middleware runtime, fallbacks to AsyncLocalStorage to access request data + */ + const clerkClientStore = clerkClientStorage.getStore(); + if (clerkClientStore) { + requestData = clerkClientStore; + } else { + /** + * For BAPI usage from application server, fallbacks to access request data via `NextRequest` + */ + const request = buildRequestLike(); + const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData); + requestData = decryptClerkRequestData(encryptedRequestData); + } + + if (requestData?.secretKey || requestData?.publishableKey) { + return createClerkClientWithOptions({ + secretKey: requestData.secretKey, + publishableKey: requestData.publishableKey, + }); + } + + return clerkClient; +}; + +const clerkClient: ClerkClient & typeof clerkClientForRequest = Object.assign( + clerkClientForRequest, + // TODO SDK-1839 - Remove `clerkClient` singleton in the next major version of `@clerk/nextjs` + clerkClientSingletonProxy, +); + export { clerkClient }; diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 8d4a00dcad8..6de46fe0283 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -12,7 +12,7 @@ import { NextResponse } from 'next/server'; import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils'; import { withLogger } from '../utils/debugLogger'; -import { clerkClient } from './clerkClient'; +import { clerkClient, clerkClientStorage } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; import type { AuthProtect } from './protect'; @@ -91,82 +91,88 @@ export const clerkMiddleware: ClerkMiddleware = withLogger('clerkMiddleware', lo signUpUrl, }; - clerkClient.telemetry.record( - eventMethodCalled('clerkMiddleware', { - handler: Boolean(handler), - satellite: Boolean(options.isSatellite), - proxy: Boolean(options.proxyUrl), - }), - ); - - const nextMiddleware: NextMiddleware = async (request, event) => { - const clerkRequest = createClerkRequest(request); - logger.debug('options', options); - logger.debug('url', () => clerkRequest.toJSON()); - - const requestState = await clerkClient.authenticateRequest( - clerkRequest, - createAuthenticateRequestOptions(clerkRequest, options), + return clerkClientStorage.run(options, () => { + clerkClient().telemetry.record( + eventMethodCalled('clerkMiddleware', { + handler: Boolean(handler), + satellite: Boolean(options.isSatellite), + proxy: Boolean(options.proxyUrl), + }), ); - logger.debug('requestState', () => ({ - status: requestState.status, - headers: JSON.stringify(Object.fromEntries(requestState.headers)), - reason: requestState.reason, - })); - - const locationHeader = requestState.headers.get(constants.Headers.Location); - if (locationHeader) { - return new Response(null, { status: 307, headers: requestState.headers }); - } else if (requestState.status === AuthStatus.Handshake) { - throw new Error('Clerk: handshake status without redirect'); - } - - const authObject = requestState.toAuth(); - logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() })); + const nextMiddleware: NextMiddleware = async (request, event) => { + const clerkRequest = createClerkRequest(request); + logger.debug('options', options); + logger.debug('url', () => clerkRequest.toJSON()); - const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); - const protect = createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); - const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { protect, redirectToSignIn }); - - let handlerResult: Response = NextResponse.next(); - try { - handlerResult = (await handler?.(() => authObjWithMethods, request, event)) || handlerResult; - } catch (e: any) { - handlerResult = handleControlFlowErrors(e, clerkRequest, requestState); - } - - // TODO @nikos: we need to make this more generic - // and move the logic in clerk/backend - if (requestState.headers) { - requestState.headers.forEach((value, key) => { - handlerResult.headers.append(key, value); - }); - } + const requestState = await clerkClient().authenticateRequest( + clerkRequest, + createAuthenticateRequestOptions(clerkRequest, options), + ); - if (isRedirect(handlerResult)) { - logger.debug('handlerResult is redirect'); - return serverRedirectWithAuth(clerkRequest, handlerResult, options); - } + logger.debug('requestState', () => ({ + status: requestState.status, + headers: JSON.stringify(Object.fromEntries(requestState.headers)), + reason: requestState.reason, + })); + + const locationHeader = requestState.headers.get(constants.Headers.Location); + if (locationHeader) { + return new Response(null, { status: 307, headers: requestState.headers }); + } else if (requestState.status === AuthStatus.Handshake) { + throw new Error('Clerk: handshake status without redirect'); + } + + const authObject = requestState.toAuth(); + logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() })); + + const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); + const protect = createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); + const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { protect, redirectToSignIn }); + + let handlerResult: Response = NextResponse.next(); + try { + handlerResult = + (await clerkClientStorage.run( + options, + async () => await handler?.(() => authObjWithMethods, request, event), + )) || handlerResult; + } catch (e: any) { + handlerResult = handleControlFlowErrors(e, clerkRequest, requestState); + } + + // TODO @nikos: we need to make this more generic + // and move the logic in clerk/backend + if (requestState.headers) { + requestState.headers.forEach((value, key) => { + handlerResult.headers.append(key, value); + }); + } + + if (isRedirect(handlerResult)) { + logger.debug('handlerResult is redirect'); + return serverRedirectWithAuth(clerkRequest, handlerResult, options); + } + + if (options.debug) { + setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); + } + + decorateRequest(clerkRequest, handlerResult, requestState, options); + + return handlerResult; + }; - if (options.debug) { - setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' }); + // If we have a request and event, we're being called as a middleware directly + // eg, export default clerkMiddleware; + if (request && event) { + return nextMiddleware(request, event); } - decorateRequest(clerkRequest, handlerResult, requestState, { secretKey, signInUrl, signUpUrl, publishableKey }); - - return handlerResult; - }; - - // If we have a request and event, we're being called as a middleware directly - // eg, export default clerkMiddleware; - if (request && event) { - return nextMiddleware(request, event); - } - - // Otherwise, return a middleware that can be called with a request and event - // eg, export default clerkMiddleware(auth => { ... }); - return nextMiddleware; + // Otherwise, return a middleware that can be called with a request and event + // eg, export default clerkMiddleware(auth => { ... }); + return nextMiddleware; + }); }); const parseRequestAndEvent = (args: unknown[]) => { From 82f6fd23bf1675f41b9341e7f713f2a19012dbcc Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:08:06 -0300 Subject: [PATCH 2/9] Fetch dynamic keys from `currentUser` --- packages/nextjs/src/app-router/server/currentUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/app-router/server/currentUser.ts b/packages/nextjs/src/app-router/server/currentUser.ts index ae3a2090f85..4d7cb2d7be1 100644 --- a/packages/nextjs/src/app-router/server/currentUser.ts +++ b/packages/nextjs/src/app-router/server/currentUser.ts @@ -9,5 +9,5 @@ export async function currentUser(): Promise { return null; } - return clerkClient.users.getUser(userId); + return clerkClient().users.getUser(userId); } From 4a4b66c5f797a4650c88612241af67c6bf122ef5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:24:07 -0300 Subject: [PATCH 3/9] Call `clerkClient()` within tests --- integration/tests/dynamic-keys.test.ts | 73 +++++++++++++++++++ .../server/__tests__/authMiddleware.test.ts | 22 +++--- .../src/server/__tests__/clerkClient.test.ts | 2 +- .../server/__tests__/clerkMiddleware.test.ts | 34 ++++----- packages/nextjs/src/server/authMiddleware.ts | 4 +- packages/nextjs/src/server/clerkClient.ts | 6 +- 6 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 integration/tests/dynamic-keys.test.ts diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts new file mode 100644 index 00000000000..1fe831755d1 --- /dev/null +++ b/integration/tests/dynamic-keys.test.ts @@ -0,0 +1,73 @@ +import { test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; + +test.describe('dynamic keys @nextjs', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/middleware.ts', + () => `import { clerkMiddleware } from '@clerk/nextjs/server'; +import { NextResponse } from "next/server"; + +export default clerkMiddleware(); + +export const config = { + matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], +};`, + ) + .addFile( + 'src/app/provider.tsx', + () => `'use client' +import { ClerkProvider } from "@clerk/nextjs" + +export function Provider({ children }: { children: any }) { + return ( + + {children} + + ) +}`, + ) + .addFile( + 'src/app/layout.tsx', + () => `import './globals.css'; +import { Inter } from 'next/font/google'; +import { Provider } from './provider'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + `, + ) + .commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withEmailCodes); + await app.dev(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + // test('redirects to `signInUrl` on `auth.protect`') + // test('uses to `secretKey` for auth signature') +}); diff --git a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts index 31deb0b2be6..b474ffea8a0 100644 --- a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts @@ -228,7 +228,7 @@ describe('authMiddleware(params)', () => { })(mockRequest({ url: '/ignored' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).not.toBeCalled(); + expect(clerkClient().authenticateRequest).not.toBeCalled(); expect(beforeAuthSpy).not.toBeCalled(); expect(afterAuthSpy).not.toBeCalled(); }); @@ -243,7 +243,7 @@ describe('authMiddleware(params)', () => { })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); expect(beforeAuthSpy).toBeCalled(); expect(afterAuthSpy).toBeCalled(); }); @@ -310,7 +310,7 @@ describe('authMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('skip'); - expect(clerkClient.authenticateRequest).not.toBeCalled(); + expect(clerkClient().authenticateRequest).not.toBeCalled(); expect(afterAuthSpy).not.toBeCalled(); }); @@ -322,7 +322,7 @@ describe('authMiddleware(params)', () => { })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); expect(afterAuthSpy).toBeCalled(); }); @@ -335,7 +335,7 @@ describe('authMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/custom-redirect'); - expect(clerkClient.authenticateRequest).not.toBeCalled(); + expect(clerkClient().authenticateRequest).not.toBeCalled(); expect(afterAuthSpy).not.toBeCalled(); }); @@ -358,7 +358,7 @@ describe('authMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get('x-before-auth-header')).toEqual('before'); expect(resp?.headers.get('x-after-auth-header')).toEqual('after'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); @@ -372,7 +372,7 @@ describe('authMiddleware(params)', () => { expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('uses authenticateRequest result as auth', async () => { @@ -383,7 +383,7 @@ describe('authMiddleware(params)', () => { await authMiddleware({ afterAuth: afterAuthSpy })(req, event); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); expect(afterAuthSpy).toBeCalledWith( { userId: null, @@ -421,7 +421,7 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { @@ -433,7 +433,7 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set', async () => { @@ -443,7 +443,7 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://google.com/'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); diff --git a/packages/nextjs/src/server/__tests__/clerkClient.test.ts b/packages/nextjs/src/server/__tests__/clerkClient.test.ts index 1ceedefc0f7..58b6e883ddd 100644 --- a/packages/nextjs/src/server/__tests__/clerkClient.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkClient.test.ts @@ -4,7 +4,7 @@ import { clerkClient } from '../clerkClient'; describe('clerkClient', () => { it('should pass version package to userAgent', async () => { - await clerkClient.users.getUser('user_test'); + await clerkClient().users.getUser('user_test'); expect(global.fetch).toBeCalled(); expect((global.fetch as any).mock.calls[0][1].headers).toMatchObject({ diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index d4e19aa1609..2763d06f9ba 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -253,7 +253,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to sign-in url when redirectToSignIn is called with the correct returnBackUrl', async () => { @@ -270,7 +270,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toContain('/protected'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to sign-in url with redirect_url set to the provided returnBackUrl param', async () => { @@ -289,7 +289,7 @@ describe('clerkMiddleware(params)', () => { expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toEqual( 'https://www.clerk.com/hello', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to sign-in url without a redirect_url when returnBackUrl is null', async () => { @@ -306,7 +306,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull(); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); @@ -331,7 +331,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('does not redirect to sign-in url when protect is called, the user is signed in and the request is a page request', async () => { @@ -354,7 +354,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get('location')).toBeFalsy(); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('throws a not found error when protect is called, the user is signed out, and is not a page request', async () => { @@ -377,7 +377,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get(constants.Headers.AuthReason)).toContain('protect-rewrite'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('throws a not found error when protect is called with RBAC params the user does not fulfill, and is a page request', async () => { @@ -400,7 +400,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get(constants.Headers.AuthReason)).toContain('protect-rewrite'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthenticatedUrl when protect is called with the redirectUrl param, the user is signed out, and is a page request', async () => { @@ -424,7 +424,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/hello'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthorizedUrl when protect is called with the redirectUrl param, the user does not fulfill the RBAC params, and is a page request', async () => { @@ -454,7 +454,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/discover'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); @@ -479,7 +479,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('forwards headers from authenticateRequest when auth().protect() is called', async () => { @@ -507,7 +507,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.headers.get('X-Clerk-Auth')).toEqual('1'); expect(resp?.headers.get('Set-Cookie')).toEqual('session=;'); expect(resp?.headers.get('location')).toContain('sign-in'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthenticatedUrl when protect is called with the unauthenticatedUrl param, the user is signed out, and is a page request', async () => { @@ -534,7 +534,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthenticatedUrl'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('redirects to unauthorizedUrl when protect is called with the unauthorizedUrl param, the user is signed in but does not have permissions, and is a page request', async () => { @@ -564,7 +564,7 @@ describe('clerkMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthorizedUrl'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); }); @@ -592,7 +592,7 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { @@ -616,7 +616,7 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set (user-returned redirect)', async () => { @@ -642,6 +642,6 @@ describe('Dev Browser JWT when redirecting to cross origin for page requests', f expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient.authenticateRequest).toBeCalled(); + expect(clerkClient().authenticateRequest).toBeCalled(); }); }); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index f878d6c31c4..4ab37781d98 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -135,7 +135,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { const isApiRoute = createApiRoutes(options.apiRoutes); const defaultAfterAuth = createDefaultAfterAuth(isPublicRoute, isApiRoute, options); - clerkClient.telemetry.record( + clerkClient().telemetry.record( eventMethodCalled('authMiddleware', { publicRoutes: Boolean(options.publicRoutes), ignoredRoutes: Boolean(options.ignoredRoutes), @@ -184,7 +184,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { return setHeader(beforeAuthRes, constants.Headers.AuthReason, 'before-auth-redirect'); } - const requestState = await clerkClient.authenticateRequest( + const requestState = await clerkClient().authenticateRequest( clerkRequest, createAuthenticateRequestOptions(clerkRequest, options), ); diff --git a/packages/nextjs/src/server/clerkClient.ts b/packages/nextjs/src/server/clerkClient.ts index 7ef41d9f3c7..eb8006408ec 100644 --- a/packages/nextjs/src/server/clerkClient.ts +++ b/packages/nextjs/src/server/clerkClient.ts @@ -48,7 +48,9 @@ const clerkClientSingleton = createClerkClient(clerkClientDefaultOptions); */ const clerkClientSingletonProxy = new Proxy(clerkClientSingleton, { get(target, prop, receiver) { - deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); + if (Object.getPrototypeOf(target) === Object.getPrototypeOf(clerkClientSingleton)) { + deprecated('clerkClient object', 'Use `clerkClient()` as a function instead.'); + } return Reflect.get(target, prop, receiver); }, @@ -83,7 +85,7 @@ const clerkClientForRequest = () => { }); } - return clerkClient; + return clerkClientSingleton; }; const clerkClient: ClerkClient & typeof clerkClientForRequest = Object.assign( From 6d96f08364d8363b6bcdc4d3967fe8bf48a54e06 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:43:29 -0300 Subject: [PATCH 4/9] Rollback changes on `authMiddleware` --- .../server/__tests__/authMiddleware.test.ts | 22 +++++++++---------- packages/nextjs/src/server/authMiddleware.ts | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts index b474ffea8a0..31deb0b2be6 100644 --- a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts @@ -228,7 +228,7 @@ describe('authMiddleware(params)', () => { })(mockRequest({ url: '/ignored' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); - expect(clerkClient().authenticateRequest).not.toBeCalled(); + expect(clerkClient.authenticateRequest).not.toBeCalled(); expect(beforeAuthSpy).not.toBeCalled(); expect(afterAuthSpy).not.toBeCalled(); }); @@ -243,7 +243,7 @@ describe('authMiddleware(params)', () => { })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); expect(beforeAuthSpy).toBeCalled(); expect(afterAuthSpy).toBeCalled(); }); @@ -310,7 +310,7 @@ describe('authMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('skip'); - expect(clerkClient().authenticateRequest).not.toBeCalled(); + expect(clerkClient.authenticateRequest).not.toBeCalled(); expect(afterAuthSpy).not.toBeCalled(); }); @@ -322,7 +322,7 @@ describe('authMiddleware(params)', () => { })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); expect(resp?.status).toEqual(200); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); expect(afterAuthSpy).toBeCalled(); }); @@ -335,7 +335,7 @@ describe('authMiddleware(params)', () => { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/custom-redirect'); - expect(clerkClient().authenticateRequest).not.toBeCalled(); + expect(clerkClient.authenticateRequest).not.toBeCalled(); expect(afterAuthSpy).not.toBeCalled(); }); @@ -358,7 +358,7 @@ describe('authMiddleware(params)', () => { expect(resp?.status).toEqual(200); expect(resp?.headers.get('x-before-auth-header')).toEqual('before'); expect(resp?.headers.get('x-after-auth-header')).toEqual('after'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); }); }); @@ -372,7 +372,7 @@ describe('authMiddleware(params)', () => { expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); }); it('uses authenticateRequest result as auth', async () => { @@ -383,7 +383,7 @@ describe('authMiddleware(params)', () => { await authMiddleware({ afterAuth: afterAuthSpy })(req, event); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); expect(afterAuthSpy).toBeCalledWith( { userId: null, @@ -421,7 +421,7 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); }); it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { @@ -433,7 +433,7 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(resp?.headers.get('location')).toEqual( 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', ); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); }); it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set', async () => { @@ -443,7 +443,7 @@ describe('Dev Browser JWT when redirecting to cross origin', function () { expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toEqual('https://google.com/'); - expect(clerkClient().authenticateRequest).toBeCalled(); + expect(clerkClient.authenticateRequest).toBeCalled(); }); }); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index 4ab37781d98..f878d6c31c4 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -135,7 +135,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { const isApiRoute = createApiRoutes(options.apiRoutes); const defaultAfterAuth = createDefaultAfterAuth(isPublicRoute, isApiRoute, options); - clerkClient().telemetry.record( + clerkClient.telemetry.record( eventMethodCalled('authMiddleware', { publicRoutes: Boolean(options.publicRoutes), ignoredRoutes: Boolean(options.ignoredRoutes), @@ -184,7 +184,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { return setHeader(beforeAuthRes, constants.Headers.AuthReason, 'before-auth-redirect'); } - const requestState = await clerkClient().authenticateRequest( + const requestState = await clerkClient.authenticateRequest( clerkRequest, createAuthenticateRequestOptions(clerkRequest, options), ); From b10192749d652bb7b5ee353bbc85ffc47562a926 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:54:24 -0300 Subject: [PATCH 5/9] Run handshake tests --- integration/tests/dynamic-keys.test.ts | 63 ++++++++++---------------- integration/tests/handshake.test.ts | 2 +- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index 1fe831755d1..83d053dd1ac 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -2,6 +2,7 @@ import { test } from '@playwright/test'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; +import { createTestUtils } from '../testUtils'; test.describe('dynamic keys @nextjs', () => { test.describe.configure({ mode: 'parallel' }); @@ -12,48 +13,28 @@ test.describe('dynamic keys @nextjs', () => { .clone() .addFile( 'src/middleware.ts', - () => `import { clerkMiddleware } from '@clerk/nextjs/server'; -import { NextResponse } from "next/server"; + () => `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' -export default clerkMiddleware(); + const isProtectedRoute = createRouteMatcher(['/protected']); -export const config = { - matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], -};`, - ) - .addFile( - 'src/app/provider.tsx', - () => `'use client' -import { ClerkProvider } from "@clerk/nextjs" + export default clerkMiddleware((auth, request) => { + if (isProtectedRoute(request)) { + auth().protect(); + } + }, { + signInUrl: '/foobar' + }); -export function Provider({ children }: { children: any }) { - return ( - - {children} - - ) -}`, + export const config = { + matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], + };`, ) .addFile( - 'src/app/layout.tsx', - () => `import './globals.css'; -import { Inter } from 'next/font/google'; -import { Provider } from './provider'; - -const inter = Inter({ subsets: ['latin'] }); - -export const metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { + 'src/app/protected/page.tsx', + () => ` +export default function Page({ children }: { children: React.ReactNode }) { return ( - - - {children} - - +

Protected

); } `, @@ -68,6 +49,12 @@ export default function RootLayout({ children }: { children: React.ReactNode }) await app.teardown(); }); - // test('redirects to `signInUrl` on `auth.protect`') - // test('uses to `secretKey` for auth signature') + test('redirects to `signInUrl` on `auth.protect`', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToStart(); + await u.po.expect.toBeSignedOut(); + + await u.page.goToRelative('/protected'); + await u.page.waitForURL(/foobar/); + }); }); diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index 5057145ef21..7d8cd203e08 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -8,7 +8,7 @@ import { generateConfig, getJwksFromSecretKey } from '../testUtils/handshake'; const PORT = 4199; -test.skip('Client handshake @generic', () => { +test.describe('Client handshake @generic', () => { test.describe.configure({ mode: 'serial' }); let app: Application; From a0851a640db3e2c3b399e2696ffd68c88b8f4b6f Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:34:15 -0300 Subject: [PATCH 6/9] Add test for `signInUrl` --- integration/tests/dynamic-keys.test.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index 83d053dd1ac..8ec70b192d2 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -22,26 +22,36 @@ test.describe('dynamic keys @nextjs', () => { auth().protect(); } }, { + secretKey: process.env.CLERK_DYNAMIC_SECRET_KEY, signInUrl: '/foobar' }); export const config = { - matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], + matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };`, ) .addFile( 'src/app/protected/page.tsx', - () => ` + () => `import { SignedIn, SignedOut } from '@clerk/nextjs'; + export default function Page({ children }: { children: React.ReactNode }) { return ( -

Protected

+
+ + Signed in + + + + Signed out + +
); } `, ) .commit(); await app.setup(); - await app.withEnv(appConfigs.envs.withEmailCodes); + await app.withEnv(appConfigs.envs.withDynamicKeys); await app.dev(); }); @@ -51,10 +61,13 @@ export default function Page({ children }: { children: React.ReactNode }) { test('redirects to `signInUrl` on `auth.protect`', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); + await u.page.goToStart(); + await u.po.expect.toBeSignedOut(); await u.page.goToRelative('/protected'); + await u.page.waitForURL(/foobar/); }); }); From aa18e355e8677d019e7169bc719e0241861ee16a Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:23:51 -0300 Subject: [PATCH 7/9] Add test to resolve auth signature --- integration/presets/envs.ts | 29 +++++++++++----------- integration/tests/dynamic-keys.test.ts | 33 ++++++++++---------------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 13dad47aa48..8ef8d7e9651 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -29,8 +29,7 @@ const withEmailCodes = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withEmailLinks = environmentConfig() .setId('withEmailLinks') @@ -39,8 +38,7 @@ const withEmailLinks = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-links'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withCustomRoles = environmentConfig() .setId('withCustomRoles') @@ -51,8 +49,7 @@ const withCustomRoles = environmentConfig() .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-custom-roles'].pk) .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-up') - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withEmailCodesQuickstart = withEmailCodes .removeEnvVariable('public', 'CLERK_SIGN_IN_URL') @@ -63,30 +60,33 @@ const withAPCore1ClerkLatest = environmentConfig() .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withAPCore1ClerkV4 = environmentConfig() .setId('withAPCore1ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['with-email-codes'].sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['with-email-codes'].pk); const withAPCore2ClerkLatest = environmentConfig() .setId('withAPCore2ClerkLatest') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) - .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js') - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_JS_URL', constants.E2E_APP_CLERK_JS || 'http://localhost:18211/clerk.browser.js'); const withAPCore2ClerkV4 = environmentConfig() .setId('withAPCore2ClerkV4') .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true) .setEnvVariable('private', 'CLERK_SECRET_KEY', envKeys['core-2-all-enabled'].sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk) - .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.CLERK_ENCRYPTION_KEY); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', envKeys['core-2-all-enabled'].pk); + +const withDynamicKeys = withEmailCodes + .clone() + .setId('withDynamicKeys') + .setEnvVariable('private', 'CLERK_SECRET_KEY', '') + .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', envKeys['with-email-codes'].sk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY); export const envs = { withEmailCodes, @@ -97,4 +97,5 @@ export const envs = { withAPCore1ClerkV4, withAPCore2ClerkLatest, withAPCore2ClerkV4, + withDynamicKeys, } as const; diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index 8ec70b192d2..f0be5864de2 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -30,25 +30,6 @@ test.describe('dynamic keys @nextjs', () => { matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };`, ) - .addFile( - 'src/app/protected/page.tsx', - () => `import { SignedIn, SignedOut } from '@clerk/nextjs'; - -export default function Page({ children }: { children: React.ReactNode }) { - return ( -
- - Signed in - - - - Signed out - -
- ); -} - `, - ) .commit(); await app.setup(); await app.withEnv(appConfigs.envs.withDynamicKeys); @@ -59,7 +40,7 @@ export default function Page({ children }: { children: React.ReactNode }) { await app.teardown(); }); - test('redirects to `signInUrl` on `auth.protect`', async ({ page, context }) => { + test('redirects to `signInUrl` on `auth().protect()`', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToStart(); @@ -70,4 +51,16 @@ export default function Page({ children }: { children: React.ReactNode }) { await u.page.waitForURL(/foobar/); }); + + test('resolves auth signature with `secretKey` on `auth().protect()`', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.page.goToStart(); + + await u.po.expect.toBeSignedOut(); + + await u.page.goToRelative('/page-protected'); + + await u.page.waitForURL(/foobar/); + }); }); From 22c6e4572a27b1f64b111db47e40c973754ad4f7 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 27 Jun 2024 07:54:06 -0300 Subject: [PATCH 8/9] Add test to call `clerkClient` on application runtime --- integration/tests/dynamic-keys.test.ts | 47 ++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index f0be5864de2..c686527226d 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -1,12 +1,14 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; import { createTestUtils } from '../testUtils'; test.describe('dynamic keys @nextjs', () => { test.describe.configure({ mode: 'parallel' }); let app: Application; + let fakeUser: FakeUser; test.beforeAll(async () => { app = await appConfigs.next.appRouter @@ -30,16 +32,41 @@ test.describe('dynamic keys @nextjs', () => { matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'], };`, ) + .addFile( + 'src/app/current-user/page.tsx', + () => `import { currentUser } from '@clerk/nextjs/server' + + export default async function Page(){ + const user = await currentUser() + + return

{user ?

{user.firstName}

:

User not found

}

+ } + `, + ) .commit(); + await app.setup(); + + await app.withEnv(appConfigs.envs.withCustomRoles); + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser(); + await m.services.users.createBapiUser(fakeUser); + await app.withEnv(appConfigs.envs.withDynamicKeys); await app.dev(); }); test.afterAll(async () => { + await fakeUser.deleteIfExists(); await app.teardown(); }); + test.afterEach(async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.signOut(); + await u.page.context().clearCookies(); + }); + test('redirects to `signInUrl` on `auth().protect()`', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); @@ -56,11 +83,27 @@ test.describe('dynamic keys @nextjs', () => { const u = createTestUtils({ app, page, context }); await u.page.goToStart(); - await u.po.expect.toBeSignedOut(); await u.page.goToRelative('/page-protected'); await u.page.waitForURL(/foobar/); }); + + test('calls `clerkClient` with dynamic keys from application runtime', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.page.goToStart(); + await u.po.expect.toBeSignedOut(); + await u.page.goToRelative('/current-user'); + + await expect(u.page.getByText(/User not found/i)).toBeVisible(); + + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/current-user'); + await expect(u.page.getByText(/User not found/i)).toBeVisible(); + }); }); From 068938cd47877f30000da7164801c80ec8ac3365 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:30:49 -0300 Subject: [PATCH 9/9] Add test to call `clerkClient` on middleware runtime --- integration/tests/dynamic-keys.test.ts | 56 +++++++++++--------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index c686527226d..4c08e8656ce 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -2,27 +2,35 @@ import { expect, test } from '@playwright/test'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; -import type { FakeUser } from '../testUtils'; import { createTestUtils } from '../testUtils'; test.describe('dynamic keys @nextjs', () => { test.describe.configure({ mode: 'parallel' }); let app: Application; - let fakeUser: FakeUser; test.beforeAll(async () => { app = await appConfigs.next.appRouter .clone() .addFile( 'src/middleware.ts', - () => `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' + () => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' + import { NextResponse } from 'next/server' const isProtectedRoute = createRouteMatcher(['/protected']); + const shouldFetchBapi = createRouteMatcher(['/fetch-bapi-from-middleware']); - export default clerkMiddleware((auth, request) => { + export default clerkMiddleware(async (auth, request) => { if (isProtectedRoute(request)) { auth().protect(); } + + if (shouldFetchBapi(request)){ + const count = await clerkClient().users.getCount(); + + if (count){ + return NextResponse.redirect(new URL('/users-count', request.url)) + } + } }, { secretKey: process.env.CLERK_DYNAMIC_SECRET_KEY, signInUrl: '/foobar' @@ -33,31 +41,24 @@ test.describe('dynamic keys @nextjs', () => { };`, ) .addFile( - 'src/app/current-user/page.tsx', - () => `import { currentUser } from '@clerk/nextjs/server' + 'src/app/users-count/page.tsx', + () => `import { clerkClient } from '@clerk/nextjs/server' export default async function Page(){ - const user = await currentUser() + const count = await clerkClient().users.getCount() - return

{user ?

{user.firstName}

:

User not found

}

+ return

Users count: {count}

} `, ) .commit(); await app.setup(); - - await app.withEnv(appConfigs.envs.withCustomRoles); - const m = createTestUtils({ app }); - fakeUser = m.services.users.createFakeUser(); - await m.services.users.createBapiUser(fakeUser); - await app.withEnv(appConfigs.envs.withDynamicKeys); await app.dev(); }); test.afterAll(async () => { - await fakeUser.deleteIfExists(); await app.teardown(); }); @@ -81,29 +82,20 @@ test.describe('dynamic keys @nextjs', () => { test('resolves auth signature with `secretKey` on `auth().protect()`', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); - - await u.page.goToStart(); - await u.po.expect.toBeSignedOut(); - await u.page.goToRelative('/page-protected'); - await u.page.waitForURL(/foobar/); }); test('calls `clerkClient` with dynamic keys from application runtime', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/users-count'); + await expect(u.page.getByText(/Users count/i)).toBeVisible(); + }); - await u.page.goToStart(); - await u.po.expect.toBeSignedOut(); - await u.page.goToRelative('/current-user'); - - await expect(u.page.getByText(/User not found/i)).toBeVisible(); - - await u.po.signIn.goTo(); - await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); - await u.po.expect.toBeSignedIn(); - - await u.page.goToRelative('/current-user'); - await expect(u.page.getByText(/User not found/i)).toBeVisible(); + test('calls `clerkClient` with dynamic keys from middleware runtime', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/fetch-bapi-from-middleware'); + await u.page.waitForAppUrl('/users-count'); + await expect(u.page.getByText(/Users count/i)).toBeVisible(); }); });