From 7b200af4908839ea661ddf2a76811057b545cafc Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Fri, 2 Feb 2024 12:30:13 +0000 Subject: [PATCH] feat(nextjs): Improve auth().redirectToSignIn() and auth().protect() redirect mechanism * feat(nextjs): Make auth().redirectToSignIn() in middleware work without return * feat(nextjs): Make auth().protect() respect returnBackUrl by using redirectToSignIn internally --- .changeset/chilled-bikes-rule.md | 24 +++ .../templates/next-app-router/package.json | 2 +- packages/backend/src/constants.ts | 2 + packages/backend/src/createRedirect.ts | 4 +- .../src/__snapshots__/constants.test.ts.snap | 1 + packages/nextjs/src/app-router/server/auth.ts | 28 ++-- packages/nextjs/src/server/authMiddleware.ts | 2 +- .../nextjs/src/server/clerkMiddleware.test.ts | 141 ++++++++++++++++-- packages/nextjs/src/server/clerkMiddleware.ts | 87 ++++++----- packages/nextjs/src/server/protect.ts | 7 +- packages/nextjs/src/server/utils.ts | 3 +- packages/nextjs/src/utils/response.ts | 2 +- 12 files changed, 229 insertions(+), 74 deletions(-) create mode 100644 .changeset/chilled-bikes-rule.md diff --git a/.changeset/chilled-bikes-rule.md b/.changeset/chilled-bikes-rule.md new file mode 100644 index 00000000000..fe0e41e78d2 --- /dev/null +++ b/.changeset/chilled-bikes-rule.md @@ -0,0 +1,24 @@ +--- +'@clerk/backend': patch +'@clerk/nextjs': patch +--- + +The `auth().redirectToSignIn()` helper no longer needs to be explicitly returned when called within the middleware. The following examples are now equivalent: + +```js +// Before +export default clerkMiddleware(auth => { + if (protectedRoute && !auth.user) { + return auth().redirectToSignIn() + } +}) + +// After +export default clerkMiddleware(auth => { + if (protectedRoute && !auth.user) { + auth().redirectToSignIn() + } +}) +``` + +Calling `auth().protect()` from a page will now automatically redirect back to the same page by setting `redirect_url` to the request url before the redirect to the sign-in URL takes place. diff --git a/integration/templates/next-app-router/package.json b/integration/templates/next-app-router/package.json index 5e298c001bd..30f6398d692 100644 --- a/integration/templates/next-app-router/package.json +++ b/integration/templates/next-app-router/package.json @@ -12,7 +12,7 @@ "@types/node": "^18.17.0", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", - "next": "13.5.4", + "next": "13", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "5.1.6" diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index c5e840e3e79..1fd9578a93a 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -10,6 +10,7 @@ const Attributes = { AuthStatus: '__clerkAuthStatus', AuthReason: '__clerkAuthReason', AuthMessage: '__clerkAuthMessage', + ClerkUrl: '__clerkUrl', } as const; const Cookies = { @@ -29,6 +30,7 @@ const Headers = { AuthStatus: 'x-clerk-auth-status', AuthReason: 'x-clerk-auth-reason', AuthMessage: 'x-clerk-auth-message', + ClerkUrl: 'x-clerk-clerk-url', EnableDebug: 'x-clerk-debug', ClerkRedirectTo: 'x-clerk-redirect-to', CloudFrontForwardedProto: 'cloudfront-forwarded-proto', diff --git a/packages/backend/src/createRedirect.ts b/packages/backend/src/createRedirect.ts index e03a99f990d..5d06a2838cf 100644 --- a/packages/backend/src/createRedirect.ts +++ b/packages/backend/src/createRedirect.ts @@ -1,6 +1,6 @@ import { errorThrower, parsePublishableKey } from './util/shared'; -const buildUrl = (_baseUrl: string | URL, _targetUrl: string | URL, _returnBackUrl?: string | URL) => { +const buildUrl = (_baseUrl: string | URL, _targetUrl: string | URL, _returnBackUrl?: string | URL | null) => { if (_baseUrl === '') { return legacyBuildUrl(_targetUrl.toString(), _returnBackUrl?.toString()); } @@ -58,7 +58,7 @@ const buildAccountsBaseUrl = (frontendApi?: string) => { }; type RedirectAdapter = (url: string) => RedirectReturn; -type RedirectToParams = { returnBackUrl?: string | URL }; +type RedirectToParams = { returnBackUrl?: string | URL | null }; export type RedirectFun = (params?: RedirectToParams) => ReturnType; /** diff --git a/packages/fastify/src/__snapshots__/constants.test.ts.snap b/packages/fastify/src/__snapshots__/constants.test.ts.snap index 1d7d50eb444..8087494688c 100644 --- a/packages/fastify/src/__snapshots__/constants.test.ts.snap +++ b/packages/fastify/src/__snapshots__/constants.test.ts.snap @@ -18,6 +18,7 @@ exports[`constants from environment variables 1`] = ` "AuthToken": "x-clerk-auth-token", "Authorization": "authorization", "ClerkRedirectTo": "x-clerk-redirect-to", + "ClerkUrl": "x-clerk-clerk-url", "CloudFrontForwardedProto": "cloudfront-forwarded-proto", "ContentType": "content-type", "EnableDebug": "x-clerk-debug", diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 2a3411ade65..3b8221fbbf2 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -8,6 +8,7 @@ import { createGetAuth } from '../../server/createGetAuth'; import { authAuthHeaderMissing } from '../../server/errors'; import type { AuthProtect } from '../../server/protect'; import { createProtect } from '../../server/protect'; +import { getAuthKeyFromRequest } from '../../server/utils'; import { buildRequestLike } from './utils'; type Auth = AuthObject & { protect: AuthProtect; redirectToSignIn: RedirectFun> }; @@ -19,16 +20,23 @@ export const auth = (): Auth => { noAuthStatusMessage: authAuthHeaderMissing(), })(request); - const protect = createProtect({ request, authObject, notFound, redirect }); - const redirectToSignIn = createRedirect({ - redirectAdapter: redirect, - baseUrl: createClerkRequest(request).clerkUrl.toString(), - // TODO: Support runtime-value configuration of these options - // via setting and reading headers from clerkMiddleware - publishableKey: PUBLISHABLE_KEY, - signInUrl: SIGN_IN_URL, - signUpUrl: SIGN_UP_URL, - }).redirectToSignIn; + const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl'); + + const redirectToSignIn: RedirectFun = (opts = {}) => { + return createRedirect({ + redirectAdapter: redirect, + baseUrl: createClerkRequest(request).clerkUrl.toString(), + // TODO: Support runtime-value configuration of these options + // via setting and reading headers from clerkMiddleware + publishableKey: PUBLISHABLE_KEY, + signInUrl: SIGN_IN_URL, + signUpUrl: SIGN_UP_URL, + }).redirectToSignIn({ + returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl?.toString(), + }); + }; + + const protect = createProtect({ request, authObject, redirectToSignIn, notFound, redirect }); return Object.assign(authObject, { protect, redirectToSignIn }); }; diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index dd26fbd9ac4..d286bb5c4b3 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -215,7 +215,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => { logger.debug(`Added ${constants.Headers.EnableDebug} on request`); } - const result = decorateRequest(nextRequest, finalRes, requestState) || NextResponse.next(); + const result = decorateRequest(clerkRequest, finalRes, requestState) || NextResponse.next(); if (requestState.headers) { requestState.headers.forEach((value, key) => { diff --git a/packages/nextjs/src/server/clerkMiddleware.test.ts b/packages/nextjs/src/server/clerkMiddleware.test.ts index 4c947b022c8..c683c0f3407 100644 --- a/packages/nextjs/src/server/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/clerkMiddleware.test.ts @@ -1,4 +1,4 @@ -// There is no need to execute the complete authenticateRequest to test authMiddleware +// There is no need to execute the complete authenticateRequest to test clerkMiddleware // This mock SHOULD exist before the import of authenticateRequest import { AuthStatus, constants } from '@clerk/backend/internal'; import { describe, expect } from '@jest/globals'; @@ -36,7 +36,7 @@ afterAll(() => { global.console.warn = consoleWarn; }); -// Removing this mock will cause the authMiddleware tests to fail due to missing publishable key +// Removing this mock will cause the clerkMiddleware tests to fail due to missing publishable key // This mock SHOULD exist before the imports jest.mock('./constants', () => { return { @@ -179,7 +179,7 @@ describe('authenticateRequest & handshake', () => { }); }); -describe('authMiddleware(params)', () => { +describe('clerkMiddleware(params)', () => { it('renders route as normally when used without params', async () => { const signInResp = await clerkMiddleware()(mockRequest({ url: '/sign-in' }), {} as NextFetchEvent); expect(signInResp?.status).toEqual(200); @@ -215,48 +215,75 @@ describe('authMiddleware(params)', () => { }); describe('auth().redirectToSignIn()', () => { - it('redirects to sign-in url when redirectToSignIn is calle and the request is a page request', async () => { + it('redirects to sign-in url when redirectToSignIn is called and the request is a page request', async () => { const req = mockRequest({ url: '/protected', headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), appendDevBrowserCookie: true, }); - authenticateRequestMock.mockResolvedValueOnce({ - status: AuthStatus.SignedOut, - headers: new Headers(), - toAuth: () => ({ userId: null }), + const resp = await clerkMiddleware(auth => { + auth().redirectToSignIn(); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toContain('sign-in'); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(clerkClient.authenticateRequest).toBeCalled(); + }); + + it('redirects to sign-in url when redirectToSignIn is called with the correct returnBackUrl', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), + appendDevBrowserCookie: true, }); const resp = await clerkMiddleware(auth => { - return auth().redirectToSignIn(); + auth().redirectToSignIn(); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); + expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toContain('/protected'); expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); expect(clerkClient.authenticateRequest).toBeCalled(); }); - it('redirects to sign-in url when redirectToSignIn is calle and the request is not a page request', async () => { + it('redirects to sign-in url with redirect_url set to the provided returnBackUrl param', async () => { const req = mockRequest({ url: '/protected', - headers: new Headers(), + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), appendDevBrowserCookie: true, }); - authenticateRequestMock.mockResolvedValueOnce({ - status: AuthStatus.SignedOut, - headers: new Headers(), - toAuth: () => ({ userId: null }), + const resp = await clerkMiddleware(auth => { + auth().redirectToSignIn({ returnBackUrl: 'https://www.clerk.com/hello' }); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toContain('sign-in'); + expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toEqual( + 'https://www.clerk.com/hello', + ); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(clerkClient.authenticateRequest).toBeCalled(); + }); + + it('redirects to sign-in url without a redirect_url when returnBackUrl is null', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), + appendDevBrowserCookie: true, }); const resp = await clerkMiddleware(auth => { - return auth().redirectToSignIn(); + auth().redirectToSignIn({ returnBackUrl: null }); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); expect(resp?.headers.get('location')).toContain('sign-in'); + expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull(); expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); expect(clerkClient.authenticateRequest).toBeCalled(); }); @@ -406,6 +433,88 @@ describe('authMiddleware(params)', () => { expect(clerkClient.authenticateRequest).toBeCalled(); }); }); + + describe('auth().redirectToSignIn()', () => { + it('redirects to sign-in url even if called without a return statement', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), + appendDevBrowserCookie: true, + }); + + authenticateRequestMock.mockResolvedValueOnce({ + status: AuthStatus.SignedOut, + headers: new Headers(), + toAuth: () => ({ userId: null }), + }); + + const resp = await clerkMiddleware(auth => { + auth().protect(); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toContain('sign-in'); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(clerkClient.authenticateRequest).toBeCalled(); + }); + + it('redirects to unauthenticatedUrl when protect is called with the unauthenticatedUrl param, the user is signed out, and is a page request', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), + appendDevBrowserCookie: true, + }); + + authenticateRequestMock.mockResolvedValueOnce({ + status: AuthStatus.SignedOut, + headers: new Headers(), + toAuth: () => ({ userId: null }), + }); + + const resp = await clerkMiddleware(auth => { + auth().protect({ + unauthenticatedUrl: 'https://www.clerk.com/unauthenticatedUrl', + unauthorizedUrl: 'https://www.clerk.com/unauthorizedUrl', + }); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthenticatedUrl'); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); + expect(clerkClient.authenticateRequest).toBeCalled(); + }); + + it('redirects to unauthorizedUrl when protect is called with the unauthorizedUrl param, the user is signed in but does not have permissions, and is a page request', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), + appendDevBrowserCookie: true, + }); + + authenticateRequestMock.mockResolvedValueOnce({ + status: AuthStatus.SignedOut, + headers: new Headers(), + toAuth: () => ({ userId: 'userId', has: () => false }), + }); + + const resp = await clerkMiddleware(auth => { + auth().protect( + { permission: 'random-permission' }, + { + unauthenticatedUrl: 'https://www.clerk.com/unauthenticatedUrl', + unauthorizedUrl: 'https://www.clerk.com/unauthorizedUrl', + }, + ); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toContain('https://www.clerk.com/unauthorizedUrl'); + expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); + expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); + expect(clerkClient.authenticateRequest).toBeCalled(); + }); + }); }); describe('Dev Browser JWT when redirecting to cross origin for page requests', function () { diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index f6a8260f2bc..3ee2fba06a0 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -22,9 +22,11 @@ import { setRequestHeadersOnNextResponse, } from './utils'; -const PROTECT_REWRITE = 'CLERK_PROTECT_REWRITE'; -const PROTECT_REDIRECT_TO_URL = 'CLERK_PROTECT_REDIRECT_TO_URL'; -const PROTECT_REDIRECT_TO_SIGN_IN = 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN'; +const CONTROL_FLOW_ERROR = { + FORCE_NOT_FOUND: 'CLERK_PROTECT_REWRITE', + REDIRECT_TO_URL: 'CLERK_PROTECT_REDIRECT_TO_URL', + REDIRECT_TO_SIGN_IN: 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN', +}; export type ClerkMiddlewareAuthObject = AuthObject & { protect: AuthProtect; @@ -85,30 +87,15 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { const authObject = requestState.toAuth(); - const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { - protect: createMiddlewareProtect(clerkRequest, authObject), - redirectToSignIn: createMiddlewareRedirectToSignIn(clerkRequest, requestState), - }); + const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); + const protect = createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); + const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { protect, redirectToSignIn }); let handlerResult: Response = NextResponse.next(); try { handlerResult = (await handler?.(() => authObjWithMethods, request, event)) || handlerResult; } catch (e: any) { - switch (e.message) { - case PROTECT_REWRITE: - // Rewrite to a bogus URL to force not found error - handlerResult = NextResponse.rewrite(`${clerkRequest.clerkUrl.origin}/clerk_${Date.now()}`); - setHeader(handlerResult, constants.Headers.AuthReason, 'protect-rewrite'); - break; - case PROTECT_REDIRECT_TO_URL: - handlerResult = redirectAdapter(e.redirectUrl); - break; - case PROTECT_REDIRECT_TO_SIGN_IN: - handlerResult = authObjWithMethods.redirectToSignIn(); - break; - default: - throw e; - } + handlerResult = handleControlFlowErrors(e, clerkRequest, requestState); } if (isRedirect(handlerResult)) { @@ -121,6 +108,9 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => { } decorateRequest(clerkRequest, handlerResult, requestState); + + // TODO @nikos: we need to make this more generic + // and move the logic in clerk/backend if (requestState.headers) { requestState.headers.forEach((value, key) => { handlerResult.headers.append(key, value); @@ -170,41 +160,62 @@ const redirectAdapter = (url: string | URL) => { const createMiddlewareRedirectToSignIn = ( clerkRequest: ClerkRequest, - requestState: RequestState, ): ClerkMiddlewareAuthObject['redirectToSignIn'] => { return (opts = {}) => { - return createRedirect({ - redirectAdapter, - baseUrl: clerkRequest.clerkUrl, - signInUrl: requestState.signInUrl, - signUpUrl: requestState.signUpUrl, - publishableKey: PUBLISHABLE_KEY, - }).redirectToSignIn({ - returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkRequest.clerkUrl.toString(), - }); + const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN) as any; + err.returnBackUrl = opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkRequest.clerkUrl.toString(); + throw err; }; }; const createMiddlewareProtect = ( clerkRequest: ClerkRequest, authObject: AuthObject, + redirectToSignIn: RedirectFun, ): ClerkMiddlewareAuthObject['protect'] => { return ((params, options) => { const notFound = () => { - throw new Error(PROTECT_REWRITE) as any; + throw new Error(CONTROL_FLOW_ERROR.FORCE_NOT_FOUND) as any; }; const redirect = (url: string) => { - const err = new Error(PROTECT_REDIRECT_TO_URL) as any; + const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_URL) as any; err.redirectUrl = url; throw err; }; - const redirectToSignIn = () => { - throw new Error(PROTECT_REDIRECT_TO_SIGN_IN) as any; - }; - // @ts-expect-error TS is not happy even though the types are correct return createProtect({ request: clerkRequest, redirect, notFound, authObject, redirectToSignIn })(params, options); }) as AuthProtect; }; + +// Handle errors thrown by protect() and redirectToSignIn() calls, +// as we want to align the APIs between middleware, pages and route handlers +// Normally, middleware requires to explicitly return a response, but we want to +// avoid discrepancies between the APIs as it's easy to miss the `return` statement +// especially when copy-pasting code from one place to another. +// This function handles the known errors thrown by the APIs described above, +// and returns the appropriate response. +const handleControlFlowErrors = (e: any, clerkRequest: ClerkRequest, requestState: RequestState): Response => { + switch (e.message) { + case CONTROL_FLOW_ERROR.FORCE_NOT_FOUND: + // Rewrite to a bogus URL to force not found error + return setHeader( + NextResponse.rewrite(`${clerkRequest.clerkUrl.origin}/clerk_${Date.now()}`), + constants.Headers.AuthReason, + 'protect-rewrite', + ); + case CONTROL_FLOW_ERROR.REDIRECT_TO_URL: + return redirectAdapter(e.redirectUrl); + case CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN: + return createRedirect({ + redirectAdapter, + baseUrl: clerkRequest.clerkUrl, + signInUrl: requestState.signInUrl, + signUpUrl: requestState.signUpUrl, + publishableKey: PUBLISHABLE_KEY, + }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); + default: + throw e; + } +}; diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 5ec377be05b..0ab4fd2f4f5 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -1,4 +1,4 @@ -import type { AuthObject, SignedInAuthObject } from '@clerk/backend/internal'; +import type { AuthObject, RedirectFun, SignedInAuthObject } from '@clerk/backend/internal'; import { constants } from '@clerk/backend/internal'; import type { CheckAuthorizationParamsWithCustomPermissions, @@ -6,7 +6,6 @@ import type { } from '@clerk/types'; import { constants as nextConstants } from '../constants'; -import { SIGN_IN_URL } from './constants'; type AuthProtectOptions = { unauthorizedUrl?: string; unauthenticatedUrl?: string }; @@ -44,7 +43,7 @@ export const createProtect = (opts: { * protect() in pages throws a notFound error if signed out * use this callback to customise the behavior */ - redirectToSignIn?: () => void; + redirectToSignIn: RedirectFun; }): AuthProtect => { const { redirectToSignIn, authObject, redirect, notFound, request } = opts; @@ -64,7 +63,7 @@ export const createProtect = (opts: { } if (isPageRequest(request)) { // TODO: Handle runtime values. What happens if runtime values are set in middleware and in ClerkProvider as well? - return redirectToSignIn ? redirectToSignIn() : redirect(SIGN_IN_URL); + return redirectToSignIn(); } return notFound(); }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index fe213872c1b..985a2214f2c 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -108,7 +108,7 @@ export const injectSSRStateIntoObject = (obj: O, authObject: T) => { }; // Auth result will be set as both a query param & header when applicable -export function decorateRequest(req: Request, res: Response, requestState: RequestState): Response { +export function decorateRequest(req: ClerkRequest, res: Response, requestState: RequestState): Response { const { reason, message, status, token } = requestState; // pass-through case, convert to next() if (!res) { @@ -147,6 +147,7 @@ export function decorateRequest(req: Request, res: Response, requestState: Reque [constants.Headers.AuthToken]: token || '', [constants.Headers.AuthMessage]: message || '', [constants.Headers.AuthReason]: reason || '', + [constants.Headers.ClerkUrl]: req.clerkUrl.toString(), }); res.headers.set(nextConstants.Headers.NextRewrite, rewriteURL.href); } diff --git a/packages/nextjs/src/utils/response.ts b/packages/nextjs/src/utils/response.ts index 14b59439773..7cb87b535d9 100644 --- a/packages/nextjs/src/utils/response.ts +++ b/packages/nextjs/src/utils/response.ts @@ -43,7 +43,7 @@ export const isRedirect = (res: Response) => { return res.headers.get(nextConstants.Headers.NextRedirect); }; -export const setHeader = (res: Response, name: string, val: string) => { +export const setHeader = (res: T, name: string, val: string): T => { res.headers.set(name, val); return res; };