From 5e23bc8b7687bba0b047ab56f7f37c491a862cf4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Sat, 23 May 2026 20:33:54 -0700 Subject: [PATCH 1/2] fix(fastify): use runtime keys for auth client Co-authored-by: Jeff Escalante --- .changeset/silent-fastify-handshakes.md | 5 ++ .../fastify/src/__tests__/clerkPlugin.test.ts | 3 +- .../src/__tests__/withClerkMiddleware.test.ts | 82 +++++++++++++++++-- packages/fastify/src/clerkPlugin.ts | 2 + packages/fastify/src/types.ts | 8 +- packages/fastify/src/withClerkMiddleware.ts | 19 ++++- 6 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 .changeset/silent-fastify-handshakes.md diff --git a/.changeset/silent-fastify-handshakes.md b/.changeset/silent-fastify-handshakes.md new file mode 100644 index 00000000000..44c7ba87e64 --- /dev/null +++ b/.changeset/silent-fastify-handshakes.md @@ -0,0 +1,5 @@ +--- +'@clerk/fastify': patch +--- + +Fixed an issue where secrets passed directly to clerkPlugin() were not used when verifying sessions, causing authentication failures when keys are loaded at runtime. The runtime-key Clerk client is also now available on `request.clerk`. diff --git a/packages/fastify/src/__tests__/clerkPlugin.test.ts b/packages/fastify/src/__tests__/clerkPlugin.test.ts index 8dbe1939c53..6adc90af05f 100644 --- a/packages/fastify/src/__tests__/clerkPlugin.test.ts +++ b/packages/fastify/src/__tests__/clerkPlugin.test.ts @@ -49,13 +49,14 @@ describe('clerkPlugin()', () => { }, ); - test('adds auth decorator', () => { + test('adds request decorators', () => { const doneFn = vi.fn(); const fastify = createFastifyInstanceMock(); clerkPlugin(fastify, {}, doneFn); expect(fastify.decorateRequest).toHaveBeenCalledWith('auth', null); + expect(fastify.decorateRequest).toHaveBeenCalledWith('clerk', null); expect(doneFn).toHaveBeenCalled(); }); }); diff --git a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts index d08316f99ef..b8050f06ddf 100644 --- a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts +++ b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts @@ -4,17 +4,21 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { clerkPlugin, getAuth } from '../index'; -const authenticateRequestMock = vi.fn(); +const { authenticateRequestMock, createClerkClientMock, mockClerkClient } = vi.hoisted(() => { + const authenticateRequestMock = vi.fn(); + const mockClerkClient = { + authenticateRequest: (...args: any) => authenticateRequestMock(...args), + }; + const createClerkClientMock = vi.fn(() => mockClerkClient); + + return { authenticateRequestMock, createClerkClientMock, mockClerkClient }; +}); 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 +28,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(), @@ -142,6 +178,40 @@ describe('withClerkMiddleware(options)', () => { }); }); + test('exposes the runtime key clerk client instance on request.clerk', 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', + }); + + let clerkOnRequest: unknown; + fastify.get('/', (request: FastifyRequest, reply: FastifyReply) => { + clerkOnRequest = request.clerk; + reply.send({}); + }); + + const response = await fastify.inject({ + method: 'GET', + path: '/', + }); + + expect(response.statusCode).toEqual(200); + expect(clerkOnRequest).toBe(mockClerkClient); + expect(createClerkClientMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + secretKey: 'runtime_secret_key', + publishableKey: 'runtime_publishable_key', + }), + ); + }); + 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..90c58a3a0da 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); + instance.decorateRequest('clerk', null); + // 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 7b1224ea271..7335800f085 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 bca237ce8d4..9af00fb3a39 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) { @@ -93,5 +103,6 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => { // @ts-expect-error Inject auth so getAuth can read it fastifyRequest.auth = requestState.toAuth(); + fastifyRequest.clerk = clerkClient; }; }; From d807c2a519adf3f2ed974934bf6510aa4c6e8dee Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Sun, 24 May 2026 16:51:07 -0700 Subject: [PATCH 2/2] fix types --- packages/fastify/src/clerkPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fastify/src/clerkPlugin.ts b/packages/fastify/src/clerkPlugin.ts index 90c58a3a0da..d6e95f6bb20 100644 --- a/packages/fastify/src/clerkPlugin.ts +++ b/packages/fastify/src/clerkPlugin.ts @@ -11,7 +11,7 @@ const plugin: FastifyPluginCallback = ( done, ) => { instance.decorateRequest('auth', null); - instance.decorateRequest('clerk', null); + instance.decorateRequest('clerk', null as any); // run clerk as a middleware to all scoped routes const hookName = opts.hookName || 'preHandler';