From 64f9e699d10e3ecfb6333510bcb793aa16ce1aeb Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 17 Oct 2024 15:59:37 +0300 Subject: [PATCH 1/8] chore(nextjs): Support `unstable_rethrow` inside `clerkMiddleware` --- packages/nextjs/src/server/clerkMiddleware.ts | 143 ++++++++++++++---- 1 file changed, 111 insertions(+), 32 deletions(-) diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 592064c8a34..db023512f68 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -4,6 +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 { notFound as nextjsNotFound } from 'next/navigation'; import type { NextMiddleware, NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -23,11 +24,77 @@ import { setRequestHeadersOnNextResponse, } from './utils'; -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', -}; +const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND'; + +export type NotFoundError = Error & { digest: typeof NOT_FOUND_ERROR_CODE }; + +export function isNotFoundError(error: unknown): error is NotFoundError { + if (typeof error !== 'object' || error === null || !('digest' in error)) { + return false; + } + + return error.digest === NOT_FOUND_ERROR_CODE; +} + +const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'; + +export enum RedirectType { + push = 'push', + replace = 'replace', +} + +export type RedirectError = Error & { + digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${string};${307};`; +} & T; + +export function nextjsRedirectError( + url: string, + type: RedirectType, + extra: Record, + statusCode: 307 = 307, +): RedirectError { + const error = new Error(REDIRECT_ERROR_CODE) as RedirectError; + error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`; + Object.assign(error, extra); + return error; +} + +/** + * Checks an error to determine if it's an error generated by the + * `redirect(url)` helper. + * + * @param error the error that may reference a redirect error + * @returns true if the error is a redirect error + */ +function isRedirectError(error: unknown): error is RedirectError<{ redirectUrl: string }> { + if (typeof error !== 'object' || error === null || !('digest' in error) || typeof error.digest !== 'string') { + return false; + } + + const digest = error.digest.split(';'); + const [errorCode, type] = digest; + const destination = digest.slice(2, -2).join(';'); + const status = digest.at(-2); + + const statusCode = Number(status); + + return ( + errorCode === REDIRECT_ERROR_CODE && + (type === 'replace' || type === 'push') && + typeof destination === 'string' && + !isNaN(statusCode) && + statusCode === 307 + ); +} + +function isRedirectToSignInError(error: unknown): error is RedirectError<{ returnBackUrl: string }> { + if (isRedirectError(error)) { + console.log('dwadawdwada', { ...error }); + return 'returnBackUrl' in error; + } + + return false; +} export type ClerkMiddlewareAuthObject = AuthObject & { protect: AuthProtect; @@ -56,16 +123,19 @@ 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); */ (options?: ClerkMiddlewareOptions): NextMiddleware; + /** * @example * export default clerkMiddleware; @@ -220,9 +290,10 @@ const createMiddlewareRedirectToSignIn = ( clerkRequest: ClerkRequest, ): ClerkMiddlewareAuthObject['redirectToSignIn'] => { return (opts = {}) => { - 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 url = clerkRequest.clerkUrl.toString(); + throw nextjsRedirectError(url, RedirectType.push, { + returnBackUrl: (opts.returnBackUrl === null ? '' : opts.returnBackUrl) || url, + }); }; }; @@ -233,13 +304,18 @@ const createMiddlewareProtect = ( ): ClerkMiddlewareAuthObject['protect'] => { return ((params, options) => { const notFound = () => { - throw new Error(CONTROL_FLOW_ERROR.FORCE_NOT_FOUND) as any; + // re-use Nextjs notFound + // throw new Error(CONTROL_FLOW_ERROR.FORCE_NOT_FOUND) as any; + throw nextjsNotFound(); }; const redirect = (url: string) => { - const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_URL) as any; - err.redirectUrl = url; - throw err; + // const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_URL) as any; + // err.redirectUrl = url; + // throw err; + throw nextjsRedirectError(url, RedirectType.push, { + redirectUrl: url, + }); }; // @ts-expect-error TS is not happy even though the types are correct @@ -255,25 +331,28 @@ const createMiddlewareProtect = ( // 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: requestState.publishableKey, - }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); - default: - throw e; + if (isNotFoundError(e)) { + // 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', + ); + } + + if (isRedirectToSignInError(e)) { + return createRedirect({ + redirectAdapter, + baseUrl: clerkRequest.clerkUrl, + signInUrl: requestState.signInUrl, + signUpUrl: requestState.signUpUrl, + publishableKey: requestState.publishableKey, + }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); + } + + if (isRedirectError(e)) { + return redirectAdapter(e.redirectUrl); } + + throw e; }; From 6d1d640440374f904c2a5cf41ba3d5db8d2fb78d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 17 Oct 2024 16:41:07 +0300 Subject: [PATCH 2/8] tidy things up --- packages/nextjs/src/server/clerkMiddleware.ts | 103 +++------------- packages/nextjs/src/server/nextErrors.ts | 111 ++++++++++++++++++ 2 files changed, 125 insertions(+), 89 deletions(-) create mode 100644 packages/nextjs/src/server/nextErrors.ts diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index db023512f68..a1547d5c506 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -13,6 +13,13 @@ import { withLogger } from '../utils/debugLogger'; import { clerkClient } from './clerkClient'; import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; import { errorThrower } from './errorThrower'; +import { + isNextjsNotFoundError, + isNextjsRedirectError, + isRedirectToSignInError, + nextjsRedirectError, + redirectToSignInError, +} from './nextErrors'; import type { AuthProtect } from './protect'; import { createProtect } from './protect'; import type { NextMiddlewareEvtParam, NextMiddlewareRequestParam, NextMiddlewareReturn } from './types'; @@ -24,78 +31,6 @@ import { setRequestHeadersOnNextResponse, } from './utils'; -const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND'; - -export type NotFoundError = Error & { digest: typeof NOT_FOUND_ERROR_CODE }; - -export function isNotFoundError(error: unknown): error is NotFoundError { - if (typeof error !== 'object' || error === null || !('digest' in error)) { - return false; - } - - return error.digest === NOT_FOUND_ERROR_CODE; -} - -const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'; - -export enum RedirectType { - push = 'push', - replace = 'replace', -} - -export type RedirectError = Error & { - digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${string};${307};`; -} & T; - -export function nextjsRedirectError( - url: string, - type: RedirectType, - extra: Record, - statusCode: 307 = 307, -): RedirectError { - const error = new Error(REDIRECT_ERROR_CODE) as RedirectError; - error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`; - Object.assign(error, extra); - return error; -} - -/** - * Checks an error to determine if it's an error generated by the - * `redirect(url)` helper. - * - * @param error the error that may reference a redirect error - * @returns true if the error is a redirect error - */ -function isRedirectError(error: unknown): error is RedirectError<{ redirectUrl: string }> { - if (typeof error !== 'object' || error === null || !('digest' in error) || typeof error.digest !== 'string') { - return false; - } - - const digest = error.digest.split(';'); - const [errorCode, type] = digest; - const destination = digest.slice(2, -2).join(';'); - const status = digest.at(-2); - - const statusCode = Number(status); - - return ( - errorCode === REDIRECT_ERROR_CODE && - (type === 'replace' || type === 'push') && - typeof destination === 'string' && - !isNaN(statusCode) && - statusCode === 307 - ); -} - -function isRedirectToSignInError(error: unknown): error is RedirectError<{ returnBackUrl: string }> { - if (isRedirectError(error)) { - console.log('dwadawdwada', { ...error }); - return 'returnBackUrl' in error; - } - - return false; -} - export type ClerkMiddlewareAuthObject = AuthObject & { protect: AuthProtect; redirectToSignIn: RedirectFun; @@ -291,9 +226,7 @@ const createMiddlewareRedirectToSignIn = ( ): ClerkMiddlewareAuthObject['redirectToSignIn'] => { return (opts = {}) => { const url = clerkRequest.clerkUrl.toString(); - throw nextjsRedirectError(url, RedirectType.push, { - returnBackUrl: (opts.returnBackUrl === null ? '' : opts.returnBackUrl) || url, - }); + redirectToSignInError(url, opts.returnBackUrl); }; }; @@ -303,20 +236,12 @@ const createMiddlewareProtect = ( redirectToSignIn: RedirectFun, ): ClerkMiddlewareAuthObject['protect'] => { return ((params, options) => { - const notFound = () => { - // re-use Nextjs notFound - // throw new Error(CONTROL_FLOW_ERROR.FORCE_NOT_FOUND) as any; - throw nextjsNotFound(); - }; - - const redirect = (url: string) => { - // const err = new Error(CONTROL_FLOW_ERROR.REDIRECT_TO_URL) as any; - // err.redirectUrl = url; - // throw err; - throw nextjsRedirectError(url, RedirectType.push, { + const notFound = () => nextjsNotFound(); + + const redirect = (url: string) => + nextjsRedirectError(url, { redirectUrl: url, }); - }; // @ts-expect-error TS is not happy even though the types are correct return createProtect({ request: clerkRequest, redirect, notFound, authObject, redirectToSignIn })(params, options); @@ -331,7 +256,7 @@ const createMiddlewareProtect = ( // 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 => { - if (isNotFoundError(e)) { + if (isNextjsNotFoundError(e)) { // Rewrite to a bogus URL to force not found error return setHeader( NextResponse.rewrite(`${clerkRequest.clerkUrl.origin}/clerk_${Date.now()}`), @@ -350,7 +275,7 @@ const handleControlFlowErrors = (e: any, clerkRequest: ClerkRequest, requestStat }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); } - if (isRedirectError(e)) { + if (isNextjsRedirectError(e)) { return redirectAdapter(e.redirectUrl); } diff --git a/packages/nextjs/src/server/nextErrors.ts b/packages/nextjs/src/server/nextErrors.ts new file mode 100644 index 00000000000..c0594fd3a3d --- /dev/null +++ b/packages/nextjs/src/server/nextErrors.ts @@ -0,0 +1,111 @@ +/** + * Clerk's identifiers that are used alongside the ones from Next.js + */ +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', +}; + +/** + * In-house implementation of `notFound()` + * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/not-found.ts + */ +const NOT_FOUND_ERROR_CODE = 'NEXT_NOT_FOUND'; + +type NotFoundError = Error & { + digest: typeof NOT_FOUND_ERROR_CODE; + clerk_digest: typeof CONTROL_FLOW_ERROR.FORCE_NOT_FOUND; +}; + +function isNextjsNotFoundError(error: unknown): error is NotFoundError { + if (typeof error !== 'object' || error === null || !('digest' in error)) { + return false; + } + + return error.digest === NOT_FOUND_ERROR_CODE; +} + +function nextjsNotFound(): never { + const error = new Error(NOT_FOUND_ERROR_CODE); + (error as NotFoundError).digest = NOT_FOUND_ERROR_CODE; + (error as NotFoundError).clerk_digest = CONTROL_FLOW_ERROR.FORCE_NOT_FOUND; + throw error; +} + +/** + * In-house implementation of `redirect()` + * https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts + */ + +const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'; + +type RedirectError = Error & { + digest: `${typeof REDIRECT_ERROR_CODE};${'replace'};${string};${307};`; + clerk_digest: typeof CONTROL_FLOW_ERROR.REDIRECT_TO_URL | typeof CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN; +} & T; + +function nextjsRedirectError( + url: string, + extra: Record, + type: 'replace' = 'replace', + statusCode: 307 = 307, +): never { + const error = new Error(REDIRECT_ERROR_CODE) as RedirectError; + error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`; + error.clerk_digest = CONTROL_FLOW_ERROR.REDIRECT_TO_URL; + Object.assign(error, extra); + throw error; +} + +function redirectToSignInError(url: string, returnBackUrl?: string | URL | null): never { + nextjsRedirectError(url, { + clerk_digest: CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN, + returnBackUrl: returnBackUrl === null ? '' : returnBackUrl || url, + }); +} + +/** + * Checks an error to determine if it's an error generated by the + * `redirect(url)` helper. + * + * @param error the error that may reference a redirect error + * @returns true if the error is a redirect error + */ +function isNextjsRedirectError(error: unknown): error is RedirectError<{ redirectUrl: string | URL }> { + if (typeof error !== 'object' || error === null || !('digest' in error) || typeof error.digest !== 'string') { + return false; + } + + const digest = error.digest.split(';'); + const [errorCode, type] = digest; + const destination = digest.slice(2, -2).join(';'); + const status = digest.at(-2); + + const statusCode = Number(status); + + return ( + errorCode === REDIRECT_ERROR_CODE && + (type === 'replace' || type === 'push') && + typeof destination === 'string' && + !isNaN(statusCode) && + statusCode === 307 + ); +} + +function isRedirectToSignInError(error: unknown): error is RedirectError<{ returnBackUrl: string | URL }> { + if (isNextjsRedirectError(error) && 'clerk_digest' in error) { + return error.clerk_digest === CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_IN; + } + + return false; +} + +export { + isNextjsNotFoundError, + nextjsNotFound, + redirectToSignInError, + nextjsRedirectError, + isNextjsRedirectError, + isRedirectToSignInError, +}; From ac028ebba92a3f83bc26441f5c25cfd383845f1f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 17 Oct 2024 16:43:35 +0300 Subject: [PATCH 3/8] add changeset --- .changeset/shiny-numbers-walk.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/shiny-numbers-walk.md diff --git a/.changeset/shiny-numbers-walk.md b/.changeset/shiny-numbers-walk.md new file mode 100644 index 00000000000..3ffc12abb85 --- /dev/null +++ b/.changeset/shiny-numbers-walk.md @@ -0,0 +1,6 @@ +--- +"@clerk/nextjs": major +--- + +Support `unstable_rethrow` inside `clerkMiddleware`. +We changed the errors thrown by `protect()` inside `clerkMiddleware` in order for `unstable_rethrow` to recognise them and rethrow them. From e44e45184fb3fa406c7db5c1dde01df929b2db23 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 17 Oct 2024 16:51:18 +0300 Subject: [PATCH 4/8] update with in-house not found --- packages/nextjs/src/server/clerkMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index a1547d5c506..d04e1fbb2bf 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -4,7 +4,6 @@ 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 { notFound as nextjsNotFound } from 'next/navigation'; import type { NextMiddleware, NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -17,6 +16,7 @@ import { isNextjsNotFoundError, isNextjsRedirectError, isRedirectToSignInError, + nextjsNotFound, nextjsRedirectError, redirectToSignInError, } from './nextErrors'; From 64836a6d8ad444d66d49974a04fe688814fd45c7 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 18 Oct 2024 12:50:58 +0300 Subject: [PATCH 5/8] e2e tests for unstable_rethrow on 15rc --- integration/presets/longRunningApps.ts | 2 + integration/presets/next.ts | 55 +++++++++++++++++++ .../next-app-router/src/middleware.ts | 5 ++ integration/tests/protect.test.ts | 8 +++ 4 files changed, 70 insertions(+) diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index eee5510608a..2db94271d5f 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -23,12 +23,14 @@ export const createLongRunningApps = () => { { id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks }, { id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes }, { id: 'next.appRouter.withEmailCodes', config: next.appRouter, env: envs.withEmailCodes }, + { id: 'next.appRouter.15RCwithEmailCodes', config: next.appRouter15Rc, env: envs.withEmailCodes }, { id: 'next.appRouter.withEmailCodes_persist_client', config: next.appRouter, env: envs.withEmailCodes_destroy_client, }, { id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles }, + { id: 'next.appRouter.15RCwithCustomRoles', config: next.appRouter15Rc, env: envs.withCustomRoles }, { id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart }, { id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes }, { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles }, diff --git a/integration/presets/next.ts b/integration/presets/next.ts index 68366b7ffa4..7735cd96edd 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -16,6 +16,60 @@ const appRouter = applicationConfig() .addDependency('react-dom', constants.E2E_REACT_DOM_VERSION) .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal); +const appRouter15Rc = applicationConfig() + .setName('next-app-router') + .useTemplate(templates['next-app-router']) + .setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`) + .addScript('setup', constants.E2E_NPM_FORCE ? 'npm i --force' : 'npm i') + .addScript('dev', 'npm run dev') + .addScript('build', 'npm run build') + .addScript('serve', 'npm run start') + .addDependency('next', 'rc') + .addDependency('react', 'rc') + .addDependency('react-dom', 'rc') + .addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal) + .addFile( + 'src/middleware.ts', + () => `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; +import { unstable_rethrow } from 'next/navigation'; + +const csp = \`default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' 'nonce-deadbeef'; + img-src 'self' https://img.clerk.com; + worker-src 'self' blob:; + style-src 'self' 'unsafe-inline'; + frame-src 'self' https://challenges.cloudflare.com; +\`; + +const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']); +const isAdminRoute = createRouteMatcher(['/only-admin(.*)']); +const isCSPRoute = createRouteMatcher(['/csp']); + +export default clerkMiddleware((auth, req) => { + if (isProtectedRoute(req)) { + try { auth().protect() } + catch (e) { unstable_rethrow(e) } + } + + if(isAdminRoute(req)){ + try { auth().protect({role: 'admin'}); } + catch (e) { unstable_rethrow(e) } + } + + if (isCSPRoute(req)) { + req.headers.set('Content-Security-Policy', csp.replace(/\\n/g, '')); + } +}); + +export const config = { + matcher: [ + '/((?!.*\\\\..*|_next).*)', // Don't run middleware on static files + '/', // Run middleware on index page + '/(api|trpc)(.*)', + ], // Run middleware on API routes +};`, + ); + const appRouterTurbo = appRouter .clone() .setName('next-app-router-turbopack') @@ -48,6 +102,7 @@ const appRouterAPWithClerkNextV4 = appRouterQuickstart export const next = { appRouter, + appRouter15Rc, appRouterTurbo, appRouterQuickstart, appRouterAPWithClerkNextLatest, diff --git a/integration/templates/next-app-router/src/middleware.ts b/integration/templates/next-app-router/src/middleware.ts index 2deaf8df808..e8b03dd12ba 100644 --- a/integration/templates/next-app-router/src/middleware.ts +++ b/integration/templates/next-app-router/src/middleware.ts @@ -9,6 +9,7 @@ const csp = `default-src 'self'; `; const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']); +const isAdminRoute = createRouteMatcher(['/only-admin(.*)']); const isCSPRoute = createRouteMatcher(['/csp']); export default clerkMiddleware((auth, req) => { @@ -16,6 +17,10 @@ export default clerkMiddleware((auth, req) => { auth().protect(); } + if (isAdminRoute(req)) { + auth().protect({ role: 'admin' }); + } + if (isCSPRoute(req)) { req.headers.set('Content-Security-Policy', csp.replace(/\n/g, '')); } diff --git a/integration/tests/protect.test.ts b/integration/tests/protect.test.ts index 7b2c6868d20..fc5a5bc1438 100644 --- a/integration/tests/protect.test.ts +++ b/integration/tests/protect.test.ts @@ -56,6 +56,9 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz await u.page.goToRelative('/settings/auth-protect'); await expect(u.page.getByText(/User has access/i)).toBeVisible(); + await u.page.goToRelative('/only-admin'); + await expect(u.page.getByText(/User is admin/i)).toBeVisible(); + // route handler await u.page.goToRelative('/api/settings/'); await expect(u.page.getByText(/userId/i)).toBeVisible(); @@ -89,6 +92,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz await u.po.signIn.waitForMounted(); await u.page.goToRelative('/page-protected'); await u.po.signIn.waitForMounted(); + await u.page.goToRelative('/only-admin'); + await u.po.signIn.waitForMounted(); }); test('Protect in RSCs and RCCs as `viewer`', async ({ page, context }) => { @@ -114,6 +119,9 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz await u.page.goToRelative('/settings/auth-protect'); await expect(u.page.getByText(/this page could not be found/i)).toBeVisible(); + await u.page.goToRelative('/only-admin'); + await expect(u.page.getByText(/this page could not be found/i)).toBeVisible(); + // Route Handler await u.page.goToRelative('/api/settings/').catch(() => {}); From 62bbff6a1cf00105b78840193bb670fde6d2a0e7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 21 Oct 2024 17:04:47 -0700 Subject: [PATCH 6/8] test fixes --- integration/presets/next.ts | 6 +++--- integration/templates/next-app-router/src/middleware.ts | 2 +- .../nextjs/src/server/__tests__/clerkMiddleware.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration/presets/next.ts b/integration/presets/next.ts index 7735cd96edd..a44706c2107 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -45,14 +45,14 @@ const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/sw const isAdminRoute = createRouteMatcher(['/only-admin(.*)']); const isCSPRoute = createRouteMatcher(['/csp']); -export default clerkMiddleware((auth, req) => { +export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) { - try { auth().protect() } + try { await auth.protect() } catch (e) { unstable_rethrow(e) } } if(isAdminRoute(req)){ - try { auth().protect({role: 'admin'}); } + try { await auth.protect({role: 'admin'}); } catch (e) { unstable_rethrow(e) } } diff --git a/integration/templates/next-app-router/src/middleware.ts b/integration/templates/next-app-router/src/middleware.ts index efd9a5238b5..24c94de0e8e 100644 --- a/integration/templates/next-app-router/src/middleware.ts +++ b/integration/templates/next-app-router/src/middleware.ts @@ -18,7 +18,7 @@ export default clerkMiddleware(async (auth, req) => { } if (isAdminRoute(req)) { - auth().protect({ role: 'admin' }); + await auth.protect({ role: 'admin' }); } if (isCSPRoute(req)) { diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 73bf3f6c089..1116bec37ad 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -497,7 +497,7 @@ describe('clerkMiddleware(params)', () => { expect((await clerkClient()).authenticateRequest).toBeCalled(); }); - it('forwards headers from authenticateRequest when auth().protect() is called', async () => { + it('forwards headers from authenticateRequest when auth.protect() is called', async () => { const req = mockRequest({ url: '/protected', headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }), From 13f896694f2523df1d8c389bebe4c015f230540b Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 21 Oct 2024 21:29:52 -0700 Subject: [PATCH 7/8] WIP --- integration/presets/next.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/presets/next.ts b/integration/presets/next.ts index a44706c2107..f0a66b5802a 100644 --- a/integration/presets/next.ts +++ b/integration/presets/next.ts @@ -51,8 +51,8 @@ export default clerkMiddleware(async (auth, req) => { catch (e) { unstable_rethrow(e) } } - if(isAdminRoute(req)){ - try { await auth.protect({role: 'admin'}); } + if (isAdminRoute(req)) { + try { await auth.protect({role: 'admin'}) } catch (e) { unstable_rethrow(e) } } From 8b1713c2653c808d7b8982f3a768ced273ec2755 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 21 Oct 2024 22:20:57 -0700 Subject: [PATCH 8/8] add missing integration test page --- .../templates/next-app-router/src/app/only-admin/page.tsx | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 integration/templates/next-app-router/src/app/only-admin/page.tsx diff --git a/integration/templates/next-app-router/src/app/only-admin/page.tsx b/integration/templates/next-app-router/src/app/only-admin/page.tsx new file mode 100644 index 00000000000..a2817c4a6d9 --- /dev/null +++ b/integration/templates/next-app-router/src/app/only-admin/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
User is admin
; +}