From 25efe993892b21c14458d6a43fa5b7e16e0c31da Mon Sep 17 00:00:00 2001 From: Jeff Escalante Date: Fri, 15 May 2026 10:36:25 -0500 Subject: [PATCH 01/12] fix(fastify): use runtime keys for auth client --- .changeset/silent-fastify-handshakes.md | 10 ++++ .../src/server/__tests__/clerk-client.test.ts | 24 +++++++++ packages/astro/src/server/clerk-client.ts | 2 +- packages/astro/src/server/clerk-middleware.ts | 2 +- .../src/__tests__/clerkMiddleware.test.ts | 20 ++++++-- packages/express/src/authenticateRequest.ts | 21 +++++--- .../src/__tests__/withClerkMiddleware.test.ts | 49 ++++++++++++++++--- packages/fastify/src/withClerkMiddleware.ts | 18 +++++-- .../server/__tests__/clerkClient.test.ts | 16 ++++++ .../nuxt/src/runtime/server/clerkClient.ts | 4 +- .../src/runtime/server/clerkMiddleware.ts | 2 +- .../server/__tests__/clerkMiddleware.test.ts | 7 +++ .../src/server/clerkMiddleware.ts | 2 +- .../src/server/__tests__/clerkClient.test.ts | 24 +++++++++ .../src/server/clerkMiddleware.ts | 2 +- 15 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 .changeset/silent-fastify-handshakes.md create mode 100644 packages/astro/src/server/__tests__/clerk-client.test.ts create mode 100644 packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts diff --git a/.changeset/silent-fastify-handshakes.md b/.changeset/silent-fastify-handshakes.md new file mode 100644 index 00000000000..c40d8ee14d7 --- /dev/null +++ b/.changeset/silent-fastify-handshakes.md @@ -0,0 +1,10 @@ +--- +'@clerk/fastify': patch +'@clerk/express': patch +'@clerk/astro': patch +'@clerk/nuxt': patch +'@clerk/tanstack-react-start': patch +'@clerk/react-router': patch +--- + +Use runtime middleware keys when creating the request client used by server-side auth middleware, so nonce handshake payload exchange works when keys are passed directly to framework middleware. diff --git a/packages/astro/src/server/__tests__/clerk-client.test.ts b/packages/astro/src/server/__tests__/clerk-client.test.ts new file mode 100644 index 00000000000..9cd7501fcd4 --- /dev/null +++ b/packages/astro/src/server/__tests__/clerk-client.test.ts @@ -0,0 +1,24 @@ +import { createClerkClient } from '@clerk/backend'; +import { describe, expect, test, vi } from 'vitest'; + +import { clerkClient } from '../clerk-client'; + +vi.mock('@clerk/backend', () => ({ + createClerkClient: vi.fn().mockReturnValue({}), +})); + +describe('clerkClient', () => { + test('passes runtime options to createClerkClient', () => { + clerkClient({ locals: {} } as any, { + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }); + + expect(vi.mocked(createClerkClient)).toHaveBeenCalledWith( + expect.objectContaining({ + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }), + ); + }); +}); diff --git a/packages/astro/src/server/clerk-client.ts b/packages/astro/src/server/clerk-client.ts index 10b53c03a27..55d841483c8 100644 --- a/packages/astro/src/server/clerk-client.ts +++ b/packages/astro/src/server/clerk-client.ts @@ -29,6 +29,6 @@ const createClerkClientWithOptions: CreateClerkClientWithOptions = (context, opt ...options, }); -const clerkClient = (context: APIContext) => createClerkClientWithOptions(context); +const clerkClient = (context: APIContext, options?: ClerkOptions) => createClerkClientWithOptions(context, options); export { clerkClient }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 4c4e3082034..0382e7fc9bc 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -117,7 +117,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { } } - const requestState = await clerkClient(context).authenticateRequest( + const requestState = await clerkClient(context, keylessOptions).authenticateRequest( clerkRequest, createAuthenticateRequestOptions(clerkRequest, keylessOptions, context), ); diff --git a/packages/express/src/__tests__/clerkMiddleware.test.ts b/packages/express/src/__tests__/clerkMiddleware.test.ts index 82c8300dc37..da28dc85949 100644 --- a/packages/express/src/__tests__/clerkMiddleware.test.ts +++ b/packages/express/src/__tests__/clerkMiddleware.test.ts @@ -216,7 +216,7 @@ describe('clerkMiddleware', () => { expect(forwarded).not.toHaveProperty('frontendApiProxy'); }); - describe('apiUrl/apiVersion default-client construction', () => { + describe('default-client construction overrides', () => { beforeEach(() => { mockCreateClerkClient.mockClear(); }); @@ -243,8 +243,22 @@ describe('clerkMiddleware', () => { expect(mockCreateClerkClient).toHaveBeenCalledWith(expect.objectContaining({ apiVersion: 'v2' })); }); - it('does not call createClerkClient at construction when apiUrl/apiVersion are not set', () => { - authenticateAndDecorateRequest({ secretKey: 'sk_test_....' }); + it('builds a per-middleware ClerkClient with runtime keys when no custom clerkClient is supplied', () => { + authenticateAndDecorateRequest({ + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }); + + expect(mockCreateClerkClient).toHaveBeenCalledWith( + expect.objectContaining({ + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }), + ); + }); + + it('does not call createClerkClient at construction when client construction overrides are not set', () => { + authenticateAndDecorateRequest({}); expect(mockCreateClerkClient).not.toHaveBeenCalled(); }); diff --git a/packages/express/src/authenticateRequest.ts b/packages/express/src/authenticateRequest.ts index 606c80ddbd8..27d25bcac9f 100644 --- a/packages/express/src/authenticateRequest.ts +++ b/packages/express/src/authenticateRequest.ts @@ -112,21 +112,26 @@ const absoluteProxyUrl = (relativeOrAbsoluteUrl: string, baseUrl: string): strin return new URL(relativeOrAbsoluteUrl, baseUrl).toString(); }; -// `apiUrl` and `apiVersion` are pinned at client construction time inside -// `@clerk/backend`'s `createAuthenticateRequest` factory (build-time values -// override runtime ones). The default singleton in `./clerkClient` is built +// Some options are pinned at client construction time inside `@clerk/backend`'s +// `createAuthenticateRequest` factory, including the API client used for nonce +// handshake payload exchange. The default singleton in `./clerkClient` is built // from env only, so passing these via `clerkMiddleware()` would be silently -// ignored. When the caller hasn't supplied their own `clerkClient` but did -// pass `apiUrl`/`apiVersion`, build a per-middleware client with those values. +// ignored. When the caller hasn't supplied their own `clerkClient` but did pass +// client construction options, build a per-middleware client with those values. const resolveDefaultClerkClient = (options: ClerkMiddlewareOptions) => { - if (!options.apiUrl && !options.apiVersion) { + const { apiUrl, apiVersion, secretKey, machineSecretKey, publishableKey, jwtKey } = options; + if (!apiUrl && !apiVersion && !secretKey && !machineSecretKey && !publishableKey && !jwtKey) { return defaultClerkClient; } const env = { ...loadApiEnv(), ...loadClientEnv() }; return createClerkClient({ ...env, - ...(options.apiUrl ? { apiUrl: options.apiUrl } : {}), - ...(options.apiVersion ? { apiVersion: options.apiVersion } : {}), + ...(apiUrl ? { apiUrl } : {}), + ...(apiVersion ? { apiVersion } : {}), + ...(secretKey ? { secretKey } : {}), + ...(machineSecretKey ? { machineSecretKey } : {}), + ...(publishableKey ? { publishableKey } : {}), + ...(jwtKey ? { jwtKey } : {}), userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, }); }; diff --git a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts index d08316f99ef..4f6f389a775 100644 --- a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts +++ b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts @@ -4,17 +4,22 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { clerkPlugin, getAuth } from '../index'; -const authenticateRequestMock = vi.fn(); +const { authenticateRequestMock, createClerkClientMock } = vi.hoisted(() => { + const authenticateRequestMock = vi.fn(); + const createClerkClientMock = vi.fn(() => { + return { + authenticateRequest: (...args: any) => authenticateRequestMock(...args), + }; + }); + + return { authenticateRequestMock, createClerkClientMock }; +}); vi.mock('@clerk/backend', async () => { const actual = await vi.importActual('@clerk/backend'); return { ...actual, - createClerkClient: () => { - return { - authenticateRequest: (...args: any) => authenticateRequestMock(...args), - }; - }, + createClerkClient: (...args: any[]) => createClerkClientMock(...args), }; }); @@ -24,6 +29,38 @@ describe('withClerkMiddleware(options)', () => { vi.restoreAllMocks(); }); + test('creates the request client with plugin runtime keys', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: () => ({ + tokenType: 'session_token', + }), + }); + const fastify = Fastify(); + await fastify.register(clerkPlugin, { + secretKey: 'runtime_secret_key', + publishableKey: 'runtime_publishable_key', + }); + + fastify.get('/', (request: FastifyRequest, reply: FastifyReply) => { + const auth = getAuth(request); + reply.send({ auth }); + }); + + const response = await fastify.inject({ + method: 'GET', + path: '/', + }); + + expect(response.statusCode).toEqual(200); + expect(createClerkClientMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + secretKey: 'runtime_secret_key', + publishableKey: 'runtime_publishable_key', + }), + ); + }); + test('handles signin with Authorization Bearer', async () => { authenticateRequestMock.mockResolvedValueOnce({ headers: new Headers(), diff --git a/packages/fastify/src/withClerkMiddleware.ts b/packages/fastify/src/withClerkMiddleware.ts index bca237ce8d4..ea9b4e463b5 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -1,9 +1,9 @@ +import { createClerkClient } from '@clerk/backend'; import { AuthStatus } from '@clerk/backend/internal'; import { clerkFrontendApiProxy, DEFAULT_PROXY_PATH, stripTrailingSlashes } from '@clerk/backend/proxy'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { Readable } from 'stream'; -import { clerkClient } from './clerkClient'; import * as constants from './constants'; import type { ClerkFastifyOptions } from './types'; import { fastifyRequestToRequest, requestToProxyRequest } from './utils'; @@ -11,11 +11,21 @@ import { fastifyRequestToRequest, requestToProxyRequest } from './utils'; export const withClerkMiddleware = (options: ClerkFastifyOptions) => { const frontendApiProxy = options.frontendApiProxy; const proxyPath = stripTrailingSlashes(frontendApiProxy?.path ?? DEFAULT_PROXY_PATH) || DEFAULT_PROXY_PATH; + const publishableKey = options.publishableKey || constants.PUBLISHABLE_KEY; + const secretKey = options.secretKey || constants.SECRET_KEY; + const clerkClient = createClerkClient({ + ...options, + publishableKey, + secretKey, + machineSecretKey: options.machineSecretKey || constants.MACHINE_SECRET_KEY, + apiUrl: options.apiUrl || constants.API_URL, + apiVersion: options.apiVersion || constants.API_VERSION, + jwtKey: options.jwtKey || constants.JWT_KEY, + userAgent: options.userAgent || `${constants.SDK_METADATA.name}@${constants.SDK_METADATA.version}`, + sdkMetadata: options.sdkMetadata || constants.SDK_METADATA, + }); return async (fastifyRequest: FastifyRequest, reply: FastifyReply) => { - const publishableKey = options.publishableKey || constants.PUBLISHABLE_KEY; - const secretKey = options.secretKey || constants.SECRET_KEY; - // Handle Frontend API proxy requests and auto-derive proxyUrl let resolvedProxyUrl = options.proxyUrl; if (frontendApiProxy) { diff --git a/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts b/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts index 9e271597d77..a7ddddde709 100644 --- a/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts +++ b/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts @@ -90,4 +90,20 @@ describe('clerkClient', () => { }), ); }); + + it('passes runtime options to createClerkClient', () => { + mockRuntimeConfig(); + + clerkClient({} as any, { + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }); + + expect(createClerkClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }), + ); + }); }); diff --git a/packages/nuxt/src/runtime/server/clerkClient.ts b/packages/nuxt/src/runtime/server/clerkClient.ts index 70a58fcea79..5f31ecffce6 100644 --- a/packages/nuxt/src/runtime/server/clerkClient.ts +++ b/packages/nuxt/src/runtime/server/clerkClient.ts @@ -1,3 +1,4 @@ +import type { ClerkOptions } from '@clerk/backend'; import { createClerkClient } from '@clerk/backend'; import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; import { deprecated } from '@clerk/shared/deprecated'; @@ -28,7 +29,7 @@ function resolveApiVersion(runtimeConfig: ReturnType): return 'v1'; } -export function clerkClient(event: H3Event) { +export function clerkClient(event: H3Event, options?: ClerkOptions) { const runtimeConfig = useRuntimeConfig(event); return createClerkClient({ @@ -51,5 +52,6 @@ export function clerkClient(event: H3Event) { version: PACKAGE_VERSION, environment: process.env.NODE_ENV, }, + ...options, }); } diff --git a/packages/nuxt/src/runtime/server/clerkMiddleware.ts b/packages/nuxt/src/runtime/server/clerkMiddleware.ts index aae8e02305f..d4a0cc2ecf0 100644 --- a/packages/nuxt/src/runtime/server/clerkMiddleware.ts +++ b/packages/nuxt/src/runtime/server/clerkMiddleware.ts @@ -115,7 +115,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => { } } - const requestState = await clerkClient(event).authenticateRequest(clerkRequest, { + const requestState = await clerkClient(event, options).authenticateRequest(clerkRequest, { ...options, acceptsToken: 'any', }); diff --git a/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts b/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts index d067a2cb948..7d36e58146b 100644 --- a/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts @@ -62,6 +62,13 @@ describe('clerkMiddleware', () => { const result = await middleware(args, mockNext); + expect(mockClerkClient).toHaveBeenCalledWith( + args, + expect.objectContaining({ + secretKey: 'sk_test_...', + publishableKey: 'pk_test_...', + }), + ); expect(mockAuthenticateRequest).toHaveBeenCalledWith(expect.any(Object), { apiUrl: undefined, secretKey: 'sk_test_...', diff --git a/packages/react-router/src/server/clerkMiddleware.ts b/packages/react-router/src/server/clerkMiddleware.ts index b78f38e05c9..78d4b64bc11 100644 --- a/packages/react-router/src/server/clerkMiddleware.ts +++ b/packages/react-router/src/server/clerkMiddleware.ts @@ -66,7 +66,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareFun organizationSyncOptions, } = loadedOptions; - const requestState = await clerkClient(args, options).authenticateRequest(clerkRequest, { + const requestState = await clerkClient(args, loadedOptions).authenticateRequest(clerkRequest, { apiUrl, secretKey: loadedOptions.secretKey, jwtKey, diff --git a/packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts b/packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts new file mode 100644 index 00000000000..e83b30ad773 --- /dev/null +++ b/packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts @@ -0,0 +1,24 @@ +import { createClerkClient } from '@clerk/backend'; +import { describe, expect, test, vi } from 'vitest'; + +import { clerkClient } from '../clerkClient'; + +vi.mock('@clerk/backend', () => ({ + createClerkClient: vi.fn().mockReturnValue({}), +})); + +describe('clerkClient', () => { + test('passes runtime options to createClerkClient', () => { + clerkClient({ + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }); + + expect(vi.mocked(createClerkClient)).toHaveBeenCalledWith( + expect.objectContaining({ + secretKey: 'sk_test_runtime', + publishableKey: 'pk_test_runtime', + }), + ); + }); +}); diff --git a/packages/tanstack-react-start/src/server/clerkMiddleware.ts b/packages/tanstack-react-start/src/server/clerkMiddleware.ts index 4b9b69b659c..468cd43894d 100644 --- a/packages/tanstack-react-start/src/server/clerkMiddleware.ts +++ b/packages/tanstack-react-start/src/server/clerkMiddleware.ts @@ -43,7 +43,7 @@ export const clerkMiddleware = ( loadedOptions.secretKey = secretKey; } - const requestState = await clerkClient().authenticateRequest(clerkRequest, { + const requestState = await clerkClient(loadedOptions).authenticateRequest(clerkRequest, { ...loadedOptions, acceptsToken: 'any', }); From 19f3473968eddbde3159aa3c35f80518b1d280ef Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 15 May 2026 12:03:26 -0700 Subject: [PATCH 02/12] feat(fastify): Add enableHandshake option to plugin --- packages/fastify/src/types.ts | 8 ++++++++ packages/fastify/src/withClerkMiddleware.ts | 13 ++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/fastify/src/types.ts b/packages/fastify/src/types.ts index 7b1224ea271..98e50544613 100644 --- a/packages/fastify/src/types.ts +++ b/packages/fastify/src/types.ts @@ -21,4 +21,12 @@ export interface FrontendApiProxyOptions { export type ClerkFastifyOptions = ClerkOptions & { hookName?: (typeof ALLOWED_HOOKS)[number]; frontendApiProxy?: FrontendApiProxyOptions; + /** + * Whether to enable the handshake flow for session verification. + * Disable this when using Clerk with a first-party API backend (e.g. a SPA calling + * a Fastify server) to prevent handshake nonce cookies set during OAuth callbacks + * from blocking authentication on subsequent API requests. + * @default true + */ + enableHandshake?: boolean; }; diff --git a/packages/fastify/src/withClerkMiddleware.ts b/packages/fastify/src/withClerkMiddleware.ts index ea9b4e463b5..54640cc4c6e 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -24,6 +24,7 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => { userAgent: options.userAgent || `${constants.SDK_METADATA.name}@${constants.SDK_METADATA.version}`, sdkMetadata: options.sdkMetadata || constants.SDK_METADATA, }); + const enableHandshake = options.enableHandshake ?? true; return async (fastifyRequest: FastifyRequest, reply: FastifyReply) => { // Handle Frontend API proxy requests and auto-derive proxyUrl @@ -94,11 +95,13 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => { requestState.headers.forEach((value, key) => reply.header(key, value)); - const locationHeader = requestState.headers.get(constants.Headers.Location); - if (locationHeader) { - return reply.code(307).send(); - } else if (requestState.status === AuthStatus.Handshake) { - throw new Error('Clerk: handshake status without redirect'); + if (enableHandshake) { + const locationHeader = requestState.headers.get(constants.Headers.Location); + if (locationHeader) { + return reply.code(307).send(); + } else if (requestState.status === AuthStatus.Handshake) { + throw new Error('Clerk: handshake status without redirect'); + } } // @ts-expect-error Inject auth so getAuth can read it From 0cc1e3c32f4fea428e50aec001e5ee5d3b99cae2 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 15 May 2026 12:03:52 -0700 Subject: [PATCH 03/12] add test --- .../src/__tests__/withClerkMiddleware.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts index 4f6f389a775..c5999205fd0 100644 --- a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts +++ b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts @@ -179,6 +179,37 @@ describe('withClerkMiddleware(options)', () => { }); }); + test('skips handshake redirect when enableHandshake is false', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + status: 'handshake', + headers: new Headers({ + location: 'https://fapi.example.com/v1/clients/handshake', + 'x-clerk-auth-status': 'handshake', + }), + toAuth: () => ({ + tokenType: 'session_token', + }), + }); + const fastify = Fastify(); + await fastify.register(clerkPlugin, { enableHandshake: false }); + + fastify.get('/', (request: FastifyRequest, reply: FastifyReply) => { + const auth = getAuth(request); + reply.send({ auth }); + }); + + const response = await fastify.inject({ + method: 'GET', + path: '/', + headers: { + cookie: '__clerk_handshake_nonce=deadbeef; __client_uat=1675692233', + }, + }); + + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual(JSON.stringify({ auth: { tokenType: 'session_token' } })); + }); + test('handles signout case by populating the req.auth', async () => { authenticateRequestMock.mockResolvedValueOnce({ headers: new Headers(), From 60312d14ddc2d5862d9617a6ce38d57a9e83e13c Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 15 May 2026 12:04:04 -0700 Subject: [PATCH 04/12] add changeset --- .changeset/sixty-lobsters-jump.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-lobsters-jump.md diff --git a/.changeset/sixty-lobsters-jump.md b/.changeset/sixty-lobsters-jump.md new file mode 100644 index 00000000000..e031a69b0ca --- /dev/null +++ b/.changeset/sixty-lobsters-jump.md @@ -0,0 +1,5 @@ +--- +"@clerk/fastify": patch +--- + +Add enableHandshake option to plugin From 9243a0a59e8e63b444af4cc7e53f28f7cdd4b008 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 21:22:58 +0000 Subject: [PATCH 05/12] fix(react-router): avoid proxy/domain type conflict in authenticateRequest options Agent-Logs-Url: https://github.com/clerk/javascript/sessions/facfca5a-129e-4332-97a6-caa1efbb40cd --- packages/react-router/src/server/clerkMiddleware.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-router/src/server/clerkMiddleware.ts b/packages/react-router/src/server/clerkMiddleware.ts index 78d4b64bc11..e8edccfd8b5 100644 --- a/packages/react-router/src/server/clerkMiddleware.ts +++ b/packages/react-router/src/server/clerkMiddleware.ts @@ -65,14 +65,13 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareFun signUpUrl, organizationSyncOptions, } = loadedOptions; + const multiDomainOrProxyOptions = proxyUrl ? { proxyUrl, isSatellite } : { domain, isSatellite }; const requestState = await clerkClient(args, loadedOptions).authenticateRequest(clerkRequest, { apiUrl, secretKey: loadedOptions.secretKey, jwtKey, - proxyUrl, - isSatellite, - domain, + ...multiDomainOrProxyOptions, publishableKey: loadedOptions.publishableKey, machineSecretKey, audience, From a79f78665b97ac8b852840d1086f855aa8b836d0 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 15 May 2026 16:11:57 -0700 Subject: [PATCH 06/12] chore: remove react-router changes --- .../src/server/__tests__/clerkMiddleware.test.ts | 7 ------- packages/react-router/src/server/clerkMiddleware.ts | 7 ++++--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts b/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts index 7d36e58146b..d067a2cb948 100644 --- a/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/react-router/src/server/__tests__/clerkMiddleware.test.ts @@ -62,13 +62,6 @@ describe('clerkMiddleware', () => { const result = await middleware(args, mockNext); - expect(mockClerkClient).toHaveBeenCalledWith( - args, - expect.objectContaining({ - secretKey: 'sk_test_...', - publishableKey: 'pk_test_...', - }), - ); expect(mockAuthenticateRequest).toHaveBeenCalledWith(expect.any(Object), { apiUrl: undefined, secretKey: 'sk_test_...', diff --git a/packages/react-router/src/server/clerkMiddleware.ts b/packages/react-router/src/server/clerkMiddleware.ts index e8edccfd8b5..b78f38e05c9 100644 --- a/packages/react-router/src/server/clerkMiddleware.ts +++ b/packages/react-router/src/server/clerkMiddleware.ts @@ -65,13 +65,14 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareFun signUpUrl, organizationSyncOptions, } = loadedOptions; - const multiDomainOrProxyOptions = proxyUrl ? { proxyUrl, isSatellite } : { domain, isSatellite }; - const requestState = await clerkClient(args, loadedOptions).authenticateRequest(clerkRequest, { + const requestState = await clerkClient(args, options).authenticateRequest(clerkRequest, { apiUrl, secretKey: loadedOptions.secretKey, jwtKey, - ...multiDomainOrProxyOptions, + proxyUrl, + isSatellite, + domain, publishableKey: loadedOptions.publishableKey, machineSecretKey, audience, From c6ba26ef5ddf59dfde9f381cfbfb52c3218c1c21 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 15 May 2026 16:30:38 -0700 Subject: [PATCH 07/12] chore(fastify): clean up branch to fastify-only changes, expose request.clerk --- .changeset/silent-fastify-handshakes.md | 7 +--- .../src/server/__tests__/clerk-client.test.ts | 24 -------------- packages/astro/src/server/clerk-client.ts | 2 +- packages/astro/src/server/clerk-middleware.ts | 2 +- .../src/__tests__/clerkMiddleware.test.ts | 20 ++---------- packages/express/src/authenticateRequest.ts | 21 +++++------- .../src/__tests__/withClerkMiddleware.test.ts | 32 ++++++++++++++----- packages/fastify/src/clerkPlugin.ts | 2 ++ packages/fastify/src/types.ts | 8 ++++- packages/fastify/src/withClerkMiddleware.ts | 1 + .../server/__tests__/clerkClient.test.ts | 16 ---------- .../nuxt/src/runtime/server/clerkClient.ts | 4 +-- .../src/runtime/server/clerkMiddleware.ts | 2 +- .../src/server/__tests__/clerkClient.test.ts | 24 -------------- .../src/server/clerkMiddleware.ts | 2 +- 15 files changed, 51 insertions(+), 116 deletions(-) delete mode 100644 packages/astro/src/server/__tests__/clerk-client.test.ts delete mode 100644 packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts diff --git a/.changeset/silent-fastify-handshakes.md b/.changeset/silent-fastify-handshakes.md index c40d8ee14d7..1300ae290f6 100644 --- a/.changeset/silent-fastify-handshakes.md +++ b/.changeset/silent-fastify-handshakes.md @@ -1,10 +1,5 @@ --- '@clerk/fastify': patch -'@clerk/express': patch -'@clerk/astro': patch -'@clerk/nuxt': patch -'@clerk/tanstack-react-start': patch -'@clerk/react-router': patch --- -Use runtime middleware keys when creating the request client used by server-side auth middleware, so nonce handshake payload exchange works when keys are passed directly to framework middleware. +Use runtime middleware keys when creating the auth client, so secrets passed directly to `clerkPlugin()` are used for handshake nonce exchange. Also exposes the per-request `ClerkClient` instance on `request.clerk`, and adds an `enableHandshake` option to opt out of handshake redirects when using Clerk as a first-party API backend. diff --git a/packages/astro/src/server/__tests__/clerk-client.test.ts b/packages/astro/src/server/__tests__/clerk-client.test.ts deleted file mode 100644 index 9cd7501fcd4..00000000000 --- a/packages/astro/src/server/__tests__/clerk-client.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createClerkClient } from '@clerk/backend'; -import { describe, expect, test, vi } from 'vitest'; - -import { clerkClient } from '../clerk-client'; - -vi.mock('@clerk/backend', () => ({ - createClerkClient: vi.fn().mockReturnValue({}), -})); - -describe('clerkClient', () => { - test('passes runtime options to createClerkClient', () => { - clerkClient({ locals: {} } as any, { - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }); - - expect(vi.mocked(createClerkClient)).toHaveBeenCalledWith( - expect.objectContaining({ - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }), - ); - }); -}); diff --git a/packages/astro/src/server/clerk-client.ts b/packages/astro/src/server/clerk-client.ts index 55d841483c8..10b53c03a27 100644 --- a/packages/astro/src/server/clerk-client.ts +++ b/packages/astro/src/server/clerk-client.ts @@ -29,6 +29,6 @@ const createClerkClientWithOptions: CreateClerkClientWithOptions = (context, opt ...options, }); -const clerkClient = (context: APIContext, options?: ClerkOptions) => createClerkClientWithOptions(context, options); +const clerkClient = (context: APIContext) => createClerkClientWithOptions(context); export { clerkClient }; diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index 0382e7fc9bc..4c4e3082034 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -117,7 +117,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { } } - const requestState = await clerkClient(context, keylessOptions).authenticateRequest( + const requestState = await clerkClient(context).authenticateRequest( clerkRequest, createAuthenticateRequestOptions(clerkRequest, keylessOptions, context), ); diff --git a/packages/express/src/__tests__/clerkMiddleware.test.ts b/packages/express/src/__tests__/clerkMiddleware.test.ts index da28dc85949..82c8300dc37 100644 --- a/packages/express/src/__tests__/clerkMiddleware.test.ts +++ b/packages/express/src/__tests__/clerkMiddleware.test.ts @@ -216,7 +216,7 @@ describe('clerkMiddleware', () => { expect(forwarded).not.toHaveProperty('frontendApiProxy'); }); - describe('default-client construction overrides', () => { + describe('apiUrl/apiVersion default-client construction', () => { beforeEach(() => { mockCreateClerkClient.mockClear(); }); @@ -243,22 +243,8 @@ describe('clerkMiddleware', () => { expect(mockCreateClerkClient).toHaveBeenCalledWith(expect.objectContaining({ apiVersion: 'v2' })); }); - it('builds a per-middleware ClerkClient with runtime keys when no custom clerkClient is supplied', () => { - authenticateAndDecorateRequest({ - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }); - - expect(mockCreateClerkClient).toHaveBeenCalledWith( - expect.objectContaining({ - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }), - ); - }); - - it('does not call createClerkClient at construction when client construction overrides are not set', () => { - authenticateAndDecorateRequest({}); + it('does not call createClerkClient at construction when apiUrl/apiVersion are not set', () => { + authenticateAndDecorateRequest({ secretKey: 'sk_test_....' }); expect(mockCreateClerkClient).not.toHaveBeenCalled(); }); diff --git a/packages/express/src/authenticateRequest.ts b/packages/express/src/authenticateRequest.ts index 27d25bcac9f..606c80ddbd8 100644 --- a/packages/express/src/authenticateRequest.ts +++ b/packages/express/src/authenticateRequest.ts @@ -112,26 +112,21 @@ const absoluteProxyUrl = (relativeOrAbsoluteUrl: string, baseUrl: string): strin return new URL(relativeOrAbsoluteUrl, baseUrl).toString(); }; -// Some options are pinned at client construction time inside `@clerk/backend`'s -// `createAuthenticateRequest` factory, including the API client used for nonce -// handshake payload exchange. The default singleton in `./clerkClient` is built +// `apiUrl` and `apiVersion` are pinned at client construction time inside +// `@clerk/backend`'s `createAuthenticateRequest` factory (build-time values +// override runtime ones). The default singleton in `./clerkClient` is built // from env only, so passing these via `clerkMiddleware()` would be silently -// ignored. When the caller hasn't supplied their own `clerkClient` but did pass -// client construction options, build a per-middleware client with those values. +// ignored. When the caller hasn't supplied their own `clerkClient` but did +// pass `apiUrl`/`apiVersion`, build a per-middleware client with those values. const resolveDefaultClerkClient = (options: ClerkMiddlewareOptions) => { - const { apiUrl, apiVersion, secretKey, machineSecretKey, publishableKey, jwtKey } = options; - if (!apiUrl && !apiVersion && !secretKey && !machineSecretKey && !publishableKey && !jwtKey) { + if (!options.apiUrl && !options.apiVersion) { return defaultClerkClient; } const env = { ...loadApiEnv(), ...loadClientEnv() }; return createClerkClient({ ...env, - ...(apiUrl ? { apiUrl } : {}), - ...(apiVersion ? { apiVersion } : {}), - ...(secretKey ? { secretKey } : {}), - ...(machineSecretKey ? { machineSecretKey } : {}), - ...(publishableKey ? { publishableKey } : {}), - ...(jwtKey ? { jwtKey } : {}), + ...(options.apiUrl ? { apiUrl: options.apiUrl } : {}), + ...(options.apiVersion ? { apiVersion: options.apiVersion } : {}), userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, }); }; diff --git a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts index c5999205fd0..3e432cd59f0 100644 --- a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts +++ b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts @@ -4,22 +4,19 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { clerkPlugin, getAuth } from '../index'; -const { authenticateRequestMock, createClerkClientMock } = vi.hoisted(() => { +const { authenticateRequestMock, createClerkClientMock, mockClerkClient } = vi.hoisted(() => { const authenticateRequestMock = vi.fn(); - const createClerkClientMock = vi.fn(() => { - return { - authenticateRequest: (...args: any) => authenticateRequestMock(...args), - }; - }); + const mockClerkClient = { authenticateRequest: (...args: any) => authenticateRequestMock(...args) }; + const createClerkClientMock = vi.fn(() => mockClerkClient); - return { authenticateRequestMock, createClerkClientMock }; + return { authenticateRequestMock, createClerkClientMock, mockClerkClient }; }); vi.mock('@clerk/backend', async () => { const actual = await vi.importActual('@clerk/backend'); return { ...actual, - createClerkClient: (...args: any[]) => createClerkClientMock(...args), + createClerkClient: createClerkClientMock, }; }); @@ -210,6 +207,25 @@ describe('withClerkMiddleware(options)', () => { expect(response.body).toEqual(JSON.stringify({ auth: { tokenType: 'session_token' } })); }); + test('exposes the clerk client instance on request.clerk', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: () => ({ tokenType: 'session_token' }), + }); + const fastify = Fastify(); + await fastify.register(clerkPlugin); + + let clerkOnRequest: unknown; + fastify.get('/', (request: FastifyRequest, reply: FastifyReply) => { + clerkOnRequest = request.clerk; + reply.send({}); + }); + + await fastify.inject({ method: 'GET', path: '/' }); + + expect(clerkOnRequest).toBe(mockClerkClient); + }); + test('handles signout case by populating the req.auth', async () => { authenticateRequestMock.mockResolvedValueOnce({ headers: new Headers(), diff --git a/packages/fastify/src/clerkPlugin.ts b/packages/fastify/src/clerkPlugin.ts index 477894881a2..8b8d16cbdfb 100644 --- a/packages/fastify/src/clerkPlugin.ts +++ b/packages/fastify/src/clerkPlugin.ts @@ -11,6 +11,8 @@ const plugin: FastifyPluginCallback = ( done, ) => { instance.decorateRequest('auth', null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + instance.decorateRequest('clerk', null as any); // run clerk as a middleware to all scoped routes const hookName = opts.hookName || 'preHandler'; if (!ALLOWED_HOOKS.includes(hookName)) { diff --git a/packages/fastify/src/types.ts b/packages/fastify/src/types.ts index 98e50544613..c1ed7e8128c 100644 --- a/packages/fastify/src/types.ts +++ b/packages/fastify/src/types.ts @@ -1,6 +1,12 @@ -import type { ClerkOptions } from '@clerk/backend'; +import type { ClerkClient, ClerkOptions } from '@clerk/backend'; import type { ShouldProxyFn } from '@clerk/shared/proxy'; +declare module 'fastify' { + interface FastifyRequest { + clerk: ClerkClient; + } +} + export const ALLOWED_HOOKS = ['onRequest', 'preHandler'] as const; /** diff --git a/packages/fastify/src/withClerkMiddleware.ts b/packages/fastify/src/withClerkMiddleware.ts index 54640cc4c6e..e5b2f386341 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -106,5 +106,6 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => { // @ts-expect-error Inject auth so getAuth can read it fastifyRequest.auth = requestState.toAuth(); + fastifyRequest.clerk = clerkClient; }; }; diff --git a/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts b/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts index a7ddddde709..9e271597d77 100644 --- a/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts +++ b/packages/nuxt/src/runtime/server/__tests__/clerkClient.test.ts @@ -90,20 +90,4 @@ describe('clerkClient', () => { }), ); }); - - it('passes runtime options to createClerkClient', () => { - mockRuntimeConfig(); - - clerkClient({} as any, { - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }); - - expect(createClerkClientMock).toHaveBeenCalledWith( - expect.objectContaining({ - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }), - ); - }); }); diff --git a/packages/nuxt/src/runtime/server/clerkClient.ts b/packages/nuxt/src/runtime/server/clerkClient.ts index 5f31ecffce6..70a58fcea79 100644 --- a/packages/nuxt/src/runtime/server/clerkClient.ts +++ b/packages/nuxt/src/runtime/server/clerkClient.ts @@ -1,4 +1,3 @@ -import type { ClerkOptions } from '@clerk/backend'; import { createClerkClient } from '@clerk/backend'; import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; import { deprecated } from '@clerk/shared/deprecated'; @@ -29,7 +28,7 @@ function resolveApiVersion(runtimeConfig: ReturnType): return 'v1'; } -export function clerkClient(event: H3Event, options?: ClerkOptions) { +export function clerkClient(event: H3Event) { const runtimeConfig = useRuntimeConfig(event); return createClerkClient({ @@ -52,6 +51,5 @@ export function clerkClient(event: H3Event, options?: ClerkOptions) { version: PACKAGE_VERSION, environment: process.env.NODE_ENV, }, - ...options, }); } diff --git a/packages/nuxt/src/runtime/server/clerkMiddleware.ts b/packages/nuxt/src/runtime/server/clerkMiddleware.ts index d4a0cc2ecf0..aae8e02305f 100644 --- a/packages/nuxt/src/runtime/server/clerkMiddleware.ts +++ b/packages/nuxt/src/runtime/server/clerkMiddleware.ts @@ -115,7 +115,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => { } } - const requestState = await clerkClient(event, options).authenticateRequest(clerkRequest, { + const requestState = await clerkClient(event).authenticateRequest(clerkRequest, { ...options, acceptsToken: 'any', }); diff --git a/packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts b/packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts deleted file mode 100644 index e83b30ad773..00000000000 --- a/packages/tanstack-react-start/src/server/__tests__/clerkClient.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createClerkClient } from '@clerk/backend'; -import { describe, expect, test, vi } from 'vitest'; - -import { clerkClient } from '../clerkClient'; - -vi.mock('@clerk/backend', () => ({ - createClerkClient: vi.fn().mockReturnValue({}), -})); - -describe('clerkClient', () => { - test('passes runtime options to createClerkClient', () => { - clerkClient({ - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }); - - expect(vi.mocked(createClerkClient)).toHaveBeenCalledWith( - expect.objectContaining({ - secretKey: 'sk_test_runtime', - publishableKey: 'pk_test_runtime', - }), - ); - }); -}); diff --git a/packages/tanstack-react-start/src/server/clerkMiddleware.ts b/packages/tanstack-react-start/src/server/clerkMiddleware.ts index 468cd43894d..4b9b69b659c 100644 --- a/packages/tanstack-react-start/src/server/clerkMiddleware.ts +++ b/packages/tanstack-react-start/src/server/clerkMiddleware.ts @@ -43,7 +43,7 @@ export const clerkMiddleware = ( loadedOptions.secretKey = secretKey; } - const requestState = await clerkClient(loadedOptions).authenticateRequest(clerkRequest, { + const requestState = await clerkClient().authenticateRequest(clerkRequest, { ...loadedOptions, acceptsToken: 'any', }); From 189a8973696ca7688fea83a4356d546bf944fa89 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 15 May 2026 17:00:04 -0700 Subject: [PATCH 08/12] Delete .changeset/sixty-lobsters-jump.md --- .changeset/sixty-lobsters-jump.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/sixty-lobsters-jump.md diff --git a/.changeset/sixty-lobsters-jump.md b/.changeset/sixty-lobsters-jump.md deleted file mode 100644 index e031a69b0ca..00000000000 --- a/.changeset/sixty-lobsters-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@clerk/fastify": patch ---- - -Add enableHandshake option to plugin From 99b1219be9257c812097b1221fd310069c292255 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 15 May 2026 17:01:13 -0700 Subject: [PATCH 09/12] chore: update changeset --- .changeset/silent-fastify-handshakes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silent-fastify-handshakes.md b/.changeset/silent-fastify-handshakes.md index 1300ae290f6..1329f64322c 100644 --- a/.changeset/silent-fastify-handshakes.md +++ b/.changeset/silent-fastify-handshakes.md @@ -2,4 +2,4 @@ '@clerk/fastify': patch --- -Use runtime middleware keys when creating the auth client, so secrets passed directly to `clerkPlugin()` are used for handshake nonce exchange. Also exposes the per-request `ClerkClient` instance on `request.clerk`, and adds an `enableHandshake` option to opt out of handshake redirects when using Clerk as a first-party API backend. +Fixed an issue where secrets passed directly to clerkPlugin() were not used when verifying sessions, causing authentication failures when keys are loaded at runtime (e.g. AWS Secrets Manager on Lambda). From e5f9ebcc1ec5cd2ce5e2cc5cefe176a537a9d287 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 15 May 2026 17:02:09 -0700 Subject: [PATCH 10/12] chore: remove as any --- packages/fastify/src/clerkPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fastify/src/clerkPlugin.ts b/packages/fastify/src/clerkPlugin.ts index 8b8d16cbdfb..90c58a3a0da 100644 --- a/packages/fastify/src/clerkPlugin.ts +++ b/packages/fastify/src/clerkPlugin.ts @@ -11,8 +11,8 @@ const plugin: FastifyPluginCallback = ( done, ) => { instance.decorateRequest('auth', null); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - instance.decorateRequest('clerk', null as any); + instance.decorateRequest('clerk', null); + // run clerk as a middleware to all scoped routes const hookName = opts.hookName || 'preHandler'; if (!ALLOWED_HOOKS.includes(hookName)) { From a4ef868cadad2bd809ddbd8135e63a876335c072 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 15 May 2026 17:07:38 -0700 Subject: [PATCH 11/12] chore: revert --- packages/fastify/src/clerkPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/fastify/src/clerkPlugin.ts b/packages/fastify/src/clerkPlugin.ts index 90c58a3a0da..d55e2756f25 100644 --- a/packages/fastify/src/clerkPlugin.ts +++ b/packages/fastify/src/clerkPlugin.ts @@ -11,7 +11,8 @@ const plugin: FastifyPluginCallback = ( done, ) => { instance.decorateRequest('auth', null); - instance.decorateRequest('clerk', null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + instance.decorateRequest('clerk', null as any); // run clerk as a middleware to all scoped routes const hookName = opts.hookName || 'preHandler'; From 54ed74dd1e98c609e3eb8600d29a20a40501c759 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Sat, 16 May 2026 06:42:30 -0700 Subject: [PATCH 12/12] chore: stripe cookies when enableHandshake is false --- .../src/__tests__/withClerkMiddleware.test.ts | 29 ++++++++++++++++++ packages/fastify/src/withClerkMiddleware.ts | 30 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts index 3e432cd59f0..eaddf36fda8 100644 --- a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts +++ b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts @@ -207,6 +207,35 @@ describe('withClerkMiddleware(options)', () => { expect(response.body).toEqual(JSON.stringify({ auth: { tokenType: 'session_token' } })); }); + test('strips handshake cookies and query params before authenticating when enableHandshake is false', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: () => ({ tokenType: 'session_token' }), + }); + const fastify = Fastify(); + await fastify.register(clerkPlugin, { enableHandshake: false }); + + fastify.get('/', (_request: FastifyRequest, reply: FastifyReply) => { + reply.send({}); + }); + + await fastify.inject({ + method: 'GET', + path: '/?__clerk_handshake=token123&__clerk_handshake_nonce=nonce456&foo=bar', + headers: { + cookie: '__clerk_handshake=token123; __clerk_handshake_nonce=nonce456; __client_uat=1675692233', + }, + }); + + const [req] = authenticateRequestMock.mock.calls[0]; + expect(new URL(req.url).searchParams.has('__clerk_handshake')).toBe(false); + expect(new URL(req.url).searchParams.has('__clerk_handshake_nonce')).toBe(false); + expect(new URL(req.url).searchParams.get('foo')).toBe('bar'); + expect(req.headers.get('cookie')).not.toContain('__clerk_handshake='); + expect(req.headers.get('cookie')).not.toContain('__clerk_handshake_nonce='); + expect(req.headers.get('cookie')).toContain('__client_uat=1675692233'); + }); + test('exposes the clerk client instance on request.clerk', async () => { authenticateRequestMock.mockResolvedValueOnce({ headers: new Headers(), diff --git a/packages/fastify/src/withClerkMiddleware.ts b/packages/fastify/src/withClerkMiddleware.ts index e5b2f386341..c380278cc96 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -8,6 +8,30 @@ import * as constants from './constants'; import type { ClerkFastifyOptions } from './types'; import { fastifyRequestToRequest, requestToProxyRequest } from './utils'; +function stripHandshakeCookiesAndParams(req: Request, cookieNames: string[]): Request { + const url = new URL(req.url); + for (const name of cookieNames) { + url.searchParams.delete(name); + } + + const headers = new Headers(req.headers); + const cookieHeader = headers.get('cookie'); + if (cookieHeader) { + const filtered = cookieHeader + .split(';') + .map(c => c.trim()) + .filter(c => !cookieNames.some(name => c === name || c.startsWith(`${name}=`))) + .join('; '); + if (filtered) { + headers.set('cookie', filtered); + } else { + headers.delete('cookie'); + } + } + + return new Request(url.toString(), { method: req.method, headers }); +} + export const withClerkMiddleware = (options: ClerkFastifyOptions) => { const frontendApiProxy = options.frontendApiProxy; const proxyPath = stripTrailingSlashes(frontendApiProxy?.path ?? DEFAULT_PROXY_PATH) || DEFAULT_PROXY_PATH; @@ -83,7 +107,11 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => { } } - const req = fastifyRequestToRequest(fastifyRequest); + let req = fastifyRequestToRequest(fastifyRequest); + + if (!enableHandshake) { + req = stripHandshakeCookiesAndParams(req, [constants.Cookies.Handshake, constants.Cookies.HandshakeNonce]); + } const requestState = await clerkClient.authenticateRequest(req, { ...options,