Skip to content

Commit 1a4d410

Browse files
authored
feat(nextjs): Allows access to request object to dynamically define clerkMiddleware options (clerk#4160)
1 parent a5a3f0d commit 1a4d410

4 files changed

Lines changed: 81 additions & 32 deletions

File tree

.changeset/ten-months-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/nextjs": minor
3+
---
4+
5+
Allows access to request object to dynamically define `clerkMiddleware` options

packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,33 @@ describe('clerkMiddleware(params)', () => {
226226
expect(decryptedData).toEqual(options);
227227
});
228228

229+
it('allows access to request object to dynamically define options', async () => {
230+
const options = {
231+
secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx',
232+
publishableKey: 'pk_test_xxxxxxxxxxxxx',
233+
signInUrl: '/foo',
234+
signUpUrl: '/bar',
235+
};
236+
const resp = await clerkMiddleware(
237+
() => {
238+
return NextResponse.next();
239+
},
240+
req => ({
241+
...options,
242+
domain: req.nextUrl.host,
243+
}),
244+
)(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent);
245+
expect(resp?.status).toEqual(200);
246+
247+
const requestData = resp?.headers.get('x-middleware-request-x-clerk-request-data');
248+
assert.ok(requestData);
249+
250+
const decryptedData = decryptClerkRequestData(requestData);
251+
252+
expect(resp?.headers.get('x-middleware-request-x-clerk-request-data')).toBeDefined();
253+
expect(decryptedData).toEqual({ ...options, domain: 'www.clerk.com' });
254+
});
255+
229256
describe('auth().redirectToSignIn()', () => {
230257
it('redirects to sign-in url when redirectToSignIn is called and the request is a page request', async () => {
231258
const req = mockRequest({

packages/nextjs/src/server/clerkClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { constants } from '@clerk/backend/internal';
44
import { deprecated } from '@clerk/shared/deprecated';
55

66
import { buildRequestLike, isPrerenderingBailout } from '../app-router/server/utils';
7-
import { clerkMiddlewareRequestDataStore } from './clerkMiddleware';
7+
import { clerkMiddlewareRequestDataStorage } from './clerkMiddleware';
88
import {
99
API_URL,
1010
API_VERSION,
@@ -62,7 +62,7 @@ const clerkClientForRequest = () => {
6262
}
6363

6464
// Fallbacks between options from middleware runtime and `NextRequest` from application server
65-
const options = clerkMiddlewareRequestDataStore.getStore() ?? requestData;
65+
const options = clerkMiddlewareRequestDataStorage.getStore()?.get('requestData') ?? requestData;
6666
if (options?.secretKey || options?.publishableKey) {
6767
return createClerkClientWithOptions(options);
6868
}

packages/nextjs/src/server/clerkMiddleware.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { AuthObject, ClerkClient } from '@clerk/backend';
44
import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal';
55
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
66
import { eventMethodCalled } from '@clerk/shared/telemetry';
7-
import type { NextMiddleware } from 'next/server';
7+
import type { NextMiddleware, NextRequest } from 'next/server';
88
import { NextResponse } from 'next/server';
99

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

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

47+
type ClerkMiddlewareOptionsCallback = (req: NextRequest) => ClerkMiddlewareOptions;
48+
4749
/**
4850
* Middleware for Next.js that handles authentication and authorization with Clerk.
4951
* For more details, please refer to the docs: https://clerk.com/docs/references/nextjs/clerk-middleware
@@ -54,6 +56,11 @@ interface ClerkMiddleware {
5456
* export default clerkMiddleware((auth, request, event) => { ... }, options);
5557
*/
5658
(handler: ClerkMiddlewareHandler, options?: ClerkMiddlewareOptions): NextMiddleware;
59+
/**
60+
* @example
61+
* export default clerkMiddleware((auth, request, event) => { ... }, (req) => options);
62+
*/
63+
(handler: ClerkMiddlewareHandler, options?: ClerkMiddlewareOptionsCallback): NextMiddleware;
5764
/**
5865
* @example
5966
* export default clerkMiddleware(options);
@@ -66,38 +73,47 @@ interface ClerkMiddleware {
6673
(request: NextMiddlewareRequestParam, event: NextMiddlewareEvtParam): NextMiddlewareReturn;
6774
}
6875

69-
export const clerkMiddlewareRequestDataStore = new AsyncLocalStorage<Partial<AuthenticateRequestOptions>>();
76+
const clerkMiddlewareRequestDataStore = new Map<'requestData', AuthenticateRequestOptions>();
77+
export const clerkMiddlewareRequestDataStorage = new AsyncLocalStorage<typeof clerkMiddlewareRequestDataStore>();
7078

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

75-
const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () =>
76-
errorThrower.throwMissingPublishableKeyError(),
77-
);
78-
const secretKey = assertKey(params.secretKey || SECRET_KEY, () => errorThrower.throwMissingSecretKeyError());
79-
const signInUrl = params.signInUrl || SIGN_IN_URL;
80-
const signUpUrl = params.signUpUrl || SIGN_UP_URL;
81-
82-
const options = {
83-
...params,
84-
publishableKey,
85-
secretKey,
86-
signInUrl,
87-
signUpUrl,
88-
};
83+
return clerkMiddlewareRequestDataStorage.run(clerkMiddlewareRequestDataStore, () => {
84+
const nextMiddleware: NextMiddleware = withLogger('clerkMiddleware', logger => async (request, event) => {
85+
// Handles the case where `options` is a callback function to dynamically access `NextRequest`
86+
const resolvedParams = typeof params === 'function' ? params(request) : params;
8987

90-
return clerkMiddlewareRequestDataStore.run(options, () => {
91-
clerkClient().telemetry.record(
92-
eventMethodCalled('clerkMiddleware', {
93-
handler: Boolean(handler),
94-
satellite: Boolean(options.isSatellite),
95-
proxy: Boolean(options.proxyUrl),
96-
}),
97-
);
88+
const publishableKey = assertKey(resolvedParams.publishableKey || PUBLISHABLE_KEY, () =>
89+
errorThrower.throwMissingPublishableKeyError(),
90+
);
91+
const secretKey = assertKey(resolvedParams.secretKey || SECRET_KEY, () =>
92+
errorThrower.throwMissingSecretKeyError(),
93+
);
94+
const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL;
95+
const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL;
96+
97+
const options = {
98+
publishableKey,
99+
secretKey,
100+
signInUrl,
101+
signUpUrl,
102+
...resolvedParams,
103+
};
104+
105+
// Propagates the request data to be accessed on the server application runtime from helpers such as `clerkClient`
106+
clerkMiddlewareRequestDataStore.set('requestData', options);
107+
108+
clerkClient().telemetry.record(
109+
eventMethodCalled('clerkMiddleware', {
110+
handler: Boolean(handler),
111+
satellite: Boolean(options.isSatellite),
112+
proxy: Boolean(options.proxyUrl),
113+
}),
114+
);
98115

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

132148
let handlerResult: Response = NextResponse.next();
133149
try {
134-
const userHandlerResult = await clerkMiddlewareRequestDataStore.run(options, async () =>
135-
handler?.(() => authObjWithMethods, request, event),
150+
const userHandlerResult = await clerkMiddlewareRequestDataStorage.run(
151+
clerkMiddlewareRequestDataStore,
152+
async () => handler?.(() => authObjWithMethods, request, event),
136153
);
137154
handlerResult = userHandlerResult || handlerResult;
138155
} catch (e: any) {
@@ -156,7 +173,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
156173
setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' });
157174
}
158175

159-
decorateRequest(clerkRequest, handlerResult, requestState, params);
176+
decorateRequest(clerkRequest, handlerResult, requestState, resolvedParams);
160177

161178
return handlerResult;
162179
});
@@ -184,7 +201,7 @@ const parseHandlerAndOptions = (args: unknown[]) => {
184201
return [
185202
typeof args[0] === 'function' ? args[0] : undefined,
186203
(args.length === 2 ? args[1] : typeof args[0] === 'function' ? {} : args[0]) || {},
187-
] as [ClerkMiddlewareHandler | undefined, ClerkMiddlewareOptions];
204+
] as [ClerkMiddlewareHandler | undefined, ClerkMiddlewareOptions | ClerkMiddlewareOptionsCallback];
188205
};
189206

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

0 commit comments

Comments
 (0)