From ac3226165f3da644e9f08f6c905b025f8a41a6a8 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 13 Oct 2026 10:25:32 -0700 Subject: [PATCH 1/5] test(nextjs): cover getAuth() and auth() with machine tokens --- integration/testUtils/machineAuthHelpers.ts | 8 +- integration/tests/next-machine.test.ts | 123 +++++++++++++++++- .../server/__tests__/createGetAuth.test.ts | 33 +++++ .../__tests__/getAuthDataFromRequest.test.ts | 47 ++++++- 4 files changed, 203 insertions(+), 8 deletions(-) diff --git a/integration/testUtils/machineAuthHelpers.ts b/integration/testUtils/machineAuthHelpers.ts index 4b0ac197340..30d182bb84c 100644 --- a/integration/testUtils/machineAuthHelpers.ts +++ b/integration/testUtils/machineAuthHelpers.ts @@ -24,8 +24,8 @@ export type FakeMachineNetwork = { cleanup: () => Promise; }; -async function createFakeMachineNetwork(clerkClient: ClerkClient): Promise { - const fakeCompanyName = faker.company.name(); +export async function createFakeMachineNetwork(clerkClient: ClerkClient): Promise { + const fakeCompanyName = `${faker.company.name()} ${Date.now()}`; const primaryServer = await clerkClient.machines.create({ name: `${fakeCompanyName} Primary API Server`, @@ -68,7 +68,7 @@ async function createFakeMachineNetwork(clerkClient: ClerkClient): Promise { +export async function createJwtM2MToken(clerkClient: ClerkClient, senderSecretKey: string): Promise { return clerkClient.m2m.createToken({ machineSecretKey: senderSecretKey, secondsUntilExpiration: 60 * 30, @@ -181,7 +181,7 @@ export type MachineAuthTestAdapter = { const createApiKeysEnv = (): EnvironmentConfig => appConfigs.envs.withAPIKeys.clone(); -const createMachineClient = () => +export const createMachineClient = () => createClerkClient({ secretKey: instanceKeys.get('with-api-keys').sk, }); diff --git a/integration/tests/next-machine.test.ts b/integration/tests/next-machine.test.ts index 80b9cbc878d..5b1d88b010d 100644 --- a/integration/tests/next-machine.test.ts +++ b/integration/tests/next-machine.test.ts @@ -5,8 +5,15 @@ import { expect, test } from '@playwright/test'; import type { Application } from '../models/application'; import { appConfigs } from '../presets'; import { createTestUtils } from '../testUtils'; -import type { MachineAuthTestAdapter } from '../testUtils/machineAuthHelpers'; -import { registerApiKeyAuthTests, registerM2MAuthTests, registerOAuthAuthTests } from '../testUtils/machineAuthHelpers'; +import type { FakeMachineNetwork, MachineAuthTestAdapter } from '../testUtils/machineAuthHelpers'; +import { + createFakeMachineNetwork, + createJwtM2MToken, + createMachineClient, + registerApiKeyAuthTests, + registerM2MAuthTests, + registerOAuthAuthTests, +} from '../testUtils/machineAuthHelpers'; import type { FakeAPIKey, FakeUser } from '../testUtils/usersService'; const adapter: MachineAuthTestAdapter = { @@ -189,4 +196,116 @@ test.describe('Next.js machine authentication @machine', () => { expect(await res.json()).toEqual({ userId: fakeBapiUser.id, tokenType: TokenType.ApiKey }); }); }); + + test.describe('session helpers with machine tokens', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + let network: FakeMachineNetwork; + let jwtToken: string; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + network = await createFakeMachineNetwork(createMachineClient()); + jwtToken = (await createJwtM2MToken(createMachineClient(), network.scopedSender.secretKey)).token; + + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/pages/api/pages-session.ts', + () => ` + import { getAuth } from '@clerk/nextjs/server'; + import type { NextApiRequest, NextApiResponse } from 'next'; + + export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { userId, tokenType } = getAuth(req); + + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + return res.status(200).json({ userId, tokenType }); + } + `, + ) + .addFile( + 'src/app/api/app-session/route.ts', + () => ` + import { auth } from '@clerk/nextjs/server'; + + export async function GET() { + const { userId, tokenType } = await auth(); + + if (!userId) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ userId, tokenType }); + } + `, + ) + .addFile( + 'src/app/api/app-machine/route.ts', + () => ` + import { auth } from '@clerk/nextjs/server'; + + export async function GET() { + const { subject, tokenType, isAuthenticated } = await auth({ acceptsToken: 'm2m_token' }); + + if (!isAuthenticated) { + return Response.json({ error: 'Unauthorized' }, { status: 401 }); + } + + return Response.json({ subject, tokenType }); + } + `, + ) + .commit(); + await app.setup(); + await app.withEnv( + appConfigs.envs.withAPIKeys + .clone() + .setEnvVariable('private', 'CLERK_MACHINE_SECRET_KEY', network.primaryServer.secretKey), + ); + await app.dev(); + }); + + test.afterAll(async () => { + await network?.cleanup(); + await app?.teardown(); + }); + + test('should return 401 from getAuth() if a JWT-format M2M token is sent', async ({ request }) => { + const res = await request.get(new URL('/api/pages-session', app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${jwtToken}` }, + }); + expect(res.status()).toBe(401); + }); + + test('should return 401 from getAuth() if an opaque M2M token is sent', async ({ request }) => { + const res = await request.get(new URL('/api/pages-session', app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, + }); + expect(res.status()).toBe(401); + }); + + test('should return 401 from auth() if a JWT-format M2M token is sent with a padded Authorization header', async ({ + request, + }) => { + const res = await request.get(new URL('/api/app-session', app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${jwtToken} x` }, + }); + expect(res.status()).toBe(401); + }); + + test('should return 200 from auth() if an accepted M2M token is sent with a padded Authorization header', async ({ + request, + }) => { + const res = await request.get(new URL('/api/app-machine', app.serverUrl).toString(), { + headers: { Authorization: `Bearer ${jwtToken} x` }, + }); + expect(res.status()).toBe(200); + expect(await res.json()).toEqual({ subject: network.scopedSender.id, tokenType: TokenType.M2MToken }); + }); + }); }); diff --git a/packages/nextjs/src/server/__tests__/createGetAuth.test.ts b/packages/nextjs/src/server/__tests__/createGetAuth.test.ts index 5574da4b6a1..98b51f645ca 100644 --- a/packages/nextjs/src/server/__tests__/createGetAuth.test.ts +++ b/packages/nextjs/src/server/__tests__/createGetAuth.test.ts @@ -85,4 +85,37 @@ describe('getAuth(req)', () => { expect(() => getAuth(req, { secretKey: mockSecretKey })).toThrowError(); }); + + const toBase64Url = (value: object) => + btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + const mockOAuthJwt = [ + toBase64Url({ alg: 'RS256', typ: 'at+jwt' }), + toBase64Url({ sub: 'user_123', scope: 'profile email' }), + 'c2lnbmF0dXJl', + ].join('.'); + const mockM2MJwt = [toBase64Url({ alg: 'RS256', typ: 'JWT' }), toBase64Url({ sub: 'mch_123' }), 'c2lnbmF0dXJl'].join( + '.', + ); + + it.each([ + ['a JWT-format OAuth token', mockOAuthJwt], + ['a JWT-format M2M token', mockM2MJwt], + ['an opaque OAuth token', 'oat_123'], + ['an opaque M2M token', 'mt_123'], + ['an API key', 'ak_123'], + ])('returns signed-out when the middleware verified %s', (_, token) => { + const req = new NextRequest('https://www.clerk.com', { + headers: new Headers({ + [constants.Headers.AuthStatus]: AuthStatus.SignedIn, + [constants.Headers.AuthToken]: token, + [constants.Headers.AuthSignature]: HmacSHA1(token, mockSecretKey).toString(), + }), + }); + + const auth = getAuth(req, { secretKey: mockSecretKey }); + + expect(auth.tokenType).toBe('session_token'); + expect(auth.userId).toBeNull(); + expect(auth.isAuthenticated).toBe(false); + }); }); diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 1a49c6b6c13..9bd798d1e32 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -1,9 +1,10 @@ import type { MachineAuthObject } from '@clerk/backend'; import type { AuthenticatedMachineObject, MachineTokenType, SignedOutAuthObject } from '@clerk/backend/internal'; -import { constants } from '@clerk/backend/internal'; +import { AuthStatus, constants } from '@clerk/backend/internal'; import { NextRequest } from 'next/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { HmacSHA1 } from '../../vendor/crypto-es'; import { getAuthDataFromRequest } from '../data/getAuthDataFromRequest'; import { encryptClerkRequestData } from '../utils'; @@ -306,7 +307,7 @@ describe('getAuthDataFromRequest', () => { const mockM2MJwt = [ toBase64Url(JSON.stringify({ alg: 'RS256', typ: 'JWT' })), toBase64Url(JSON.stringify({ sub: 'mch_testmachine123', exp: 9999999999, iat: 1666648250 })), - 'fakesignature', + 'c2lnbmF0dXJl', ].join('.'); it('returns signed-out when JWT-format M2M token is sent to a session-only endpoint', () => { @@ -352,5 +353,47 @@ describe('getAuthDataFromRequest', () => { expect(auth.isAuthenticated).toBe(false); }); + + const mockOAuthJwt = [ + toBase64Url(JSON.stringify({ alg: 'RS256', typ: 'at+jwt' })), + toBase64Url(JSON.stringify({ sub: 'user_123', scope: 'profile email', exp: 9999999999, iat: 1666648250 })), + 'c2lnbmF0dXJl', + ].join('.'); + + const mockVerifiedRequestWithPaddedHeader = (token: string, tokenType: MachineTokenType) => + mockRequest({ + url: '/api/protected', + headers: new Headers({ + [constants.Headers.Authorization]: `Bearer ${token} x`, + [constants.Headers.AuthStatus]: AuthStatus.SignedIn, + [constants.Headers.AuthToken]: token, + [constants.Headers.AuthSignature]: HmacSHA1(token, 'sk_test_xxxxxxxxxxxxxxxxxx').toString(), + }), + machineAuthObject: createMockMachineAuthObject({ tokenType, isAuthenticated: true }), + }); + + it.each([ + ['OAuth', mockOAuthJwt, 'oauth_token'], + ['M2M', mockM2MJwt, 'm2m_token'], + ] as const)( + 'returns signed-out when a verified JWT-format %s token is sent with a padded Authorization header', + (_, token, tokenType) => { + const auth = getAuthDataFromRequest(mockVerifiedRequestWithPaddedHeader(token, tokenType)); + + expect(auth.tokenType).toBe('session_token'); + expect((auth as SignedOutAuthObject).userId).toBeNull(); + expect(auth.isAuthenticated).toBe(false); + }, + ); + + it('returns the machine auth object when an accepted machine token is sent with a padded Authorization header', async () => { + const auth = getAuthDataFromRequest(mockVerifiedRequestWithPaddedHeader(mockOAuthJwt, 'oauth_token'), { + acceptsToken: 'oauth_token', + }); + + expect(auth.tokenType).toBe('oauth_token'); + expect(auth.isAuthenticated).toBe(true); + await expect(auth.getToken()).resolves.toBe(mockOAuthJwt); + }); }); }); From 076fd3451ac256b2c57b8f1f5647f097eca89fee Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 13 Oct 2026 10:25:33 -0700 Subject: [PATCH 2/5] fix(nextjs): reject machine tokens where a session is expected --- .changeset/shy-tokens-sign-out.md | 5 +++++ .../src/server/data/getAuthDataFromRequest.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/shy-tokens-sign-out.md diff --git a/.changeset/shy-tokens-sign-out.md b/.changeset/shy-tokens-sign-out.md new file mode 100644 index 00000000000..d5be2d52587 --- /dev/null +++ b/.changeset/shy-tokens-sign-out.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Fix `getAuth()` and `auth()` returning OAuth and M2M tokens as signed-in user sessions. Machine tokens are now rejected where a session is expected, so both helpers return a signed-out state for them. `getAuth()` also no longer throws when the request carries an opaque machine token such as an API key. diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index f62f6ab4798..6eae8f294d0 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -44,6 +44,16 @@ const getAuthHeaders = (req: RequestLike) => { }; }; +const getBearerToken = (req: RequestLike) => { + const [scheme, token] = getHeader(req, constants.Headers.Authorization)?.split(' ', 2) ?? []; + + if (!token) { + return scheme; + } + + return scheme === 'Bearer' ? token : undefined; +}; + /** * Creates auth options object with fallbacks from encrypted request data */ @@ -89,6 +99,10 @@ export const getSessionAuthDataFromRequest = ( } else { assertTokenSignature(authToken as string, options.secretKey, authSignature); + if (isMachineToken(authToken as string)) { + return signedOutAuthObject(options); + } + const jwt = decodeJwt(authToken as string); opts.logger?.debug('jwt', jwt.raw); @@ -114,7 +128,7 @@ export const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRe const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - const bearerToken = getHeader(req, constants.Headers.Authorization)?.replace('Bearer ', ''); + const bearerToken = getBearerToken(req); const acceptsToken = opts.acceptsToken || TokenType.SessionToken; const options = createAuthOptions(req, opts); From 89469ad4a82ca89378edf7190b9e77888b97acb4 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 13 Oct 2026 10:29:21 -0700 Subject: [PATCH 3/5] chore(nextjs): simplify machine token test fixtures --- .../src/server/__tests__/createGetAuth.test.ts | 15 +++++---------- .../__tests__/getAuthDataFromRequest.test.ts | 6 +++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/createGetAuth.test.ts b/packages/nextjs/src/server/__tests__/createGetAuth.test.ts index 98b51f645ca..e3b81d6795f 100644 --- a/packages/nextjs/src/server/__tests__/createGetAuth.test.ts +++ b/packages/nextjs/src/server/__tests__/createGetAuth.test.ts @@ -86,16 +86,11 @@ describe('getAuth(req)', () => { expect(() => getAuth(req, { secretKey: mockSecretKey })).toThrowError(); }); - const toBase64Url = (value: object) => - btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); - const mockOAuthJwt = [ - toBase64Url({ alg: 'RS256', typ: 'at+jwt' }), - toBase64Url({ sub: 'user_123', scope: 'profile email' }), - 'c2lnbmF0dXJl', - ].join('.'); - const mockM2MJwt = [toBase64Url({ alg: 'RS256', typ: 'JWT' }), toBase64Url({ sub: 'mch_123' }), 'c2lnbmF0dXJl'].join( - '.', - ); + const toBase64Url = (str: string) => btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + const mockJwt = (header: object, payload: object) => + [JSON.stringify(header), JSON.stringify(payload), 'signature'].map(toBase64Url).join('.'); + const mockOAuthJwt = mockJwt({ alg: 'RS256', typ: 'at+jwt' }, { sub: 'user_123' }); + const mockM2MJwt = mockJwt({ alg: 'RS256', typ: 'JWT' }, { sub: 'mch_123' }); it.each([ ['a JWT-format OAuth token', mockOAuthJwt], diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 9bd798d1e32..369c3830537 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -307,7 +307,7 @@ describe('getAuthDataFromRequest', () => { const mockM2MJwt = [ toBase64Url(JSON.stringify({ alg: 'RS256', typ: 'JWT' })), toBase64Url(JSON.stringify({ sub: 'mch_testmachine123', exp: 9999999999, iat: 1666648250 })), - 'c2lnbmF0dXJl', + toBase64Url('signature'), ].join('.'); it('returns signed-out when JWT-format M2M token is sent to a session-only endpoint', () => { @@ -356,8 +356,8 @@ describe('getAuthDataFromRequest', () => { const mockOAuthJwt = [ toBase64Url(JSON.stringify({ alg: 'RS256', typ: 'at+jwt' })), - toBase64Url(JSON.stringify({ sub: 'user_123', scope: 'profile email', exp: 9999999999, iat: 1666648250 })), - 'c2lnbmF0dXJl', + toBase64Url(JSON.stringify({ sub: 'user_123', exp: 9999999999, iat: 1666648250 })), + toBase64Url('signature'), ].join('.'); const mockVerifiedRequestWithPaddedHeader = (token: string, tokenType: MachineTokenType) => From 3c098a3448269135061181373f02366ff0838d17 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 13 Oct 2026 10:37:24 -0700 Subject: [PATCH 4/5] refactor(nextjs): drop Authorization header parsing change and trim tests The session guard alone rejects machine tokens on both paths, so the header parsing change is not needed. --- integration/tests/next-machine.test.ts | 21 +++------ .../server/__tests__/createGetAuth.test.ts | 14 +++--- .../__tests__/getAuthDataFromRequest.test.ts | 43 +++++-------------- .../src/server/data/getAuthDataFromRequest.ts | 12 +----- 4 files changed, 24 insertions(+), 66 deletions(-) diff --git a/integration/tests/next-machine.test.ts b/integration/tests/next-machine.test.ts index 5b1d88b010d..9f81b14c73e 100644 --- a/integration/tests/next-machine.test.ts +++ b/integration/tests/next-machine.test.ts @@ -275,16 +275,17 @@ test.describe('Next.js machine authentication @machine', () => { await app?.teardown(); }); - test('should return 401 from getAuth() if a JWT-format M2M token is sent', async ({ request }) => { - const res = await request.get(new URL('/api/pages-session', app.serverUrl).toString(), { + test('should return 200 from auth() if the route accepts M2M tokens', async ({ request }) => { + const res = await request.get(new URL('/api/app-machine', app.serverUrl).toString(), { headers: { Authorization: `Bearer ${jwtToken}` }, }); - expect(res.status()).toBe(401); + expect(res.status()).toBe(200); + expect(await res.json()).toEqual({ subject: network.scopedSender.id, tokenType: TokenType.M2MToken }); }); - test('should return 401 from getAuth() if an opaque M2M token is sent', async ({ request }) => { + test('should return 401 from getAuth() if a JWT-format M2M token is sent', async ({ request }) => { const res = await request.get(new URL('/api/pages-session', app.serverUrl).toString(), { - headers: { Authorization: `Bearer ${network.scopedSenderToken.token}` }, + headers: { Authorization: `Bearer ${jwtToken}` }, }); expect(res.status()).toBe(401); }); @@ -297,15 +298,5 @@ test.describe('Next.js machine authentication @machine', () => { }); expect(res.status()).toBe(401); }); - - test('should return 200 from auth() if an accepted M2M token is sent with a padded Authorization header', async ({ - request, - }) => { - const res = await request.get(new URL('/api/app-machine', app.serverUrl).toString(), { - headers: { Authorization: `Bearer ${jwtToken} x` }, - }); - expect(res.status()).toBe(200); - expect(await res.json()).toEqual({ subject: network.scopedSender.id, tokenType: TokenType.M2MToken }); - }); }); }); diff --git a/packages/nextjs/src/server/__tests__/createGetAuth.test.ts b/packages/nextjs/src/server/__tests__/createGetAuth.test.ts index e3b81d6795f..41d20308bbb 100644 --- a/packages/nextjs/src/server/__tests__/createGetAuth.test.ts +++ b/packages/nextjs/src/server/__tests__/createGetAuth.test.ts @@ -87,16 +87,16 @@ describe('getAuth(req)', () => { }); const toBase64Url = (str: string) => btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); - const mockJwt = (header: object, payload: object) => - [JSON.stringify(header), JSON.stringify(payload), 'signature'].map(toBase64Url).join('.'); - const mockOAuthJwt = mockJwt({ alg: 'RS256', typ: 'at+jwt' }, { sub: 'user_123' }); - const mockM2MJwt = mockJwt({ alg: 'RS256', typ: 'JWT' }, { sub: 'mch_123' }); + const mockOAuthJwt = [ + JSON.stringify({ alg: 'RS256', typ: 'at+jwt' }), + JSON.stringify({ sub: 'user_123' }), + 'signature', + ] + .map(toBase64Url) + .join('.'); it.each([ ['a JWT-format OAuth token', mockOAuthJwt], - ['a JWT-format M2M token', mockM2MJwt], - ['an opaque OAuth token', 'oat_123'], - ['an opaque M2M token', 'mt_123'], ['an API key', 'ak_123'], ])('returns signed-out when the middleware verified %s', (_, token) => { const req = new NextRequest('https://www.clerk.com', { diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 369c3830537..49f3c6c7876 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -354,46 +354,23 @@ describe('getAuthDataFromRequest', () => { expect(auth.isAuthenticated).toBe(false); }); - const mockOAuthJwt = [ - toBase64Url(JSON.stringify({ alg: 'RS256', typ: 'at+jwt' })), - toBase64Url(JSON.stringify({ sub: 'user_123', exp: 9999999999, iat: 1666648250 })), - toBase64Url('signature'), - ].join('.'); - - const mockVerifiedRequestWithPaddedHeader = (token: string, tokenType: MachineTokenType) => - mockRequest({ + it('returns signed-out when a verified JWT-format M2M token is sent with a padded Authorization header', () => { + const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: `Bearer ${token} x`, + [constants.Headers.Authorization]: `Bearer ${mockM2MJwt} x`, [constants.Headers.AuthStatus]: AuthStatus.SignedIn, - [constants.Headers.AuthToken]: token, - [constants.Headers.AuthSignature]: HmacSHA1(token, 'sk_test_xxxxxxxxxxxxxxxxxx').toString(), + [constants.Headers.AuthToken]: mockM2MJwt, + [constants.Headers.AuthSignature]: HmacSHA1(mockM2MJwt, 'sk_test_xxxxxxxxxxxxxxxxxx').toString(), }), - machineAuthObject: createMockMachineAuthObject({ tokenType, isAuthenticated: true }), + machineAuthObject: createMockMachineAuthObject({ tokenType: 'm2m_token', isAuthenticated: true }), }); - it.each([ - ['OAuth', mockOAuthJwt, 'oauth_token'], - ['M2M', mockM2MJwt, 'm2m_token'], - ] as const)( - 'returns signed-out when a verified JWT-format %s token is sent with a padded Authorization header', - (_, token, tokenType) => { - const auth = getAuthDataFromRequest(mockVerifiedRequestWithPaddedHeader(token, tokenType)); - - expect(auth.tokenType).toBe('session_token'); - expect((auth as SignedOutAuthObject).userId).toBeNull(); - expect(auth.isAuthenticated).toBe(false); - }, - ); - - it('returns the machine auth object when an accepted machine token is sent with a padded Authorization header', async () => { - const auth = getAuthDataFromRequest(mockVerifiedRequestWithPaddedHeader(mockOAuthJwt, 'oauth_token'), { - acceptsToken: 'oauth_token', - }); + const auth = getAuthDataFromRequest(req); - expect(auth.tokenType).toBe('oauth_token'); - expect(auth.isAuthenticated).toBe(true); - await expect(auth.getToken()).resolves.toBe(mockOAuthJwt); + expect(auth.tokenType).toBe('session_token'); + expect((auth as SignedOutAuthObject).userId).toBeNull(); + expect(auth.isAuthenticated).toBe(false); }); }); }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index 6eae8f294d0..348e7f277e2 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -44,16 +44,6 @@ const getAuthHeaders = (req: RequestLike) => { }; }; -const getBearerToken = (req: RequestLike) => { - const [scheme, token] = getHeader(req, constants.Headers.Authorization)?.split(' ', 2) ?? []; - - if (!token) { - return scheme; - } - - return scheme === 'Bearer' ? token : undefined; -}; - /** * Creates auth options object with fallbacks from encrypted request data */ @@ -128,7 +118,7 @@ export const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRe const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - const bearerToken = getBearerToken(req); + const bearerToken = getHeader(req, constants.Headers.Authorization)?.replace('Bearer ', ''); const acceptsToken = opts.acceptsToken || TokenType.SessionToken; const options = createAuthOptions(req, opts); From 1b5ab4c1ef281b4e86a049df61407458faaf623b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 13 Oct 2026 11:15:27 -0700 Subject: [PATCH 5/5] chore(nextjs): simplify changeset and point to acceptsToken --- .changeset/shy-tokens-sign-out.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/shy-tokens-sign-out.md b/.changeset/shy-tokens-sign-out.md index d5be2d52587..4a1958dad15 100644 --- a/.changeset/shy-tokens-sign-out.md +++ b/.changeset/shy-tokens-sign-out.md @@ -2,4 +2,4 @@ '@clerk/nextjs': patch --- -Fix `getAuth()` and `auth()` returning OAuth and M2M tokens as signed-in user sessions. Machine tokens are now rejected where a session is expected, so both helpers return a signed-out state for them. `getAuth()` also no longer throws when the request carries an opaque machine token such as an API key. +Fix `getAuth()` and `auth()` returning OAuth and M2M tokens as signed-in user sessions. They now return a signed-out state for machine tokens. To authenticate OAuth or M2M tokens on a route, use `auth({ acceptsToken })`.