diff --git a/.changeset/pink-eggs-enjoy.md b/.changeset/pink-eggs-enjoy.md new file mode 100644 index 00000000000..8699873d62e --- /dev/null +++ b/.changeset/pink-eggs-enjoy.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': minor +--- + +Replace redirectUrl of protect with `unauthorizedUrl` and `unauthenticatedUrl`. diff --git a/packages/nextjs/src/server/clerkMiddleware.test.ts b/packages/nextjs/src/server/clerkMiddleware.test.ts index c03142d2e40..4c947b022c8 100644 --- a/packages/nextjs/src/server/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/clerkMiddleware.test.ts @@ -330,7 +330,7 @@ describe('authMiddleware(params)', () => { expect(clerkClient.authenticateRequest).toBeCalled(); }); - it('throws a not found error when protect is called with RBAC params the user does not fulfil, and is a page request', async () => { + it('throws a not found error when protect is called with RBAC params the user does not fulfill, and is a page request', async () => { const req = mockRequest({ url: '/protected', headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), @@ -352,7 +352,7 @@ describe('authMiddleware(params)', () => { expect(clerkClient.authenticateRequest).toBeCalled(); }); - it('redirects to redirectUrl when protect is called with the redirectUrl param, the user is signed out, and is a page request', async () => { + it('redirects to unauthenticatedUrl when protect is called with the redirectUrl param, the user is signed out, and is a page request', async () => { const req = mockRequest({ url: '/protected', headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), @@ -366,11 +366,41 @@ describe('authMiddleware(params)', () => { }); const resp = await clerkMiddleware(auth => { - auth().protect({ redirectUrl: 'https://www.clerk.com/hello' }); + auth().protect({ unauthenticatedUrl: 'https://www.clerk.com/hello' }); })(req, {} as NextFetchEvent); expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toContain('https://www.clerk.com/hello'); + expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/hello'); + 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 redirectUrl param, the user does not fulfill the RBAC params, and is a page request', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), + appendDevBrowserCookie: true, + }); + + authenticateRequestMock.mockResolvedValueOnce({ + status: AuthStatus.SignedIn, + headers: new Headers(), + toAuth: () => ({ userId: 'user-id', has: () => false }), + }); + + const resp = await clerkMiddleware(auth => { + auth().protect( + { role: 'random-role' }, + { + unauthorizedUrl: 'https://www.clerk.com/discover', + unauthenticatedUrl: 'https://www.clerk.com/hello', + }, + ); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(307); + expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/discover'); expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('redirect'); expect(resp?.headers.get(constants.Headers.ClerkRedirectTo)).toEqual('true'); expect(clerkClient.authenticateRequest).toBeCalled(); diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 0ebdf05a10b..5ec377be05b 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -8,7 +8,7 @@ import type { import { constants as nextConstants } from '../constants'; import { SIGN_IN_URL } from './constants'; -type AuthProtectOptions = { redirectUrl?: string }; +type AuthProtectOptions = { unauthorizedUrl?: string; unauthenticatedUrl?: string }; /** * @experimental @@ -17,10 +17,12 @@ type AuthProtectOptions = { redirectUrl?: string }; */ export interface AuthProtect { (params?: CheckAuthorizationParamsWithCustomPermissions, options?: AuthProtectOptions): SignedInAuthObject; + ( params?: (has: CheckAuthorizationWithCustomPermissions) => boolean, options?: AuthProtectOptions, ): SignedInAuthObject; + (options?: AuthProtectOptions): SignedInAuthObject; } @@ -47,16 +49,18 @@ export const createProtect = (opts: { const { redirectToSignIn, authObject, redirect, notFound, request } = opts; return ((...args: any[]) => { - const paramsOrFunction = args[0]?.redirectUrl + const optionValuesAsParam = args[0]?.unauthenticatedUrl || args[0]?.unauthorizedUrl; + const paramsOrFunction = optionValuesAsParam ? undefined : (args[0] as | CheckAuthorizationParamsWithCustomPermissions | ((has: CheckAuthorizationWithCustomPermissions) => boolean)); - const redirectUrl = (args[0]?.redirectUrl || args[1]?.redirectUrl) as string | undefined; + const unauthenticatedUrl = (args[0]?.unauthenticatedUrl || args[1]?.unauthenticatedUrl) as string | undefined; + const unauthorizedUrl = (args[0]?.unauthorizedUrl || args[1]?.unauthorizedUrl) as string | undefined; const handleUnauthenticated = () => { - if (redirectUrl) { - return redirect(redirectUrl); + if (unauthenticatedUrl) { + return redirect(unauthenticatedUrl); } if (isPageRequest(request)) { // TODO: Handle runtime values. What happens if runtime values are set in middleware and in ClerkProvider as well? @@ -66,8 +70,8 @@ export const createProtect = (opts: { }; const handleUnauthorized = () => { - if (redirectUrl) { - return redirect(redirectUrl); + if (unauthorizedUrl) { + return redirect(unauthorizedUrl); } return notFound(); };