diff --git a/.changeset/silent-fastify-handshakes.md b/.changeset/silent-fastify-handshakes.md new file mode 100644 index 00000000000..3e26bf8c033 --- /dev/null +++ b/.changeset/silent-fastify-handshakes.md @@ -0,0 +1,5 @@ +--- +'@clerk/fastify': patch +--- + +Fixed `clerkPlugin()` to honor `publishableKey` and `secretKey` passed in plugin options when authenticating Fastify requests. The plugin now also exposes `request.clerk`, which uses the same plugin keys and resolves the correct Clerk API host for non-production publishable keys. 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..46a80e25d49 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,69 @@ 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('creates the request client with an apiUrl derived from the runtime publishable key', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), + toAuth: () => ({ + tokenType: 'session_token', + }), + }); + const fastify = Fastify(); + await fastify.register(clerkPlugin, { + secretKey: 'runtime_secret_key', + publishableKey: 'pk_test_aW1tdW5lLWhhd2stNjUuY2xlcmsuYWNjb3VudHNzdGFnZS5kZXYk', + }); + + fastify.get('/', (_request: FastifyRequest, reply: FastifyReply) => { + reply.send({}); + }); + + const response = await fastify.inject({ + method: 'GET', + path: '/', + }); + + expect(response.statusCode).toEqual(200); + expect(createClerkClientMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + apiUrl: 'https://api.clerkstage.dev', + publishableKey: 'pk_test_aW1tdW5lLWhhd2stNjUuY2xlcmsuYWNjb3VudHNzdGFnZS5kZXYk', + }), + ); + }); + test('handles signin with Authorization Bearer', async () => { authenticateRequestMock.mockResolvedValueOnce({ headers: new Headers(), @@ -142,6 +209,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..d6e95f6bb20 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 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 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..877a51eec9a 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -1,9 +1,10 @@ +import { createClerkClient } from '@clerk/backend'; import { AuthStatus } from '@clerk/backend/internal'; import { clerkFrontendApiProxy, DEFAULT_PROXY_PATH, stripTrailingSlashes } from '@clerk/backend/proxy'; +import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; 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 +12,22 @@ 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 apiUrl = options.apiUrl || apiUrlFromPublishableKey(publishableKey); + const clerkClient = createClerkClient({ + ...options, + publishableKey, + secretKey, + machineSecretKey: options.machineSecretKey || constants.MACHINE_SECRET_KEY, + apiUrl, + 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 +105,6 @@ export const withClerkMiddleware = (options: ClerkFastifyOptions) => { // @ts-expect-error Inject auth so getAuth can read it fastifyRequest.auth = requestState.toAuth(); + fastifyRequest.clerk = clerkClient; }; };