From 3cda8c0b7654860c0f0b36c79255942f6585a562 Mon Sep 17 00:00:00 2001 From: Dimitris Klouvas Date: Fri, 9 Feb 2024 18:10:27 +0200 Subject: [PATCH] feat(fastify): Introduce handshake --- .changeset/dull-seals-type.md | 5 ++ .../fastify/src/withClerkMiddleware.test.ts | 69 ++++++++++--------- packages/fastify/src/withClerkMiddleware.ts | 32 +++++---- 3 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 .changeset/dull-seals-type.md diff --git a/.changeset/dull-seals-type.md b/.changeset/dull-seals-type.md new file mode 100644 index 00000000000..e2ea46c5942 --- /dev/null +++ b/.changeset/dull-seals-type.md @@ -0,0 +1,5 @@ +--- +'@clerk/fastify': minor +--- + +Introduce handshake mechanism and `x-clerk-auth-status` in response diff --git a/packages/fastify/src/withClerkMiddleware.test.ts b/packages/fastify/src/withClerkMiddleware.test.ts index 5ab4654ef8a..eec86cacd6f 100644 --- a/packages/fastify/src/withClerkMiddleware.test.ts +++ b/packages/fastify/src/withClerkMiddleware.test.ts @@ -23,7 +23,8 @@ describe('withClerkMiddleware(options)', () => { }); test('handles signin with Authorization Bearer', async () => { - authenticateRequestMock.mockResolvedValue({ + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), toAuth: () => 'mockedAuth', }); const fastify = Fastify(); @@ -59,7 +60,8 @@ describe('withClerkMiddleware(options)', () => { }); test('handles signin with cookie', async () => { - authenticateRequestMock.mockResolvedValue({ + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), toAuth: () => 'mockedAuth', }); const fastify = Fastify(); @@ -94,37 +96,42 @@ describe('withClerkMiddleware(options)', () => { ); }); - // @TODO handshake - // test('handles handshake case by redirecting the request to fapi', async () => { - // authenticateRequestMock.mockResolvedValue({ - // reason: 'auth-reason', - // message: 'auth-message', - // toAuth: () => 'mockedAuth', - // }); - // const fastify = Fastify(); - // await fastify.register(clerkPlugin); - // - // fastify.get('/', (request: FastifyRequest, reply: FastifyReply) => { - // const auth = getAuth(request); - // reply.send({ auth }); - // }); - // - // const response = await fastify.inject({ - // method: 'GET', - // path: '/', - // headers: { - // cookie: '_gcl_au=value1; ko_id=value2; __session=deadbeef; __client_uat=1675692233', - // }, - // }); - // - // expect(response.statusCode).toEqual(401); - // expect(response.headers['content-type']).toEqual('text/html'); - // expect(response.headers['x-clerk-auth-reason']).toEqual('auth-reason'); - // expect(response.headers['x-clerk-auth-message']).toEqual('auth-message'); - // }); + test('handles handshake case by redirecting the request to fapi', async () => { + authenticateRequestMock.mockResolvedValueOnce({ + status: 'handshake', + reason: 'auth-reason', + message: 'auth-message', + headers: new Headers({ location: 'https://fapi.example.com/v1/clients/handshake' }), + toAuth: () => 'mockedAuth', + }); + const fastify = Fastify(); + await fastify.register(clerkPlugin); + + fastify.get('/', (request: FastifyRequest, reply: FastifyReply) => { + const auth = getAuth(request); + reply.send({ auth }); + }); + + const response = await fastify.inject({ + method: 'GET', + path: '/', + headers: { + cookie: '_gcl_au=value1; ko_id=value2; __session=deadbeef; __client_uat=1675692233', + }, + }); + + expect(response.statusCode).toEqual(307); + expect(response.headers).toMatchObject({ + location: 'https://fapi.example.com/v1/clients/handshake', + 'x-clerk-auth-status': 'handshake', + 'x-clerk-auth-reason': 'auth-reason', + 'x-clerk-auth-message': 'auth-message', + }); + }); test('handles signout case by populating the req.auth', async () => { - authenticateRequestMock.mockResolvedValue({ + authenticateRequestMock.mockResolvedValueOnce({ + headers: new Headers(), toAuth: () => 'mockedAuth', }); const fastify = Fastify(); diff --git a/packages/fastify/src/withClerkMiddleware.ts b/packages/fastify/src/withClerkMiddleware.ts index 0a2c94a2ea8..8325796715a 100644 --- a/packages/fastify/src/withClerkMiddleware.ts +++ b/packages/fastify/src/withClerkMiddleware.ts @@ -1,31 +1,35 @@ +import type { RequestState } from '@clerk/backend/internal'; import { AuthStatus } from '@clerk/backend/internal'; -import type { FastifyRequest } from 'fastify'; +import type { FastifyReply, FastifyRequest } from 'fastify'; import { clerkClient } from './clerkClient'; import * as constants from './constants'; import type { ClerkFastifyOptions } from './types'; import { fastifyRequestToRequest } from './utils'; +const decorateResponseWithObservabilityHeaders = (reply: FastifyReply, requestState: RequestState): FastifyReply => { + return reply + .header(constants.Headers.AuthStatus, requestState.status) + .header(constants.Headers.AuthReason, requestState.reason) + .header(constants.Headers.AuthMessage, requestState.message); +}; + export const withClerkMiddleware = (options: ClerkFastifyOptions) => { - return async (fastifyRequest: FastifyRequest) => { - const secretKey = options.secretKey || constants.SECRET_KEY; - const publishableKey = options.publishableKey || constants.PUBLISHABLE_KEY; + return async (fastifyRequest: FastifyRequest, reply: FastifyReply) => { const req = fastifyRequestToRequest(fastifyRequest); const requestState = await clerkClient.authenticateRequest(req, { ...options, - secretKey, - publishableKey, + secretKey: options.secretKey || constants.SECRET_KEY, + publishableKey: options.publishableKey || constants.PUBLISHABLE_KEY, }); + requestState.headers.forEach((value, key) => reply.header(key, value)); - if (requestState.status === AuthStatus.Handshake) { - // @TODO handshake - // return reply - // .code(401) - // .header(constants.Headers.AuthReason, requestState.reason) - // .header(constants.Headers.AuthMessage, requestState.message) - // .type('text/html') - // .send(...); + const locationHeader = requestState.headers.get(constants.Headers.Location); + if (locationHeader) { + return decorateResponseWithObservabilityHeaders(reply, requestState).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