diff --git a/.changeset/protect-server-action-401.md b/.changeset/protect-server-action-401.md new file mode 100644 index 00000000000..b11c18078d8 --- /dev/null +++ b/.changeset/protect-server-action-401.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': major +--- + +Return 401 instead of 404 for unauthenticated server action requests in `auth.protect()` diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 69419e2d504..9e82f19c75c 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -518,6 +518,31 @@ describe('clerkMiddleware(params)', () => { expect((await clerkClient()).authenticateRequest).toBeCalled(); }); + it('returns 401 when protect is called, the user is signed out, and the request is a server action', async () => { + const req = mockRequest({ + url: '/protected', + headers: new Headers({ + 'next-url': '/protected', + 'next-action': '1', + }), + appendDevBrowserCookie: true, + }); + + authenticateRequestMock.mockResolvedValueOnce({ + publishableKey, + status: AuthStatus.SignedOut, + headers: new Headers(), + toAuth: () => ({ tokenType: TokenType.SessionToken, userId: null }), + }); + + const resp = await clerkMiddleware(async auth => { + await auth.protect(); + })(req, {} as NextFetchEvent); + + expect(resp?.status).toEqual(401); + expect((await clerkClient()).authenticateRequest).toBeCalled(); + }); + it('throws an unauthorized error when protect is called and the machine auth token is invalid', async () => { const req = mockRequest({ url: '/protected', diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index 3171c263aea..7a1ea7a05d3 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -126,6 +126,9 @@ export function createProtect(opts: { // TODO: Handle runtime values. What happens if runtime values are set in middleware and in ClerkProvider as well? return redirectToSignIn(); } + if (isServerActionRequest(request)) { + return unauthorized(); + } return notFound(); }; diff --git a/packages/upgrade/src/versions/core-3/changes/nextjs-protect-server-action-401.md b/packages/upgrade/src/versions/core-3/changes/nextjs-protect-server-action-401.md new file mode 100644 index 00000000000..32f3203a89b --- /dev/null +++ b/packages/upgrade/src/versions/core-3/changes/nextjs-protect-server-action-401.md @@ -0,0 +1,32 @@ +--- +title: '`auth.protect()` returns 401 instead of 404 for unauthenticated server actions' +packages: ['nextjs'] +matcher: 'auth\.protect\(' +matcherFlags: 'm' +category: 'breaking' +--- + +`auth.protect()` in `clerkMiddleware()` now returns a `401 Unauthorized` response instead of a `404 Not Found` when an unauthenticated request is made from a server action. + +Previously, unauthenticated server action requests would receive a `404` response, which made it difficult for client-side code to distinguish between "not found" and "not authenticated." The new behavior returns `401`, which is the semantically correct HTTP status code for unauthenticated requests. + +### Who is affected + +If your application uses `auth.protect()` inside `clerkMiddleware()` and you have server actions that may be called by unauthenticated users, the HTTP status code returned will change from `404` to `401`. + +### What to do + +If you have client-side error handling that checks for `404` responses from server actions when the user is signed out, update it to handle `401` instead: + +```diff +try { + await myServerAction(); +} catch (error) { +- if (error.status === 404) { ++ if (error.status === 401) { + // Handle unauthenticated user + } +} +``` + +No changes are required if you are not explicitly checking the HTTP status code in your error handling.