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 diff --git a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts index d08316f99ef..03b8c307101 100644 --- a/packages/fastify/src/__tests__/withClerkMiddleware.test.ts +++ b/packages/fastify/src/__tests__/withClerkMiddleware.test.ts @@ -142,6 +142,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(), 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 bca237ce8d4..1bd50bdb891 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -11,6 +11,7 @@ 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 enableHandshake = options.enableHandshake ?? true; return async (fastifyRequest: FastifyRequest, reply: FastifyReply) => { const publishableKey = options.publishableKey || constants.PUBLISHABLE_KEY; @@ -84,11 +85,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