diff --git a/.changeset/ten-worms-report.md b/.changeset/ten-worms-report.md new file mode 100644 index 00000000000..713f10b52d0 --- /dev/null +++ b/.changeset/ten-worms-report.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": major +--- + +Removes deprecated APIs: `authMiddleware()`, `redirectToSignIn()`, and `redirectToSignUp()`. See the migration guide to learn how to update your usage. diff --git a/integration/tests/handshake.test.ts b/integration/tests/handshake.test.ts index 29b561cb6de..b6837ae189b 100644 --- a/integration/tests/handshake.test.ts +++ b/integration/tests/handshake.test.ts @@ -39,14 +39,10 @@ test.describe('Client handshake @generic', () => { .clone() .addFile( 'src/middleware.ts', - () => `import { authMiddleware } from '@clerk/nextjs/server'; - - // Set the paths that don't require the user to be signed in - const publicPaths = ['/', /^(\\/(sign-in|sign-up|app-dir|custom)\\/*).*$/]; + () => `import { clerkMiddleware } from '@clerk/nextjs/server'; export const middleware = (req, evt) => { - return authMiddleware({ - publicRoutes: publicPaths, + return clerkMiddleware({ publishableKey: req.headers.get("x-publishable-key"), secretKey: req.headers.get("x-secret-key"), proxyUrl: req.headers.get("x-proxy-url"), @@ -1256,6 +1252,12 @@ test.describe('Client handshake with organization activation @nextjs', () => { redirect: 'manual', }); + if (testCase.name === 'Header-based auth should not handshake with expired auth') { + console.log(testCase.name); + console.log(res.headers.get('x-clerk-auth-status')); + console.log(res.headers.get('x-clerk-auth-reason')); + } + expect(res.status).toBe(testCase.then.expectStatus); const redirectSearchParams = new URLSearchParams(res.headers.get('location')); expect(redirectSearchParams.get('organization_id')).toBe(testCase.then.fapiOrganizationIdParamValue); @@ -1373,14 +1375,17 @@ test.describe('Client handshake with an organization activation avoids infinite */ const startAppWithOrganizationSyncOptions = async (clerkAPIUrl: string): Promise => { const env = appConfigs.envs.withEmailCodes.clone().setEnvVariable('private', 'CLERK_API_URL', clerkAPIUrl); + const middlewareFile = `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; + + const isProtectedRoute = createRouteMatcher(['/organizations(.*)']) - const middlewareFile = `import { authMiddleware } from '@clerk/nextjs/server'; - // Set the paths that don't require the user to be signed in - const publicPaths = ['/', /^(\\/(sign-in|sign-up|app-dir|custom)\\/*).*$/]; export const middleware = (req, evt) => { const orgSyncOptions = req.headers.get("x-organization-sync-options") - return authMiddleware({ - publicRoutes: publicPaths, + return clerkMiddleware((auth, req) => { + if (isProtectedRoute(req) && !auth().userId) { + auth().redirectToSignIn() + } + }, { publishableKey: req.headers.get("x-publishable-key"), secretKey: req.headers.get("x-secret-key"), proxyUrl: req.headers.get("x-proxy-url"), diff --git a/integration/tests/next-middleware.test.ts b/integration/tests/next-middleware.test.ts deleted file mode 100644 index 3bf7c2eb740..00000000000 --- a/integration/tests/next-middleware.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import type { Application } from '../models/application'; -import { appConfigs } from '../presets'; -import { createTestUtils } from '../testUtils'; - -test.describe('next middleware @nextjs', () => { - test.describe.configure({ mode: 'parallel' }); - let app: Application; - - test.beforeAll(async () => { - const cookieExpires = new Date().getTime() + 60 * 60 * 24; - app = await appConfigs.next.appRouter - .clone() - .addFile( - 'src/middleware.ts', - () => `import { authMiddleware } from '@clerk/nextjs/server'; -import { NextResponse } from "next/server"; - -export default authMiddleware({ - publicRoutes: ['/', '/hash/sign-in', '/hash/sign-up'], - afterAuth: async (auth, req) => { - const response = NextResponse.next(); - response.cookies.set({ - name: "first", - value: "123456789", - sameSite: "Lax", - path: "/", - domain: 'localhost', - secure: false, - expires: ${cookieExpires} - }); - response.cookies.set("second", "987654321", { - sameSite: "Lax", - secure: false, - path: "/", - domain: 'localhost', - expires: ${cookieExpires} - }); - response.cookies.set("third", "foobar", { - sameSite: "Lax", - secure: false, - path: "/", - domain: 'localhost', - expires: ${cookieExpires} - }); - return response; - }, -}); - -export const config = { - matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], -};`, - ) - .addFile( - 'src/app/provider.tsx', - () => `'use client' -import { ClerkProvider } from "@clerk/nextjs" - -export function Provider({ children }: { children: any }) { - return ( - - {children} - - ) -}`, - ) - .addFile( - 'src/app/layout.tsx', - () => `import './globals.css'; -import { Inter } from 'next/font/google'; -import { Provider } from './provider'; - -const inter = Inter({ subsets: ['latin'] }); - -export const metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - - ); -} - `, - ) - .commit(); - await app.setup(); - await app.withEnv(appConfigs.envs.withEmailCodes); - await app.dev(); - }); - - test.afterAll(async () => { - await app.teardown(); - }); - - test('authMiddleware passes through all cookies', async ({ browser }) => { - // See https://playwright.dev/docs/api/class-browsercontext - const context = await browser.newContext(); - const page = await context.newPage(); - const u = createTestUtils({ app, page }); - - await page.goto(app.serverUrl); - await u.po.signIn.waitForMounted(); - - const cookies = await context.cookies(); - - expect(cookies.find(c => c.name == 'first').value).toBe('123456789'); - expect(cookies.find(c => c.name == 'second').value).toBe('987654321'); - expect(cookies.find(c => c.name == 'third').value).toBe('foobar'); - - await context.close(); - }); -}); diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index 127181d0aad..788676885f2 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -3,7 +3,6 @@ exports[`/server public exports should not include a breaking change 1`] = ` [ "auth", - "authMiddleware", "buildClerkProps", "clerkClient", "clerkMiddleware", @@ -11,8 +10,6 @@ exports[`/server public exports should not include a breaking change 1`] = ` "createRouteMatcher", "currentUser", "getAuth", - "redirectToSignIn", - "redirectToSignUp", "verifyToken", ] `; diff --git a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts b/packages/nextjs/src/server/__tests__/authMiddleware.test.ts deleted file mode 100644 index 8051a53e8a5..00000000000 --- a/packages/nextjs/src/server/__tests__/authMiddleware.test.ts +++ /dev/null @@ -1,582 +0,0 @@ -// There is no need to execute the complete authenticateRequest to test authMiddleware -// This mock SHOULD exist before the import of authenticateRequest -import { AuthStatus } from '@clerk/backend/internal'; -import { expectTypeOf } from 'expect-type'; -import type { NextFetchEvent } from 'next/server'; -import { NextRequest, NextResponse } from 'next/server'; - -const authenticateRequestMock = jest.fn().mockResolvedValue({ - toAuth: () => ({}), - headers: new Headers(), -}); - -// Removing this mock will cause the authMiddleware tests to fail due to missing publishable key -// This mock SHOULD exist before the imports -jest.mock('../constants', () => { - return { - PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', - SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', - }; -}); - -jest.mock('../clerkClient', () => { - return { - clerkClient: { - authenticateRequest: authenticateRequestMock, - telemetry: { record: jest.fn() }, - }, - }; -}); - -import { pathToRegexp } from '@clerk/shared/pathToRegexp'; - -import { authMiddleware, DEFAULT_CONFIG_MATCHER, DEFAULT_IGNORED_ROUTES } from '../authMiddleware'; -// used to assert the mock -import { clerkClient } from '../clerkClient'; -import { createRouteMatcher } from '../routeMatcher'; - -/** - * Disable console warnings about config matchers - */ -const consoleWarn = console.warn; -global.console.warn = jest.fn(); -beforeAll(() => { - global.console.warn = jest.fn(); -}); -afterAll(() => { - global.console.warn = consoleWarn; -}); - -type MockRequestParams = { - url: string; - appendDevBrowserCookie?: boolean; - method?: string; - headers?: any; -}; - -const mockRequest = ({ - url, - appendDevBrowserCookie = false, - method = 'GET', - headers = new Headers(), -}: MockRequestParams) => { - const headersWithCookie = new Headers(headers); - if (appendDevBrowserCookie) { - headersWithCookie.append('cookie', '__clerk_db_jwt=test_jwt'); - } - return new NextRequest(new URL(url, 'https://www.clerk.com').toString(), { - method, - headers: headersWithCookie, - }); -}; - -describe('isPublicRoute', () => { - describe('should work with path patterns', function () { - it('matches path and all sub paths using *', () => { - const isPublicRoute = createRouteMatcher(['/hello(.*)']); - expect(isPublicRoute(mockRequest({ url: '/hello' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello/test/a' }))).toBe(true); - }); - - it('matches filenames with specific extensions', () => { - const isPublicRoute = createRouteMatcher(['/(.*).ts', '/(.*).js']); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.ts' }))).toBe(true); - }); - - it('works with single values (non array)', () => { - const isPublicRoute = createRouteMatcher('/test/hello.ts'); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).not.toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).not.toBe(true); - }); - }); - - describe('should work with regex patterns', function () { - it('matches path and all sub paths using *', () => { - const isPublicRoute = createRouteMatcher([/^\/hello.*$/]); - expect(isPublicRoute(mockRequest({ url: '/hello' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello/' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/hello/test/a' }))).toBe(true); - }); - - it('matches filenames with specific extensions', () => { - const isPublicRoute = createRouteMatcher([/^.*\.(ts|js)$/]); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.ts' }))).toBe(true); - }); - - it('works with single values (non array)', () => { - const isPublicRoute = createRouteMatcher(/hello/g); - expect(isPublicRoute(mockRequest({ url: '/hello.js' }))).toBe(true); - expect(isPublicRoute(mockRequest({ url: '/test/hello.js' }))).toBe(true); - }); - }); -}); - -const validRoutes = [ - '/api', - '/api/', - '/api/hello', - '/trpc', - '/trpc/hello', - '/trpc/hello.example', - '/protected', - '/protected/', - '/protected/hello', - '/protected/hello.example/hello', - '/my-protected-page', - '/my/$special/$pages', -]; - -const invalidRoutes = [ - '/_next', - '/favicon.ico', - '/_next/test.json', - '/files/api.pdf', - '/test/api/test.pdf', - '/imgs/img.png', - '/imgs/img-dash.jpg', -]; - -describe('default config matcher', () => { - it('compiles to regex using path-to-regex', () => { - [DEFAULT_CONFIG_MATCHER].flat().forEach(path => { - expect(pathToRegexp(path)).toBeInstanceOf(RegExp); - }); - }); - - describe('does not match any static files or next internals', function () { - it.each(invalidRoutes)(`does not match %s`, path => { - const matcher = createRouteMatcher(DEFAULT_CONFIG_MATCHER); - expect(matcher(mockRequest({ url: path }))).toBe(false); - }); - }); - - describe('matches /api or known framework routes', function () { - it.each(validRoutes)(`matches %s`, path => { - const matcher = createRouteMatcher(DEFAULT_CONFIG_MATCHER); - expect(matcher(mockRequest({ url: path }))).toBe(true); - }); - }); -}); - -describe('default ignored routes matcher', () => { - it('compiles to regex using path-to-regex', () => { - [DEFAULT_IGNORED_ROUTES].flat().forEach(path => { - expect(pathToRegexp(path)).toBeInstanceOf(RegExp); - }); - }); - - describe('matches all static files or next internals', function () { - it.each(invalidRoutes)(`matches %s`, path => { - const matcher = createRouteMatcher(DEFAULT_IGNORED_ROUTES); - expect(matcher(mockRequest({ url: path }))).toBe(true); - }); - }); - - describe('does not match /api or known framework routes', function () { - it.each(validRoutes)(`does not match %s`, path => { - const matcher = createRouteMatcher(DEFAULT_IGNORED_ROUTES); - expect(matcher(mockRequest({ url: path }))).toBe(false); - }); - }); -}); - -describe('authMiddleware(params)', () => { - beforeEach(() => { - authenticateRequestMock.mockClear(); - }); - - describe('without params', function () { - it('redirects to sign-in for protected route', async () => { - const resp = await authMiddleware()(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - }); - - it('renders public route', async () => { - const signInResp = await authMiddleware({ publicRoutes: '/sign-in' })( - mockRequest({ url: '/sign-in' }), - {} as NextFetchEvent, - ); - expect(signInResp?.status).toEqual(200); - expect(signInResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-in'); - - const signUpResp = await authMiddleware({ publicRoutes: ['/sign-up'] })( - mockRequest({ url: '/sign-up' }), - {} as NextFetchEvent, - ); - expect(signUpResp?.status).toEqual(200); - expect(signUpResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/sign-up'); - }); - }); - - describe('with ignoredRoutes', function () { - it('skips auth middleware execution', async () => { - const beforeAuthSpy = jest.fn(); - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - ignoredRoutes: '/ignored', - beforeAuth: beforeAuthSpy, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/ignored' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).not.toBeCalled(); - expect(beforeAuthSpy).not.toBeCalled(); - expect(afterAuthSpy).not.toBeCalled(); - }); - - it('executes auth middleware execution when is not matched', async () => { - const beforeAuthSpy = jest.fn(); - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - ignoredRoutes: '/ignored', - beforeAuth: beforeAuthSpy, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).toBeCalled(); - expect(beforeAuthSpy).toBeCalled(); - expect(afterAuthSpy).toBeCalled(); - }); - }); - - describe('with publicRoutes', function () { - it('renders public route', async () => { - const resp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/public' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(resp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/public'); - }); - - describe('when sign-in/sign-up routes are defined in env', () => { - const currentSignInUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL; - const currentSignUpUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL; - - beforeEach(() => { - process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL = '/custom-sign-in'; - process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL = '/custom-sign-up'; - }); - - afterEach(() => { - process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL = currentSignInUrl; - process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL = currentSignUpUrl; - }); - - it('renders sign-in/sign-up routes', async () => { - const signInResp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/custom-sign-in' }), {} as NextFetchEvent); - expect(signInResp?.status).toEqual(200); - expect(signInResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/custom-sign-in'); - - const signUpResp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/custom-sign-up' }), {} as NextFetchEvent); - expect(signUpResp?.status).toEqual(200); - expect(signUpResp?.headers.get('x-middleware-rewrite')).toEqual('https://www.clerk.com/custom-sign-up'); - }); - }); - - it('redirects to sign-in for protected route', async () => { - const resp = await authMiddleware({ - publicRoutes: '/public', - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - }); - }); - - describe('with beforeAuth', function () { - it('skips auth middleware execution when beforeAuth returns false', async () => { - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - beforeAuth: () => false, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(resp?.headers.get('x-clerk-auth-reason')).toEqual('skip'); - expect(clerkClient.authenticateRequest).not.toBeCalled(); - expect(afterAuthSpy).not.toBeCalled(); - }); - - it('executes auth middleware execution when beforeAuth returns undefined', async () => { - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - beforeAuth: () => undefined, - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(clerkClient.authenticateRequest).toBeCalled(); - expect(afterAuthSpy).toBeCalled(); - }); - - it('skips auth middleware execution when beforeAuth returns NextResponse.redirect', async () => { - const afterAuthSpy = jest.fn(); - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.redirect('https://www.clerk.com/custom-redirect'), - afterAuth: afterAuthSpy, - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual('https://www.clerk.com/custom-redirect'); - expect(clerkClient.authenticateRequest).not.toBeCalled(); - expect(afterAuthSpy).not.toBeCalled(); - }); - - it('executes auth middleware when beforeAuth returns NextResponse', async () => { - const resp = await authMiddleware({ - beforeAuth: () => - NextResponse.next({ - headers: { - 'x-before-auth-header': 'before', - }, - }), - afterAuth: () => - NextResponse.next({ - headers: { - 'x-after-auth-header': 'after', - }, - }), - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - expect(resp?.headers.get('x-before-auth-header')).toEqual('before'); - expect(resp?.headers.get('x-after-auth-header')).toEqual('after'); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - }); - - describe('with afterAuth', function () { - it('redirects to sign-in for protected route and sets redirect as auth reason header', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - })(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - - it('uses authenticateRequest result as auth', async () => { - const req = mockRequest({ url: '/protected' }); - const event = {} as NextFetchEvent; - authenticateRequestMock.mockResolvedValueOnce({ toAuth: () => ({ userId: null }), headers: new Headers() }); - const afterAuthSpy = jest.fn(); - - await authMiddleware({ afterAuth: afterAuthSpy })(req, event); - - expect(clerkClient.authenticateRequest).toBeCalled(); - expect(afterAuthSpy).toBeCalledWith( - { - userId: null, - isPublicRoute: false, - isApiRoute: false, - }, - req, - event, - ); - }); - }); - - describe('authenticateRequest', function () { - it('returns 307 and starts the handshake flow for handshake requestState status', async () => { - const mockLocationUrl = 'https://example.com'; - authenticateRequestMock.mockResolvedValueOnce({ - status: AuthStatus.Handshake, - headers: new Headers({ Location: mockLocationUrl }), - }); - const resp = await authMiddleware()(mockRequest({ url: '/protected' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('Location')).toEqual(mockLocationUrl); - }); - }); -}); - -describe('Dev Browser JWT when redirecting to cross origin', function () { - it('does NOT append the Dev Browser JWT when cookie is missing', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - })(mockRequest({ url: '/protected', appendDevBrowserCookie: false }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected', - ); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - - it('appends the Dev Browser JWT to the search when cookie __clerk_db_jwt exists and location is an Account Portal URL', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - })(mockRequest({ url: '/protected', appendDevBrowserCookie: true }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual( - 'https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=https%3A%2F%2Fwww.clerk.com%2Fprotected&__clerk_db_jwt=test_jwt', - ); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); - - it('does NOT append the Dev Browser JWT if x-clerk-redirect-to header is not set', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.redirect('https://google.com/'), - })(mockRequest({ url: '/protected', appendDevBrowserCookie: true }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('location')).toEqual('https://google.com/'); - expect(clerkClient.authenticateRequest).toBeCalled(); - }); -}); - -describe('isApiRoute', function () { - it('treats route as API route if apiRoutes match the route path', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/api/(.*)'], - })(mockRequest({ url: '/api/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as Page route if apiRoutes do not match the route path', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/api/(.*)'], - })(mockRequest({ url: '/page' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - }); - - it('treats route as API route if apiRoutes prop is missing and route path matches the default matcher (/api/(.*))', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })(mockRequest({ url: '/api/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as API route if apiRoutes prop is missing and route path matches the default matcher (/trpc/(.*))', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })(mockRequest({ url: '/trpc/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as API route if apiRoutes prop is missing and Request method is not-GET,OPTIONS,HEAD', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })(mockRequest({ url: '/products/items', method: 'POST' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('treats route as API route if apiRoutes prop is missing and Request headers Content-Type is application/json', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - })( - mockRequest({ url: '/products/items', headers: new Headers({ 'content-type': 'application/json' }) }), - {} as NextFetchEvent, - ); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); -}); - -describe('401 Response on Api Routes', function () { - it('returns 401 when route is not public and route matches API routes', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/products/(.*)'], - })(mockRequest({ url: '/products/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(401); - expect(resp?.headers.get('content-type')).toEqual('application/json'); - }); - - it('returns 307 when route is not public and route does not match API routes', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/products/(.*)'], - })(mockRequest({ url: '/api/items' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(307); - expect(resp?.headers.get('content-type')).not.toEqual('application/json'); - }); - - it('returns 200 when API route is public', async () => { - const resp = await authMiddleware({ - beforeAuth: () => NextResponse.next(), - publicRoutes: ['/public'], - apiRoutes: ['/public'], - })(mockRequest({ url: '/public' }), {} as NextFetchEvent); - - expect(resp?.status).toEqual(200); - }); -}); - -describe('Type tests', () => { - type AuthMiddleware = Parameters[0]; - describe('AuthMiddleware', () => { - it('is the options argument for authMiddleware', () => { - () => { - authMiddleware({} as AuthMiddleware); - }; - }); - - it('can receive the appropriate keys', () => { - expectTypeOf({ publishableKey: '', secretKey: '' }).toMatchTypeOf(); - expectTypeOf({ secretKey: '' }).toMatchTypeOf(); - expectTypeOf({ publishableKey: '', secretKey: '' }).toMatchTypeOf(); - expectTypeOf({ secretKey: '' }).toMatchTypeOf(); - }); - - describe('Multi domain', () => { - const defaultProps = { publishableKey: '', secretKey: '' }; - - it('proxyUrl (primary app)', () => { - expectTypeOf({ ...defaultProps, proxyUrl: 'test' }).toMatchTypeOf(); - }); - - it('proxyUrl + isSatellite (satellite app)', () => { - expectTypeOf({ ...defaultProps, proxyUrl: 'test', isSatellite: true }).toMatchTypeOf(); - }); - - it('domain + isSatellite (satellite app)', () => { - expectTypeOf({ ...defaultProps, domain: 'test', isSatellite: true }).toMatchTypeOf(); - }); - }); - }); -}); diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts deleted file mode 100644 index c3076b6c6c2..00000000000 --- a/packages/nextjs/src/server/authMiddleware.ts +++ /dev/null @@ -1,346 +0,0 @@ -import type { AuthObject } from '@clerk/backend'; -import type { AuthenticateRequestOptions, ClerkRequest } from '@clerk/backend/internal'; -import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; -import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; -import { eventMethodCalled } from '@clerk/shared/telemetry'; -import type { NextFetchEvent, NextMiddleware, NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; - -import { isRedirect, mergeResponses, serverRedirectWithAuth, setHeader, stringifyHeaders } from '../utils'; -import { withLogger } from '../utils/debugLogger'; -import { clerkClient } from './clerkClient'; -import { createAuthenticateRequestOptions } from './clerkMiddleware'; -import { PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; -import { informAboutProtectedRouteInfo, receivedRequestForIgnoredRoute } from './errors'; -import { errorThrower } from './errorThrower'; -import type { RouteMatcherParam } from './routeMatcher'; -import { createRouteMatcher } from './routeMatcher'; -import type { NextMiddlewareReturn } from './types'; -import { - apiEndpointUnauthorizedNextResponse, - assertKey, - decorateRequest, - redirectAdapter, - setRequestHeadersOnNextResponse, -} from './utils'; - -/** - * The default ideal matcher that excludes the _next directory (internals) and all static files, - * but it will match the root route (/) and any routes that start with /api or /trpc. - */ -export const DEFAULT_CONFIG_MATCHER = ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)']; - -/** - * Any routes matching this path will be ignored by the middleware. - * This is the inverted version of DEFAULT_CONFIG_MATCHER. - */ -export const DEFAULT_IGNORED_ROUTES = [`/((?!api|trpc))(_next.*|.+\\.[\\w]+$)`]; -/** - * Any routes matching this path will be treated as API endpoints by the middleware. - */ -export const DEFAULT_API_ROUTES = ['/api/(.*)', '/trpc/(.*)']; - -type IgnoredRoutesParam = Array | RegExp | string | ((req: NextRequest) => boolean); -type ApiRoutesParam = IgnoredRoutesParam; - -type WithExperimentalClerkUrl = T & { - /** - * When a NextJs app is hosted on a platform different from Vercel - * or inside a container (Netlify, Fly.io, AWS Amplify, docker etc), - * req.url is always set to `localhost:3000` instead of the actual host of the app. - * - * The `authMiddleware` uses the value of the available req.headers in order to construct - * and use the correct url internally. This url is then exposed as `experimental_clerkUrl`, - * intended to be used within `beforeAuth` and `afterAuth` if needed. - */ - experimental_clerkUrl: NextRequest['nextUrl']; -}; - -type BeforeAuthHandler = ( - req: WithExperimentalClerkUrl, - evt: NextFetchEvent, -) => NextMiddlewareReturn | false | Promise; - -type AfterAuthHandler = ( - auth: AuthObject & { isPublicRoute: boolean; isApiRoute: boolean }, - req: WithExperimentalClerkUrl, - evt: NextFetchEvent, -) => NextMiddlewareReturn; - -type AuthMiddlewareParams = AuthenticateRequestOptions & { - /** - * A function that is called before the authentication middleware is executed. - * If a redirect response is returned, the middleware will respect it and redirect the user. - * If false is returned, the auth middleware will not execute and the request will be handled as if the auth middleware was not present. - */ - beforeAuth?: BeforeAuthHandler; - /** - * A function that is called after the authentication middleware is executed. - * This function has access to the auth object and can be used to execute logic based on the auth state. - */ - afterAuth?: AfterAuthHandler; - /** - * A list of routes that should be accessible without authentication. - * You can use glob patterns to match multiple routes or a function to match against the request object. - * Path patterns and regular expressions are supported, for example: `['/foo', '/bar(.*)'] or `[/^\/foo\/.*$/]` - * The sign in and sign up URLs are included by default, unless a function is provided. - * For more information, see: https://clerk.com/docs - */ - publicRoutes?: RouteMatcherParam; - /** - * A list of routes that should be ignored by the middleware. - * This list typically includes routes for static files or Next.js internals. - * For improved performance, these routes should be skipped using the default config.matcher instead. - */ - ignoredRoutes?: IgnoredRoutesParam; - /** - * A list of routes that should be treated as API endpoints. - * When user is signed out, the middleware will return a 401 response for these routes, instead of redirecting the user. - * - * If omitted, the following heuristics will be used to determine an API endpoint: - * - The route path is ['/api/(.*)', '/trpc/(.*)'], - * - or the request has `Content-Type` set to `application/json`, - * - or the request method is not one of: `GET`, `OPTIONS` ,` HEAD` - * - * @default undefined - */ - apiRoutes?: ApiRoutesParam; - /** - * Enables extra debug logging. - */ - debug?: boolean; -}; - -export interface AuthMiddleware { - (params?: AuthMiddlewareParams): NextMiddleware; -} - -/** - * @deprecated `authMiddleware` is deprecated and will be removed in the next major version. - * Use {@link clerkMiddleware}` instead. - * Migration guide: https://clerk.com/docs/upgrade-guides/core-2/nextjs - */ -const authMiddleware: AuthMiddleware = (...args: unknown[]) => { - const [params = {}] = args as [AuthMiddlewareParams?]; - const publishableKey = assertKey(params.publishableKey || PUBLISHABLE_KEY, () => - errorThrower.throwMissingPublishableKeyError(), - ); - const secretKey = assertKey(params.secretKey || SECRET_KEY, () => errorThrower.throwMissingPublishableKeyError()); - const signInUrl = params.signInUrl || SIGN_IN_URL; - const signUpUrl = params.signUpUrl || SIGN_UP_URL; - - const options = { ...params, publishableKey, secretKey, signInUrl, signUpUrl }; - - const isIgnoredRoute = createRouteMatcher(options.ignoredRoutes || DEFAULT_IGNORED_ROUTES); - const isPublicRoute = createRouteMatcher(withDefaultPublicRoutes(options.publicRoutes)); - const isApiRoute = createApiRoutes(options.apiRoutes); - const defaultAfterAuth = createDefaultAfterAuth(isPublicRoute, isApiRoute, options); - - clerkClient.telemetry.record( - eventMethodCalled('authMiddleware', { - publicRoutes: Boolean(options.publicRoutes), - ignoredRoutes: Boolean(options.ignoredRoutes), - beforeAuth: Boolean(options.beforeAuth), - afterAuth: Boolean(options.afterAuth), - }), - ); - - return withLogger('authMiddleware', logger => async (_req: NextRequest, evt: NextFetchEvent) => { - if (options.debug) { - logger.enable(); - } - const clerkRequest = createClerkRequest(_req); - const nextRequest = withNormalizedClerkUrl(clerkRequest, _req); - - logger.debug('URL debug', { - url: nextRequest.nextUrl.href, - method: nextRequest.method, - headers: stringifyHeaders(nextRequest.headers), - nextUrl: nextRequest.nextUrl.href, - clerkUrl: nextRequest.experimental_clerkUrl.href, - }); - - logger.debug('Options debug', { ...options, beforeAuth: !!options.beforeAuth, afterAuth: !!options.afterAuth }); - - if (isIgnoredRoute(nextRequest)) { - logger.debug({ isIgnoredRoute: true }); - if (isDevelopmentFromSecretKey(options.secretKey) && !options.ignoredRoutes) { - console.warn( - receivedRequestForIgnoredRoute( - nextRequest.experimental_clerkUrl.href, - JSON.stringify(DEFAULT_CONFIG_MATCHER), - ), - ); - } - return setHeader(NextResponse.next(), constants.Headers.AuthReason, 'ignored-route'); - } - - const beforeAuthRes = await (options.beforeAuth && options.beforeAuth(nextRequest, evt)); - - if (beforeAuthRes === false) { - logger.debug('Before auth returned false, skipping'); - return setHeader(NextResponse.next(), constants.Headers.AuthReason, 'skip'); - } else if (beforeAuthRes && isRedirect(beforeAuthRes)) { - logger.debug('Before auth returned redirect, following redirect'); - return setHeader(beforeAuthRes, constants.Headers.AuthReason, 'before-auth-redirect'); - } - - const requestState = await clerkClient.authenticateRequest( - clerkRequest, - createAuthenticateRequestOptions(clerkRequest, options), - ); - - const locationHeader = requestState.headers.get('location'); - if (locationHeader) { - // triggering a handshake redirect - return new Response(null, { status: 307, headers: requestState.headers }); - } - - if (requestState.status === AuthStatus.Handshake) { - throw new Error('Clerk: unexpected handshake without redirect'); - } - - const auth = Object.assign(requestState.toAuth(), { - isPublicRoute: isPublicRoute(nextRequest), - isApiRoute: isApiRoute(nextRequest), - }); - - logger.debug(() => ({ auth: JSON.stringify(auth), debug: auth.debug() })); - const afterAuthRes = await (options.afterAuth || defaultAfterAuth)(auth, nextRequest, evt); - const finalRes = mergeResponses(beforeAuthRes, afterAuthRes) || NextResponse.next(); - logger.debug(() => ({ mergedHeaders: stringifyHeaders(finalRes.headers) })); - - if (isRedirect(finalRes)) { - logger.debug('Final response is redirect, following redirect'); - return serverRedirectWithAuth(clerkRequest, finalRes, options); - } - - if (options.debug) { - setRequestHeadersOnNextResponse(finalRes, nextRequest, { [constants.Headers.EnableDebug]: 'true' }); - logger.debug(`Added ${constants.Headers.EnableDebug} on request`); - } - - const result = decorateRequest(clerkRequest, finalRes, requestState, { secretKey }) || NextResponse.next(); - - if (requestState.headers) { - requestState.headers.forEach((value, key) => { - result.headers.append(key, value); - }); - } - - return result; - }); -}; - -export { authMiddleware }; - -const createDefaultAfterAuth = ( - isPublicRoute: ReturnType, - isApiRoute: ReturnType, - options: { signInUrl: string; signUpUrl: string; publishableKey: string; secretKey: string }, -) => { - return (auth: AuthObject, req: WithExperimentalClerkUrl) => { - if (!auth.userId && !isPublicRoute(req)) { - if (isApiRoute(req)) { - informAboutProtectedRoute(req.experimental_clerkUrl.pathname, options, true); - return apiEndpointUnauthorizedNextResponse(); - } else { - informAboutProtectedRoute(req.experimental_clerkUrl.pathname, options, false); - } - return createRedirect({ - redirectAdapter, - signInUrl: options.signInUrl, - signUpUrl: options.signUpUrl, - publishableKey: options.publishableKey, - // We're setting baseUrl to '' here as we want to keep the legacy behavior of - // the redirectToSignIn, redirectToSignUp helpers in the backend package. - baseUrl: '', - }).redirectToSignIn({ returnBackUrl: req.experimental_clerkUrl.href }); - } - return NextResponse.next(); - }; -}; - -const matchRoutesStartingWith = (path: string) => { - path = path.replace(/\/$/, ''); - return new RegExp(`^${path}(/.*)?$`); -}; - -const withDefaultPublicRoutes = (publicRoutes: RouteMatcherParam | undefined) => { - if (typeof publicRoutes === 'function') { - return publicRoutes; - } - - const routes = [publicRoutes || ''].flat().filter(Boolean); - // TODO: refactor it to use common config file eg SIGN_IN_URL from ./clerkClient - // we use process.env for now to support testing - const signInUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL || ''; - if (signInUrl) { - routes.push(matchRoutesStartingWith(signInUrl)); - } - // TODO: refactor it to use common config file eg SIGN_UP_URL from ./clerkClient - // we use process.env for now to support testing - const signUpUrl = process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL || ''; - if (signUpUrl) { - routes.push(matchRoutesStartingWith(signUpUrl)); - } - return routes; -}; - -// - Default behavior: -// If the route path is `['/api/(.*)*', '*/trpc/(.*)']` -// or Request has `Content-Type: application/json` -// or Request method is not-GET,OPTIONS,HEAD, -// then this is considered an API route. -// -// - If the user has provided a specific `apiRoutes` prop in `authMiddleware` then all the above are discarded, -// and only routes that match the user’s provided paths are considered API routes. -const createApiRoutes = ( - apiRoutes: RouteMatcherParam | undefined, -): ((req: WithExperimentalClerkUrl) => boolean) => { - if (apiRoutes) { - return createRouteMatcher(apiRoutes); - } - const isDefaultApiRoute = createRouteMatcher(DEFAULT_API_ROUTES); - return (req: WithExperimentalClerkUrl) => - isDefaultApiRoute(req) || isRequestMethodIndicatingApiRoute(req) || isRequestContentTypeJson(req); -}; - -const isRequestContentTypeJson = (req: NextRequest): boolean => { - const requestContentType = req.headers.get(constants.Headers.ContentType); - return requestContentType === constants.ContentTypes.Json; -}; - -const isRequestMethodIndicatingApiRoute = (req: NextRequest): boolean => { - const requestMethod = req.method.toLowerCase(); - return !['get', 'head', 'options'].includes(requestMethod); -}; - -const withNormalizedClerkUrl = ( - clerkRequest: ClerkRequest, - nextRequest: NextRequest, -): WithExperimentalClerkUrl => { - const res = nextRequest.nextUrl.clone(); - res.port = clerkRequest.clerkUrl.port; - res.protocol = clerkRequest.clerkUrl.protocol; - res.host = clerkRequest.clerkUrl.host; - return Object.assign(nextRequest, { experimental_clerkUrl: res }); -}; - -const informAboutProtectedRoute = ( - path: string, - options: AuthMiddlewareParams & { secretKey: string }, - isApiRoute: boolean, -) => { - if (options.debug || isDevelopmentFromSecretKey(options.secretKey)) { - console.warn( - informAboutProtectedRouteInfo( - path, - !!options.publicRoutes, - !!options.ignoredRoutes, - isApiRoute, - DEFAULT_IGNORED_ROUTES, - ), - ); - } -}; diff --git a/packages/nextjs/src/server/errors.ts b/packages/nextjs/src/server/errors.ts index eba3589e233..fa179ff1672 100644 --- a/packages/nextjs/src/server/errors.ts +++ b/packages/nextjs/src/server/errors.ts @@ -2,7 +2,7 @@ export const missingDomainAndProxy = ` Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl. 1) With middleware - e.g. export default clerkMiddleware({domain:'YOUR_DOMAIN',isSatellite:true}); // or the deprecated authMiddleware() + e.g. export default clerkMiddleware({domain:'YOUR_DOMAIN',isSatellite:true}); 2) With environment variables e.g. NEXT_PUBLIC_CLERK_DOMAIN='YOUR_DOMAIN' NEXT_PUBLIC_CLERK_IS_SATELLITE='true' @@ -13,26 +13,16 @@ Invalid signInUrl. A satellite application requires a signInUrl for development Check if signInUrl is missing from your configuration or if it is not an absolute URL 1) With middleware - e.g. export default clerkMiddleware({signInUrl:'SOME_URL', isSatellite:true}); // or the deprecated authMiddleware() + e.g. export default clerkMiddleware({signInUrl:'SOME_URL', isSatellite:true}); 2) With environment variables e.g. NEXT_PUBLIC_CLERK_SIGN_IN_URL='SOME_URL' NEXT_PUBLIC_CLERK_IS_SATELLITE='true'`; -export const receivedRequestForIgnoredRoute = (url: string, matcher: string) => - `Clerk: The middleware was skipped for this request URL: ${url}. For performance reasons, it's recommended to your middleware matcher to: -export const config = { - matcher: ${matcher}, -}; - -Alternatively, you can set your own ignoredRoutes. See https://clerk.com/docs/nextjs/middleware -(This log only appears in development mode) -`; - export const getAuthAuthHeaderMissing = () => authAuthHeaderMissing('getAuth'); export const authAuthHeaderMissing = (helperName = 'auth') => - `Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware() (or the deprecated authMiddleware()). Please ensure the following: -- clerkMiddleware() (or the deprecated authMiddleware()) is used in your Next.js Middleware. + `Clerk: ${helperName}() was called but Clerk can't detect usage of clerkMiddleware(). Please ensure the following: +- clerkMiddleware() is used in your Next.js Middleware. - Your Middleware matcher is configured to match this route or page. - If you are using the src directory, make sure the Middleware file is inside of it. @@ -48,54 +38,6 @@ To resolve this issue, make sure your system's clock is set to the correct time ${verifyMessage}`; -export const infiniteRedirectLoopDetected = () => - `Clerk: Infinite redirect loop detected. That usually means that we were not able to determine the auth state for this request. A list of common causes and solutions follows. - -Reason 1: -Your Clerk instance keys are incorrect, or you recently changed keys (Publishable Key, Secret Key). -How to resolve: --> Make sure you're using the correct keys from the Clerk Dashboard. If you changed keys recently, make sure to clear your browser application data and cookies. - -Reason 2: -A bug that may have already been fixed in the latest version of Clerk NextJS package. -How to resolve: --> Make sure you are using the latest version of '@clerk/nextjs' and 'next'. -`; - -export const informAboutProtectedRouteInfo = ( - path: string, - hasPublicRoutes: boolean, - hasIgnoredRoutes: boolean, - isApiRoute: boolean, - defaultIgnoredRoutes: string[], -) => { - const infoText = isApiRoute - ? `INFO: Clerk: The request to ${path} is being protected (401) because there is no signed-in user, and the path is included in \`apiRoutes\`. To prevent this behavior, choose one of:` - : `INFO: Clerk: The request to ${path} is being redirected because there is no signed-in user, and the path is not included in \`ignoredRoutes\` or \`publicRoutes\`. To prevent this behavior, choose one of:`; - const apiRoutesText = isApiRoute - ? `To prevent Clerk authentication from protecting (401) the api route, remove the rule matching "${path}" from the \`apiRoutes\` array passed to authMiddleware` - : undefined; - const publicRoutesText = hasPublicRoutes - ? `To make the route accessible to both signed in and signed out users, add "${path}" to the \`publicRoutes\` array passed to authMiddleware` - : `To make the route accessible to both signed in and signed out users, pass \`publicRoutes: ["${path}"]\` to authMiddleware`; - const ignoredRoutes = [...defaultIgnoredRoutes, path].map(r => `"${r}"`).join(', '); - const ignoredRoutesText = hasIgnoredRoutes - ? `To prevent Clerk authentication from running at all, add "${path}" to the \`ignoredRoutes\` array passed to authMiddleware` - : `To prevent Clerk authentication from running at all, pass \`ignoredRoutes: [${ignoredRoutes}]\` to authMiddleware`; - const afterAuthText = - "Pass a custom `afterAuth` to authMiddleware, and replace Clerk's default behavior of redirecting unless a route is included in publicRoutes"; - - return `${infoText} - -${[apiRoutesText, publicRoutesText, ignoredRoutesText, afterAuthText] - .filter(Boolean) - .map((text, index) => `${index + 1}. ${text}`) - .join('\n')} - -For additional information about middleware, please visit https://clerk.com/docs/nextjs/middleware -(This log only appears in development mode, or if \`debug: true\` is passed to authMiddleware)`; -}; - export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`; export const encryptionKeyInvalid = `Clerk: Unable to decrypt request data, this usually means the encryption key is invalid. Ensure the encryption key is properly set. For more information, see: https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys. (code=encryption_key_invalid)`; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index cbfa2e800b9..f7f4e073212 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -63,10 +63,3 @@ export type { Token, User, } from '@clerk/backend'; - -/** - * Deprecated APIs - * These APIs will be removed in v6 - */ -export { authMiddleware } from './authMiddleware'; -export { redirectToSignIn, redirectToSignUp } from './redirectHelpers'; diff --git a/packages/nextjs/src/server/redirectHelpers.ts b/packages/nextjs/src/server/redirectHelpers.ts deleted file mode 100644 index 8a95633588e..00000000000 --- a/packages/nextjs/src/server/redirectHelpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { constants, createRedirect } from '@clerk/backend/internal'; -import { NextResponse } from 'next/server'; - -import { setHeader } from '../utils'; -import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from './constants'; - -const redirectAdapter = (url: string) => { - const res = NextResponse.redirect(url); - return setHeader(res, constants.Headers.ClerkRedirectTo, 'true'); -}; - -const redirectHelpers = createRedirect({ - redirectAdapter, - signInUrl: SIGN_IN_URL, - signUpUrl: SIGN_UP_URL, - publishableKey: PUBLISHABLE_KEY, - // We're setting baseUrl to '' here as we want to keep the legacy behavior of - // the redirectToSignIn, redirectToSignUp helpers in the backend package. - baseUrl: '', -}); - -/** - * @deprecated - * This function is deprecated and will be removed in a future release. Please use `auth().redirectToSignIn()` instead. - */ -export const redirectToSignIn = redirectHelpers.redirectToSignIn; - -/** - * @deprecated - * This function is deprecated and will be removed in a future release. Please use `auth().redirectToSignIn()` instead. - */ -export const redirectToSignUp = redirectHelpers.redirectToSignUp; diff --git a/packages/nextjs/src/utils/__tests__/matcher.test.ts b/packages/nextjs/src/utils/__tests__/matcher.test.ts index e06cde3939f..df917c5585e 100644 --- a/packages/nextjs/src/utils/__tests__/matcher.test.ts +++ b/packages/nextjs/src/utils/__tests__/matcher.test.ts @@ -9,7 +9,7 @@ const createMatcher = (config: { matcher: string[] }) => (path: string) => { describe('nextjs matcher', () => { /** * 🚨🚨🚨🚨 - * This is the matcher we document for clerkMiddleware + authMiddleware. + * This is the matcher we document for clerkMiddleware. * Any change made to the matcher here needs to be reflected in the documentation, the dashboard * and vice versa. * 🚨🚨🚨🚨 diff --git a/packages/nextjs/src/utils/__tests__/response.test.ts b/packages/nextjs/src/utils/__tests__/response.test.ts deleted file mode 100644 index fd7d85056f9..00000000000 --- a/packages/nextjs/src/utils/__tests__/response.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { NextResponse } from 'next/server'; - -import { mergeResponses } from '../response'; - -describe('mergeResponses', function () { - it('should fail unless one response is passed', function () { - expect(mergeResponses(null, undefined)).toBe(undefined); - }); - - it('should handle non-response values', function () { - const response1 = new NextResponse(); - response1.headers.set('foo', '1'); - const finalResponse = mergeResponses(null, undefined, response1); - expect(finalResponse!.headers.get('foo')).toEqual('1'); - }); - - it('should merge the headers', function () { - const response1 = new NextResponse(); - const response2 = new NextResponse(); - response1.headers.set('foo', '1'); - response1.headers.set('bar', '1'); - response2.headers.set('bar', '2'); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.headers.get('foo')).toEqual('1'); - expect(finalResponse!.headers.get('bar')).toEqual('2'); - }); - - it('should merge the cookies', function () { - const response1 = new NextResponse(); - const response2 = new NextResponse(); - response1.cookies.set('foo', '1'); - response1.cookies.set('second', '2'); - response1.cookies.set('bar', '1'); - response2.cookies.set('bar', '2'); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.cookies.get('foo')).toEqual(response1.cookies.get('foo')); - expect(finalResponse!.cookies.get('second')).toEqual(response1.cookies.get('second')); - expect(finalResponse!.cookies.get('bar')).toEqual(response2.cookies.get('bar')); - }); - - it('should merge the cookies with non-response values', function () { - const response2 = NextResponse.next(); - response2.cookies.set('foo', '1'); - response2.cookies.set({ - name: 'second', - value: '2', - path: '/', - sameSite: 'none', - secure: true, - }); - response2.cookies.set('bar', '1', { - sameSite: 'none', - secure: true, - }); - const finalResponse = mergeResponses(null, response2); - expect(finalResponse!.cookies.get('foo')).toEqual(response2.cookies.get('foo')); - expect(finalResponse!.cookies.get('second')).toEqual(response2.cookies.get('second')); - expect(finalResponse!.cookies.get('bar')).toEqual(response2.cookies.get('bar')); - }); - - it('should use the status of the last response', function () { - const response1 = new NextResponse('', { status: 200, statusText: 'OK' }); - const response2 = new NextResponse('', { status: 201, statusText: 'Created' }); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.status).toEqual(response2.status); - expect(finalResponse!.statusText).toEqual(response2.statusText); - }); - - it('should use the body of the last response', function () { - const response1 = new NextResponse('1'); - const response2 = new NextResponse('2'); - const finalResponse = mergeResponses(response1, response2); - expect(finalResponse!.body).toEqual(response2.body); - }); -}); diff --git a/packages/nextjs/src/utils/response.ts b/packages/nextjs/src/utils/response.ts index 7cb87b535d9..56c11df06b0 100644 --- a/packages/nextjs/src/utils/response.ts +++ b/packages/nextjs/src/utils/response.ts @@ -1,44 +1,5 @@ -import { NextResponse } from 'next/server'; - import { constants as nextConstants } from '../constants'; -/** - * A function that merges two Response objects into a single response. - * The final response respects the body and the status of the last response, - * but the cookies and headers of all responses are merged. - */ -export const mergeResponses = (...responses: (NextResponse | Response | null | undefined | void)[]) => { - const normalisedResponses = responses.filter(Boolean).map(res => { - // If the response is a NextResponse, we can just return it - if (res instanceof NextResponse) { - return res; - } - - return new NextResponse(res!.body, res!); - }); - - if (normalisedResponses.length === 0) { - return; - } - - const lastResponse = normalisedResponses[normalisedResponses.length - 1]; - const finalResponse = new NextResponse(lastResponse.body, lastResponse); - - for (const response of normalisedResponses) { - response.headers.forEach((value: string, name: string) => { - finalResponse.headers.set(name, value); - }); - - response.cookies.getAll().forEach(cookie => { - const { name, value, ...options } = cookie; - - finalResponse.cookies.set(name, value, options); - }); - } - - return finalResponse; -}; - export const isRedirect = (res: Response) => { return res.headers.get(nextConstants.Headers.NextRedirect); }; diff --git a/playground/app-router/src/middleware.ts b/playground/app-router/src/middleware.ts index 642c1859ec1..8c4dd060d10 100644 --- a/playground/app-router/src/middleware.ts +++ b/playground/app-router/src/middleware.ts @@ -1,35 +1,7 @@ import { clerkMiddleware } from '@clerk/nextjs/server'; -import { NextMiddleware, NextResponse } from 'next/server'; -export default clerkMiddleware((auth)=> { - -}) - -// export default authMiddleware({ -// publicRoutes: ['/'], -// beforeAuth: req => { -// // console.log('middleware:beforeAuth', req.url); -// if (req.nextUrl.searchParams.get('redirect')) { -// return NextResponse.redirect('https://google.com'); -// } -// const res = NextResponse.next(); -// res.headers.set('x-before-auth', 'true'); -// return res; -// }, -// afterAuth: (auth, req) => { -// // console.log('middleware:afterAuth', auth.userId, req.url, auth.isPublicRoute); -// if (!auth.userId && !auth.isPublicRoute) { -// const url = new URL('/sign-in', req.url); -// url.searchParams.append('redirect_url', req.url); -// return NextResponse.redirect(url); -// } -// const res = NextResponse.next(); -// res.headers.set('x-after-auth', 'true'); -// return res; -// }, -// debug: true -// }); +export default clerkMiddleware() export const config = { matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'], diff --git a/playground/nextjs/middleware.ts b/playground/nextjs/middleware.ts index e41136e55ed..cf79854ca19 100644 --- a/playground/nextjs/middleware.ts +++ b/playground/nextjs/middleware.ts @@ -1,11 +1,6 @@ -import { authMiddleware } from '@clerk/nextjs/server'; +import { clerkMiddleware } from '@clerk/nextjs/server'; -// Set the paths that don't require the user to be signed in -const publicPaths = ['/', /^(\/(sign-in|sign-up|app-dir|custom)\/*).*$/]; - -export default authMiddleware({ - publicRoutes: publicPaths, -}); +export default clerkMiddleware(); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],