diff --git a/.changeset/solid-yaks-enter.md b/.changeset/solid-yaks-enter.md new file mode 100644 index 00000000000..c95fffe9b9a --- /dev/null +++ b/.changeset/solid-yaks-enter.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Fixes a bug where `getAuth()` returns unauthenticated during Next.js Pages Router client-side navigations (such as `_next/data` requests), when the `__client_uat` cookie is missing. diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 3a9640fe23e..605ae0c34aa 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -988,6 +988,43 @@ describe('tokens.authenticateRequest(options)', () => { expect(requestState.toAuth()).toBeNull(); }); + test('cookieToken: returns signed in for non-document request when no clientUat but valid session token (production)', async () => { + server.use( + http.get('https://api.clerk.test/v1/jwks', () => { + return HttpResponse.json(mockJwks); + }), + ); + + const requestState = await authenticateRequest( + mockRequestWithCookies({ ...defaultHeaders, 'sec-fetch-dest': 'empty' }, { __session: mockJwt }), + mockOptions({ publishableKey: PK_LIVE }), + ); + + expect(requestState).toBeSignedIn(); + expect(requestState.toAuth()).toBeSignedInToAuth(); + }); + + test('cookieToken: returns signed in for non-document request when no clientUat but valid session token (development)', async () => { + server.use( + http.get('https://api.clerk.test/v1/jwks', () => { + return HttpResponse.json(mockJwks); + }), + ); + + const requestState = await authenticateRequest( + mockRequestWithCookies( + { ...defaultHeaders, 'sec-fetch-dest': 'empty' }, + { __clerk_db_jwt: 'deadbeef', __session: mockJwt }, + ), + mockOptions({ + secretKey: 'test_deadbeef', + }), + ); + + expect(requestState).toBeSignedIn(); + expect(requestState.toAuth()).toBeSignedInToAuth(); + }); + test('cookieToken: returns handshake when no cookies in development [5y]', async () => { const requestState = await authenticateRequest( mockRequestWithCookies({}), diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 9778a5e89b3..f5969076d11 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -607,7 +607,9 @@ export const authenticateRequest: AuthenticateRequest = (async ( // This can eagerly run handshake since client_uat is SameSite=Strict in dev if (!hasActiveClient && hasSessionToken) { - return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenWithoutClientUAT, ''); + if (handshakeService.isRequestEligibleForHandshake()) { + return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenWithoutClientUAT, ''); + } } if (hasActiveClient && !hasSessionToken) { @@ -621,7 +623,7 @@ export const authenticateRequest: AuthenticateRequest = (async ( return handleSessionTokenError(decodedErrors[0], 'cookie'); } - if (decodeResult.payload.iat < authenticateContext.clientUat) { + if (authenticateContext.clientUat && decodeResult.payload.iat < authenticateContext.clientUat) { return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenIATBeforeClientUAT, ''); }