Skip to content

Commit

Permalink
feat(nextjs): Allows access to request object to dynamically define…
Browse files Browse the repository at this point in the history
… `clerkMiddleware` options (#4160)
  • Loading branch information
LauraBeatris authored Sep 17, 2024
1 parent a5a3f0d commit 1a4d410
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-months-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/nextjs": minor
---

Allows access to request object to dynamically define `clerkMiddleware` options
27 changes: 27 additions & 0 deletions packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,33 @@ describe('clerkMiddleware(params)', () => {
expect(decryptedData).toEqual(options);
});

it('allows access to request object to dynamically define options', async () => {
const options = {
secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx',
publishableKey: 'pk_test_xxxxxxxxxxxxx',
signInUrl: '/foo',
signUpUrl: '/bar',
};
const resp = await clerkMiddleware(
() => {
return NextResponse.next();
},
req => ({
...options,
domain: req.nextUrl.host,
}),
)(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent);
expect(resp?.status).toEqual(200);

const requestData = resp?.headers.get('x-middleware-request-x-clerk-request-data');
assert.ok(requestData);

const decryptedData = decryptClerkRequestData(requestData);

expect(resp?.headers.get('x-middleware-request-x-clerk-request-data')).toBeDefined();
expect(decryptedData).toEqual({ ...options, domain: 'www.clerk.com' });
});

describe('auth().redirectToSignIn()', () => {
it('redirects to sign-in url when redirectToSignIn is called and the request is a page request', async () => {
const req = mockRequest({
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/src/server/clerkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { constants } from '@clerk/backend/internal';
import { deprecated } from '@clerk/shared/deprecated';

import { buildRequestLike, isPrerenderingBailout } from '../app-router/server/utils';
import { clerkMiddlewareRequestDataStore } from './clerkMiddleware';
import { clerkMiddlewareRequestDataStorage } from './clerkMiddleware';
import {
API_URL,
API_VERSION,
Expand Down Expand Up @@ -62,7 +62,7 @@ const clerkClientForRequest = () => {
}

// Fallbacks between options from middleware runtime and `NextRequest` from application server
const options = clerkMiddlewareRequestDataStore.getStore() ?? requestData;
const options = clerkMiddlewareRequestDataStorage.getStore()?.get('requestData') ?? requestData;
if (options?.secretKey || options?.publishableKey) {
return createClerkClientWithOptions(options);
}
Expand Down
77 changes: 47 additions & 30 deletions packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { AuthObject, ClerkClient } from '@clerk/backend';
import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal';
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
import { eventMethodCalled } from '@clerk/shared/telemetry';
import type { NextMiddleware } from 'next/server';
import type { NextMiddleware, NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

import { isRedirect, serverRedirectWithAuth, setHeader } from '../utils';
Expand Down Expand Up @@ -44,6 +44,8 @@ type ClerkMiddlewareHandler = (

export type ClerkMiddlewareOptions = AuthenticateRequestOptions & { debug?: boolean };

type ClerkMiddlewareOptionsCallback = (req: NextRequest) => ClerkMiddlewareOptions;

/**
* Middleware for Next.js that handles authentication and authorization with Clerk.
* For more details, please refer to the docs: https://clerk.com/docs/references/nextjs/clerk-middleware
Expand All @@ -54,6 +56,11 @@ interface ClerkMiddleware {
* export default clerkMiddleware((auth, request, event) => { ... }, options);
*/
(handler: ClerkMiddlewareHandler, options?: ClerkMiddlewareOptions): NextMiddleware;
/**
* @example
* export default clerkMiddleware((auth, request, event) => { ... }, (req) => options);
*/
(handler: ClerkMiddlewareHandler, options?: ClerkMiddlewareOptionsCallback): NextMiddleware;
/**
* @example
* export default clerkMiddleware(options);
Expand All @@ -66,38 +73,47 @@ interface ClerkMiddleware {
(request: NextMiddlewareRequestParam, event: NextMiddlewareEvtParam): NextMiddlewareReturn;
}

export const clerkMiddlewareRequestDataStore = new AsyncLocalStorage<Partial<AuthenticateRequestOptions>>();
const clerkMiddlewareRequestDataStore = new Map<'requestData', AuthenticateRequestOptions>();
export const clerkMiddlewareRequestDataStorage = new AsyncLocalStorage<typeof clerkMiddlewareRequestDataStore>();

export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
const [request, event] = parseRequestAndEvent(args);
const [handler, params] = parseHandlerAndOptions(args);

const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () =>
errorThrower.throwMissingPublishableKeyError(),
);
const secretKey = assertKey(params.secretKey || SECRET_KEY, () => errorThrower.throwMissingSecretKeyError());
const signInUrl = params.signInUrl || SIGN_IN_URL;
const signUpUrl = params.signUpUrl || SIGN_UP_URL;

const options = {
...params,
publishableKey,
secretKey,
signInUrl,
signUpUrl,
};
return clerkMiddlewareRequestDataStorage.run(clerkMiddlewareRequestDataStore, () => {
const nextMiddleware: NextMiddleware = withLogger('clerkMiddleware', logger => async (request, event) => {
// Handles the case where `options` is a callback function to dynamically access `NextRequest`
const resolvedParams = typeof params === 'function' ? params(request) : params;

return clerkMiddlewareRequestDataStore.run(options, () => {
clerkClient().telemetry.record(
eventMethodCalled('clerkMiddleware', {
handler: Boolean(handler),
satellite: Boolean(options.isSatellite),
proxy: Boolean(options.proxyUrl),
}),
);
const publishableKey = assertKey(resolvedParams.publishableKey || PUBLISHABLE_KEY, () =>
errorThrower.throwMissingPublishableKeyError(),
);
const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY, () =>
errorThrower.throwMissingSecretKeyError(),
);
const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL;
const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL;

const options = {
publishableKey,
secretKey,
signInUrl,
signUpUrl,
...resolvedParams,
};

// Propagates the request data to be accessed on the server application runtime from helpers such as `clerkClient`
clerkMiddlewareRequestDataStore.set('requestData', options);

clerkClient().telemetry.record(
eventMethodCalled('clerkMiddleware', {
handler: Boolean(handler),
satellite: Boolean(options.isSatellite),
proxy: Boolean(options.proxyUrl),
}),
);

const nextMiddleware: NextMiddleware = withLogger('clerkMiddleware', logger => async (request, event) => {
if (params.debug) {
if (options.debug) {
logger.enable();
}
const clerkRequest = createClerkRequest(request);
Expand Down Expand Up @@ -131,8 +147,9 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {

let handlerResult: Response = NextResponse.next();
try {
const userHandlerResult = await clerkMiddlewareRequestDataStore.run(options, async () =>
handler?.(() => authObjWithMethods, request, event),
const userHandlerResult = await clerkMiddlewareRequestDataStorage.run(
clerkMiddlewareRequestDataStore,
async () => handler?.(() => authObjWithMethods, request, event),
);
handlerResult = userHandlerResult || handlerResult;
} catch (e: any) {
Expand All @@ -156,7 +173,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' });
}

decorateRequest(clerkRequest, handlerResult, requestState, params);
decorateRequest(clerkRequest, handlerResult, requestState, resolvedParams);

return handlerResult;
});
Expand Down Expand Up @@ -184,7 +201,7 @@ const parseHandlerAndOptions = (args: unknown[]) => {
return [
typeof args[0] === 'function' ? args[0] : undefined,
(args.length === 2 ? args[1] : typeof args[0] === 'function' ? {} : args[0]) || {},
] as [ClerkMiddlewareHandler | undefined, ClerkMiddlewareOptions];
] as [ClerkMiddlewareHandler | undefined, ClerkMiddlewareOptions | ClerkMiddlewareOptionsCallback];
};

type AuthenticateRequest = Pick<ClerkClient, 'authenticateRequest'>['authenticateRequest'];
Expand Down

0 comments on commit 1a4d410

Please sign in to comment.