From 73bf0e4564e889f116db03760c47bde0bfe7b296 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 21 Jul 2025 10:13:40 -0700 Subject: [PATCH 01/10] chore: Add verified machine auth object to request data header --- packages/nextjs/src/server/clerkMiddleware.ts | 9 ++++++++- .../nextjs/src/server/data/getAuthDataFromRequest.ts | 6 +++++- packages/nextjs/src/server/utils.ts | 10 ++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index da58ce560c6..297124a22d2 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -265,7 +265,14 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl } : {}; - decorateRequest(clerkRequest, handlerResult, requestState, resolvedParams, keylessKeysForRequestData); + decorateRequest( + clerkRequest, + handlerResult, + requestState, + resolvedParams, + keylessKeysForRequestData, + authObject.tokenType === 'session_token' ? null : authObject, + ); return handlerResult; }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index 34da3ab3ab5..200ef15cb8e 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -40,10 +40,12 @@ export const getAuthDataFromRequestSync = ( req: RequestLike, { treatPendingAsSignedOut = true, ...opts }: GetAuthDataFromRequestOptions = {}, ): SignedInAuthObject | SignedOutAuthObject => { - const { authStatus, authMessage, authReason, authToken, authSignature } = getAuthHeaders(req); + const { authStatus, authMessage, authReason, authRequestData, authToken, authSignature } = getAuthHeaders(req); opts.logger?.debug('headers', { authStatus, authMessage, authReason }); + opts.logger?.debug('authRequestData', { authRequestData }); + const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); @@ -159,6 +161,7 @@ const getAuthHeaders = (req: RequestLike) => { const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); const authReason = getAuthKeyFromRequest(req, 'AuthReason'); const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); + const authRequestData = getHeader(req, constants.Headers.ClerkRequestData); return { authStatus, @@ -166,5 +169,6 @@ const getAuthHeaders = (req: RequestLike) => { authMessage, authReason, authSignature, + authRequestData, }; }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 43bbe3e5ab4..4ec7f5db48d 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -1,3 +1,4 @@ +import type { AuthObject } from '@clerk/backend'; import type { AuthenticateRequestOptions, ClerkRequest, RequestState } from '@clerk/backend/internal'; import { constants } from '@clerk/backend/internal'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; @@ -53,6 +54,7 @@ export function decorateRequest( requestState: RequestState, requestData: AuthenticateRequestOptions, keylessMode: Pick, + authObject: AuthObject | null, ): Response { const { reason, message, status, token } = requestState; // pass-through case, convert to next() @@ -87,7 +89,7 @@ export function decorateRequest( } if (rewriteURL) { - const clerkRequestData = encryptClerkRequestData(requestData, keylessMode); + const clerkRequestData = encryptClerkRequestData(requestData, keylessMode, authObject); setRequestHeadersOnNextResponse(res, req, { [constants.Headers.AuthStatus]: status, @@ -184,6 +186,7 @@ const KEYLESS_ENCRYPTION_KEY = 'clerk_keyless_dummy_key'; export function encryptClerkRequestData( requestData: Partial, keylessModeKeys: Pick, + authObject: AuthObject | null, ) { const isEmpty = (obj: Record | undefined) => { if (!obj) { @@ -209,7 +212,10 @@ export function encryptClerkRequestData( ? ENCRYPTION_KEY || assertKey(SECRET_KEY, () => errorThrower.throwMissingSecretKeyError()) : ENCRYPTION_KEY || SECRET_KEY || KEYLESS_ENCRYPTION_KEY; - return AES.encrypt(JSON.stringify({ ...keylessModeKeys, ...requestData }), maybeKeylessEncryptionKey).toString(); + return AES.encrypt( + JSON.stringify({ ...keylessModeKeys, ...requestData, authObject }), + maybeKeylessEncryptionKey, + ).toString(); } /** From 321711bb4cdc789a32154e511bb27b9836256e7f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 21 Jul 2025 11:38:42 -0700 Subject: [PATCH 02/10] chore: use existing header --- .../src/server/data/getAuthDataFromRequest.ts | 26 +++++++------------ packages/nextjs/src/server/utils.ts | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index 200ef15cb8e..08078f88fdd 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -1,6 +1,5 @@ import type { AuthObject } from '@clerk/backend'; import { - authenticatedMachineObject, type AuthenticateRequestOptions, AuthStatus, constants, @@ -15,7 +14,6 @@ import { signedOutAuthObject, TokenType, unauthenticatedMachineObject, - verifyMachineAuthToken, } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import type { PendingSessionOptions } from '@clerk/types'; @@ -40,12 +38,10 @@ export const getAuthDataFromRequestSync = ( req: RequestLike, { treatPendingAsSignedOut = true, ...opts }: GetAuthDataFromRequestOptions = {}, ): SignedInAuthObject | SignedOutAuthObject => { - const { authStatus, authMessage, authReason, authRequestData, authToken, authSignature } = getAuthHeaders(req); + const { authStatus, authMessage, authReason, authToken, authSignature } = getAuthHeaders(req); opts.logger?.debug('headers', { authStatus, authMessage, authReason }); - opts.logger?.debug('authRequestData', { authRequestData }); - const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); @@ -82,11 +78,12 @@ export const getAuthDataFromRequestSync = ( return authObject; }; -const handleMachineToken = async ( +const handleMachineToken = ( bearerToken: string | undefined, + authObject: AuthObject | undefined, acceptsToken: NonNullable, options: GetAuthDataFromRequestOptions, -): Promise => { +): AuthObject | null => { const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken); const acceptsOnlySessionToken = @@ -106,10 +103,6 @@ const handleMachineToken = async ( return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); } - const { data, errors } = await verifyMachineAuthToken(bearerToken, options); - const authObject = errors - ? unauthenticatedMachineObject(machineTokenType, options) - : authenticatedMachineObject(machineTokenType, bearerToken, data); return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); } @@ -127,11 +120,14 @@ export const getAuthDataFromRequestAsync = async ( const { authStatus, authMessage, authReason } = getAuthHeaders(req); opts.logger?.debug('headers', { authStatus, authMessage, authReason }); + const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); + const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); + const bearerToken = getHeader(req, constants.Headers.Authorization)?.replace('Bearer ', ''); const acceptsToken = opts.acceptsToken || TokenType.SessionToken; const options = { - secretKey: opts?.secretKey || SECRET_KEY, - publishableKey: PUBLISHABLE_KEY, + secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, + publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY, apiUrl: API_URL, authStatus, authMessage, @@ -139,7 +135,7 @@ export const getAuthDataFromRequestAsync = async ( }; // If the request has a machine token in header, handle it first. - const machineAuthObject = await handleMachineToken(bearerToken, acceptsToken, options); + const machineAuthObject = handleMachineToken(bearerToken, decryptedRequestData.authObject, acceptsToken, options); if (machineAuthObject) { return machineAuthObject; } @@ -161,7 +157,6 @@ const getAuthHeaders = (req: RequestLike) => { const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); const authReason = getAuthKeyFromRequest(req, 'AuthReason'); const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); - const authRequestData = getHeader(req, constants.Headers.ClerkRequestData); return { authStatus, @@ -169,6 +164,5 @@ const getAuthHeaders = (req: RequestLike) => { authMessage, authReason, authSignature, - authRequestData, }; }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 4ec7f5db48d..704eb44f959 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -224,7 +224,7 @@ export function encryptClerkRequestData( */ export function decryptClerkRequestData( encryptedRequestData?: string | undefined | null, -): Partial { +): Partial & { authObject?: AuthObject } { if (!encryptedRequestData) { return {}; } From 57a687551d9f81616da944d76f8044ef5caf9505 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 22 Jul 2025 16:46:47 -0700 Subject: [PATCH 03/10] chore: Reconstruct machine auth object --- packages/nextjs/src/app-router/server/auth.ts | 6 ++- .../__tests__/getAuthDataFromRequest.test.ts | 2 +- packages/nextjs/src/server/clerkMiddleware.ts | 3 +- .../src/server/data/getAuthDataFromRequest.ts | 47 ++++++++++--------- packages/nextjs/src/server/utils.ts | 4 +- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index a23fdb96e97..71278fd514d 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -179,7 +179,11 @@ export const auth: AuthFn = (async (options?: AuthOptions) => { }); }; - return Object.assign(authObject, { redirectToSignIn, redirectToSignUp }); + if (authObject.tokenType === TokenType.SessionToken) { + return Object.assign(authObject, { redirectToSignIn, redirectToSignUp }); + } + + return authObject; }) as AuthFn; auth.protect = async (...args: any[]) => { diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 82135d519d3..e630b2f520d 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -44,7 +44,7 @@ describe('getAuthDataFromRequestAsync', () => { vi.clearAllMocks(); }); - it('returns invalid token auth object when token type does not match any in acceptsToken array', async () => { + it.only('returns invalid token auth object when token type does not match any in acceptsToken array', async () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 297124a22d2..ff5672c95bd 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -15,6 +15,7 @@ import { getAuthObjectForAcceptedToken, isMachineTokenByPrefix, isTokenTypeAccepted, + makeAuthObjectSerializable, TokenType, } from '@clerk/backend/internal'; import { parsePublishableKey } from '@clerk/shared/keys'; @@ -271,7 +272,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl requestState, resolvedParams, keylessKeysForRequestData, - authObject.tokenType === 'session_token' ? null : authObject, + authObject.tokenType === 'session_token' ? null : makeAuthObjectSerializable(authObject), ); return handlerResult; diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index 08078f88fdd..cb19412be36 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -1,22 +1,24 @@ -import type { AuthObject } from '@clerk/backend'; +import type { AuthObject, MachineAuthObject } from '@clerk/backend'; +import type { + AuthenticateRequestOptions, + MachineTokenType, + SignedInAuthObject, + SignedOutAuthObject, +} from '@clerk/backend/internal'; import { - type AuthenticateRequestOptions, AuthStatus, constants, getAuthObjectForAcceptedToken, getAuthObjectFromJwt, - getMachineTokenType, invalidTokenAuthObject, isMachineTokenByPrefix, isTokenTypeAccepted, - type SignedInAuthObject, - type SignedOutAuthObject, signedOutAuthObject, TokenType, - unauthenticatedMachineObject, } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import type { PendingSessionOptions } from '@clerk/types'; +import type { AuthenticateContext } from 'node_modules/@clerk/backend/dist/tokens/authenticateContext'; import type { LoggerNoCommit } from '../../utils/debugLogger'; import { API_URL, API_VERSION, PUBLISHABLE_KEY, SECRET_KEY } from '../constants'; @@ -80,30 +82,29 @@ export const getAuthDataFromRequestSync = ( const handleMachineToken = ( bearerToken: string | undefined, - authObject: AuthObject | undefined, + rawAuthObject: AuthObject | undefined, acceptsToken: NonNullable, - options: GetAuthDataFromRequestOptions, -): AuthObject | null => { + options: Partial, +): MachineAuthObject | null => { const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken); const acceptsOnlySessionToken = acceptsToken === TokenType.SessionToken || (Array.isArray(acceptsToken) && acceptsToken.length === 1 && acceptsToken[0] === TokenType.SessionToken); - if (hasMachineToken && !acceptsOnlySessionToken) { - const machineTokenType = getMachineTokenType(bearerToken); - - // Early return if the token type is not accepted to save on the verify call - if (Array.isArray(acceptsToken) && !acceptsToken.includes(machineTokenType)) { - return invalidTokenAuthObject(); - } - // Early return for scalar acceptsToken if it does not match the machine token type - if (!Array.isArray(acceptsToken) && acceptsToken !== 'any' && machineTokenType !== acceptsToken) { - const authObject = unauthenticatedMachineObject(acceptsToken, options); - return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); - } - - return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); + if (hasMachineToken && rawAuthObject && !acceptsOnlySessionToken) { + const authObject = getAuthObjectForAcceptedToken({ + authObject: { + ...rawAuthObject, + debug: () => options, + }, + acceptsToken, + }); + return { + ...authObject, + getToken: () => (authObject.isAuthenticated ? Promise.resolve(bearerToken) : Promise.resolve(null)), + has: () => false, + } as MachineAuthObject; } return null; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 704eb44f959..90ff680e710 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -195,7 +195,7 @@ export function encryptClerkRequestData( return !Object.values(obj).some(v => v !== undefined); }; - if (isEmpty(requestData) && isEmpty(keylessModeKeys)) { + if (isEmpty(requestData) && isEmpty(keylessModeKeys) && !authObject) { return; } @@ -213,7 +213,7 @@ export function encryptClerkRequestData( : ENCRYPTION_KEY || SECRET_KEY || KEYLESS_ENCRYPTION_KEY; return AES.encrypt( - JSON.stringify({ ...keylessModeKeys, ...requestData, authObject }), + JSON.stringify({ ...keylessModeKeys, ...requestData, authObject: authObject ?? undefined }), maybeKeylessEncryptionKey, ).toString(); } From ee53247efda8fb50200ae738a5e32cd47673e09c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 22 Jul 2025 17:10:56 -0700 Subject: [PATCH 04/10] chore: clean up --- .../__tests__/getAuthDataFromRequest.test.ts | 36 ++++---- packages/nextjs/src/server/buildClerkProps.ts | 4 +- packages/nextjs/src/server/createGetAuth.ts | 16 ++-- .../src/server/data/getAuthDataFromRequest.ts | 82 ++++++++++--------- 4 files changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index e630b2f520d..bdc27804aed 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -3,7 +3,7 @@ import { constants, verifyMachineAuthToken } from '@clerk/backend/internal'; import { NextRequest } from 'next/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getAuthDataFromRequestAsync, getAuthDataFromRequestSync } from '../data/getAuthDataFromRequest'; +import { getAuthDataFromRequest } from '../data/getAuthDataFromRequest'; vi.mock('@clerk/backend/internal', async () => { const actual = await vi.importActual('@clerk/backend/internal'); @@ -39,7 +39,7 @@ const machineTokenErrorMock = [ }, ]; -describe('getAuthDataFromRequestAsync', () => { +describe('getAuthDataFromRequest', () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -52,7 +52,7 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { + const auth = await getAuthDataFromRequest(req, { acceptsToken: ['machine_token', 'oauth_token', 'session_token'], }); @@ -60,7 +60,7 @@ describe('getAuthDataFromRequestAsync', () => { expect(auth.isAuthenticated).toBe(false); }); - it('returns unauthenticated auth object when token type does not match single acceptsToken', async () => { + it('returns unauthenticated auth object when token type does not match single acceptsToken', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ @@ -68,13 +68,13 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: 'oauth_token' }); + const auth = getAuthDataFromRequest(req, { acceptsToken: 'oauth_token' }); expect(auth.tokenType).toBe('oauth_token'); expect(auth.isAuthenticated).toBe(false); }); - it('returns authenticated auth object for any valid token type', async () => { + it('returns authenticated auth object for any valid token type', () => { vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ data: { id: 'ak_id123', subject: 'user_12345' } as any, tokenType: 'api_key', @@ -88,14 +88,14 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: 'any' }); + const auth = getAuthDataFromRequest(req, { acceptsToken: 'any' }); expect(auth.tokenType).toBe('api_key'); expect((auth as AuthenticatedMachineObject<'api_key'>).id).toBe('ak_id123'); expect(auth.isAuthenticated).toBe(true); }); - it('returns authenticated object when token type exists in acceptsToken array', async () => { + it('returns authenticated object when token type exists in acceptsToken array', () => { vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ data: { id: 'ak_id123', subject: 'user_12345' } as any, tokenType: 'api_key', @@ -109,7 +109,7 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { + const auth = getAuthDataFromRequest(req, { acceptsToken: ['api_key', 'machine_token'], }); @@ -136,7 +136,7 @@ describe('getAuthDataFromRequestAsync', () => { }, ])( 'returns authenticated $tokenType object when token is valid and acceptsToken is $tokenType', - async ({ tokenType, token, data }) => { + ({ tokenType, token, data }) => { vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ data: data as any, tokenType, @@ -150,7 +150,7 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType }); + const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); expect(auth.tokenType).toBe(tokenType); expect(auth.isAuthenticated).toBe(true); @@ -173,7 +173,7 @@ describe('getAuthDataFromRequestAsync', () => { token: 'mt_123', data: undefined, }, - ])('returns unauthenticated $tokenType object when token is invalid', async ({ tokenType, token, data }) => { + ])('returns unauthenticated $tokenType object when token is invalid', ({ tokenType, token, data }) => { vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ data: data as any, tokenType, @@ -187,13 +187,13 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType }); + const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); expect(auth.tokenType).toBe(tokenType); expect(auth.isAuthenticated).toBe(false); }); - it('falls back to session token handling', async () => { + it('falls back to session token handling', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ @@ -201,14 +201,12 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req); + const auth = getAuthDataFromRequest(req); expect(auth.tokenType).toBe('session_token'); expect((auth as SignedOutAuthObject).userId).toBeNull(); expect(auth.isAuthenticated).toBe(false); }); -}); -describe('getAuthDataFromRequestSync', () => { it('only accepts session tokens', () => { const req = mockRequest({ url: '/api/protected', @@ -217,12 +215,12 @@ describe('getAuthDataFromRequestSync', () => { }), }); - const auth = getAuthDataFromRequestSync(req, { + const auth = getAuthDataFromRequest(req, { acceptsToken: 'api_key', }); expect(auth.tokenType).toBe('session_token'); - expect(auth.userId).toBeNull(); + expect((auth as SignedOutAuthObject).userId).toBeNull(); expect(auth.isAuthenticated).toBe(false); }); }); diff --git a/packages/nextjs/src/server/buildClerkProps.ts b/packages/nextjs/src/server/buildClerkProps.ts index a8da66f78e6..cfbd4686d23 100644 --- a/packages/nextjs/src/server/buildClerkProps.ts +++ b/packages/nextjs/src/server/buildClerkProps.ts @@ -1,7 +1,7 @@ import type { AuthObject, Organization, Session, User } from '@clerk/backend'; import { makeAuthObjectSerializable, stripPrivateDataFromObject } from '@clerk/backend/internal'; -import { getAuthDataFromRequestSync } from './data/getAuthDataFromRequest'; +import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; import type { RequestLike } from './types'; type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; organization?: Organization | null }; @@ -59,7 +59,7 @@ export const buildClerkProps: BuildClerkProps = (req, initialState = {}) => { }; export function getDynamicAuthData(req: RequestLike, initialState = {}) { - const authObject = getAuthDataFromRequestSync(req); + const authObject = getAuthDataFromRequest(req); return makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initialState })) as AuthObject; } diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index fe605829f80..ee0dc4701c4 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -7,8 +7,8 @@ import { withLogger } from '../utils/debugLogger'; import { isNextWithUnstableServerActions } from '../utils/sdk-versions'; import type { GetAuthDataFromRequestOptions } from './data/getAuthDataFromRequest'; import { - getAuthDataFromRequestAsync as getAuthDataFromRequestAsyncOriginal, - getAuthDataFromRequestSync as getAuthDataFromRequestSyncOriginal, + getAuthDataFromRequest as getAuthDataFromRequestOriginal, + getSessionAuthDataFromRequest as getSessionAuthDataFromRequestOriginal, } from './data/getAuthDataFromRequest'; import { getAuthAuthHeaderMissing } from './errors'; import { detectClerkMiddleware, getHeader } from './headers-utils'; @@ -54,11 +54,11 @@ export const createAsyncGetAuth = ({ assertAuthStatus(req, noAuthStatusMessage); } - const getAuthDataFromRequestAsync = (req: RequestLike, opts: GetAuthDataFromRequestOptions = {}) => { - return getAuthDataFromRequestAsyncOriginal(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); + const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRequestOptions = {}) => { + return getAuthDataFromRequestOriginal(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); }; - return getAuthDataFromRequestAsync(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); + return getAuthDataFromRequest(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); }; }); @@ -85,11 +85,11 @@ export const createSyncGetAuth = ({ assertAuthStatus(req, noAuthStatusMessage); - const getAuthDataFromRequestSync = (req: RequestLike, opts: GetAuthDataFromRequestOptions = {}) => { - return getAuthDataFromRequestSyncOriginal(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); + const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRequestOptions = {}) => { + return getSessionAuthDataFromRequestOriginal(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); }; - return getAuthDataFromRequestSync(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); + return getAuthDataFromRequest(req, { ...opts, logger, acceptsToken: opts?.acceptsToken }); }; }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index cb19412be36..a221117e070 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -36,7 +36,7 @@ export type GetAuthDataFromRequestOptions = { * Given a request object, builds an auth object from the request data. Used in server-side environments to get access * to auth data for a given request. */ -export const getAuthDataFromRequestSync = ( +export const getSessionAuthDataFromRequest = ( req: RequestLike, { treatPendingAsSignedOut = true, ...opts }: GetAuthDataFromRequestOptions = {}, ): SignedInAuthObject | SignedOutAuthObject => { @@ -58,8 +58,8 @@ export const getAuthDataFromRequestSync = ( treatPendingAsSignedOut, }; - // Only accept session tokens in the synchronous version. - // Machine tokens are not supported in this function. Any machine token input will result in a signed-out state. + // Only accept session tokens in this function. + // Machine tokens are not supported and will result in a signed-out state. if (!isTokenTypeAccepted(TokenType.SessionToken, opts.acceptsToken || TokenType.SessionToken)) { return signedOutAuthObject(options); } @@ -80,44 +80,15 @@ export const getAuthDataFromRequestSync = ( return authObject; }; -const handleMachineToken = ( - bearerToken: string | undefined, - rawAuthObject: AuthObject | undefined, - acceptsToken: NonNullable, - options: Partial, -): MachineAuthObject | null => { - const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken); - - const acceptsOnlySessionToken = - acceptsToken === TokenType.SessionToken || - (Array.isArray(acceptsToken) && acceptsToken.length === 1 && acceptsToken[0] === TokenType.SessionToken); - - if (hasMachineToken && rawAuthObject && !acceptsOnlySessionToken) { - const authObject = getAuthObjectForAcceptedToken({ - authObject: { - ...rawAuthObject, - debug: () => options, - }, - acceptsToken, - }); - return { - ...authObject, - getToken: () => (authObject.isAuthenticated ? Promise.resolve(bearerToken) : Promise.resolve(null)), - has: () => false, - } as MachineAuthObject; - } - - return null; -}; - /** * Given a request object, builds an auth object from the request data. Used in server-side environments to get access * to auth data for a given request. + * + * This function handles both session tokens and machine tokens: + * - Session tokens: Decoded from JWT and validated + * - Machine tokens: Retrieved from encrypted request data (x-clerk-request-data header) */ -export const getAuthDataFromRequestAsync = async ( - req: RequestLike, - opts: GetAuthDataFromRequestOptions = {}, -): Promise => { +export const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRequestOptions = {}): AuthObject => { const { authStatus, authMessage, authReason } = getAuthHeaders(req); opts.logger?.debug('headers', { authStatus, authMessage, authReason }); @@ -126,6 +97,7 @@ export const getAuthDataFromRequestAsync = async ( const bearerToken = getHeader(req, constants.Headers.Authorization)?.replace('Bearer ', ''); const acceptsToken = opts.acceptsToken || TokenType.SessionToken; + const options = { secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY, @@ -135,7 +107,8 @@ export const getAuthDataFromRequestAsync = async ( authReason, }; - // If the request has a machine token in header, handle it first. + // Handle machine tokens first (from encrypted request data) + // Machine tokens are passed via x-clerk-request-data header from middleware const machineAuthObject = handleMachineToken(bearerToken, decryptedRequestData.authObject, acceptsToken, options); if (machineAuthObject) { return machineAuthObject; @@ -148,8 +121,37 @@ export const getAuthDataFromRequestAsync = async ( } // Fallback to session logic for all other cases - const authObject = getAuthDataFromRequestSync(req, opts); - return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); + return getSessionAuthDataFromRequest(req, opts); +}; + +const handleMachineToken = ( + bearerToken: string | undefined, + rawAuthObject: AuthObject | undefined, + acceptsToken: NonNullable, + options: Partial, +): MachineAuthObject | null => { + const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken); + + const acceptsOnlySessionToken = + acceptsToken === TokenType.SessionToken || + (Array.isArray(acceptsToken) && acceptsToken.length === 1 && acceptsToken[0] === TokenType.SessionToken); + + if (hasMachineToken && rawAuthObject && !acceptsOnlySessionToken) { + const authObject = getAuthObjectForAcceptedToken({ + authObject: { + ...rawAuthObject, + debug: () => options, + }, + acceptsToken, + }); + return { + ...authObject, + getToken: () => (authObject.isAuthenticated ? Promise.resolve(bearerToken) : Promise.resolve(null)), + has: () => false, + } as MachineAuthObject; + } + + return null; }; const getAuthHeaders = (req: RequestLike) => { From 176182b0ff2fe6dad2a9bc463acd2955ac65f8fe Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 23 Jul 2025 11:45:33 -0700 Subject: [PATCH 05/10] chore: update tests --- .../__tests__/getAuthDataFromRequest.test.ts | 140 +++++++++++++----- 1 file changed, 107 insertions(+), 33 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index bdc27804aed..9410b9e523e 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -1,15 +1,19 @@ -import type { AuthenticatedMachineObject, SignedOutAuthObject } from '@clerk/backend/internal'; -import { constants, verifyMachineAuthToken } from '@clerk/backend/internal'; +import type { MachineAuthObject } from '@clerk/backend'; +import type { AuthenticatedMachineObject, MachineTokenType, SignedOutAuthObject } from '@clerk/backend/internal'; +import { constants } from '@clerk/backend/internal'; import { NextRequest } from 'next/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getAuthDataFromRequest } from '../data/getAuthDataFromRequest'; +import { encryptClerkRequestData } from '../utils'; -vi.mock('@clerk/backend/internal', async () => { - const actual = await vi.importActual('@clerk/backend/internal'); +vi.mock(import('../constants.js'), async importOriginal => { + const actual = await importOriginal(); return { ...actual, - verifyMachineAuthToken: vi.fn(), + ENCRYPTION_KEY: 'encryption-key', + PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx', }; }); @@ -18,41 +22,55 @@ type MockRequestParams = { appendDevBrowserCookie?: boolean; method?: string; headers?: any; + machineAuthObject?: any; // Allow any auth object type for testing }; const mockRequest = (params: MockRequestParams) => { - const { url, appendDevBrowserCookie = false, method = 'GET', headers = new Headers() } = params; + const { url, appendDevBrowserCookie = false, method = 'GET', headers = new Headers(), machineAuthObject } = params; const headersWithCookie = new Headers(headers); + if (appendDevBrowserCookie) { headersWithCookie.append('cookie', '__clerk_db_jwt=test_jwt'); } + + // Add encrypted auth object header if provided + if (machineAuthObject) { + const encryptedData = encryptClerkRequestData( + {}, // requestData + {}, // keylessModeKeys + machineAuthObject, // authObject + ); + if (encryptedData) { + headersWithCookie.set(constants.Headers.ClerkRequestData, encryptedData); + } + } + return new NextRequest(new URL(url, 'https://www.clerk.com').toString(), { method, headers: headersWithCookie }); }; -const machineTokenErrorMock = [ - { - message: 'Token type mismatch', - code: 'token-invalid', - status: 401, - name: 'MachineTokenVerificationError', - getFullMessage: () => 'Token type mismatch', - }, -]; +// Helper function to create mock machine auth objects +const createMockMachineAuthObject = (data: Partial>) => data; describe('getAuthDataFromRequest', () => { beforeEach(() => { vi.clearAllMocks(); }); - it.only('returns invalid token auth object when token type does not match any in acceptsToken array', async () => { + it('returns invalid token auth object when token type does not match any in acceptsToken array', () => { + const mockAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + }); + const req = mockRequest({ url: '/api/protected', headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject: mockAuthObject, }); - const auth = await getAuthDataFromRequest(req, { + const auth = getAuthDataFromRequest(req, { acceptsToken: ['machine_token', 'oauth_token', 'session_token'], }); @@ -60,12 +78,63 @@ describe('getAuthDataFromRequest', () => { expect(auth.isAuthenticated).toBe(false); }); + it('handles mixed token types in acceptsToken array', () => { + const mockAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + id: 'ak_id123', + }); + + const req = mockRequest({ + url: '/api/protected', + headers: new Headers({ + [constants.Headers.Authorization]: 'Bearer ak_xxx', + }), + machineAuthObject: mockAuthObject, + }); + + const auth = getAuthDataFromRequest(req, { + acceptsToken: ['api_key', 'session_token'], + }); + + expect(auth.tokenType).toBe('api_key'); + expect(auth.isAuthenticated).toBe(true); + }); + + it('falls back to session logic when machine token is not accepted', () => { + const mockAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + }); + + const req = mockRequest({ + url: '/api/protected', + headers: new Headers({ + [constants.Headers.Authorization]: 'Bearer ak_xxx', + }), + machineAuthObject: mockAuthObject, + }); + + const auth = getAuthDataFromRequest(req, { + acceptsToken: 'session_token', + }); + + expect(auth.tokenType).toBe('session_token'); + expect(auth.isAuthenticated).toBe(false); + }); + it('returns unauthenticated auth object when token type does not match single acceptsToken', () => { + const mockAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + }); + const req = mockRequest({ url: '/api/protected', headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject: mockAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: 'oauth_token' }); @@ -75,10 +144,10 @@ describe('getAuthDataFromRequest', () => { }); it('returns authenticated auth object for any valid token type', () => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: { id: 'ak_id123', subject: 'user_12345' } as any, + const mockAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', - errors: undefined, + id: 'ak_id123', + isAuthenticated: true, }); const req = mockRequest({ @@ -86,6 +155,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject: mockAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: 'any' }); @@ -96,17 +166,19 @@ describe('getAuthDataFromRequest', () => { }); it('returns authenticated object when token type exists in acceptsToken array', () => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: { id: 'ak_id123', subject: 'user_12345' } as any, + const mockAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', - errors: undefined, + id: 'ak_id123', + subject: 'user_12345', + isAuthenticated: true, }); const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer ak_secret123', + [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject: mockAuthObject, }); const auth = getAuthDataFromRequest(req, { @@ -137,10 +209,10 @@ describe('getAuthDataFromRequest', () => { ])( 'returns authenticated $tokenType object when token is valid and acceptsToken is $tokenType', ({ tokenType, token, data }) => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: data as any, + const mockAuthObject = createMockMachineAuthObject({ tokenType, - errors: undefined, + isAuthenticated: true, + ...data, }); const req = mockRequest({ @@ -148,6 +220,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: `Bearer ${token}`, }), + machineAuthObject: mockAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); @@ -173,18 +246,19 @@ describe('getAuthDataFromRequest', () => { token: 'mt_123', data: undefined, }, - ])('returns unauthenticated $tokenType object when token is invalid', ({ tokenType, token, data }) => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: data as any, + ])('returns unauthenticated $tokenType object when token is invalid', ({ tokenType, token }) => { + const mockAuthObject = createMockMachineAuthObject({ tokenType, - errors: machineTokenErrorMock as any, + isAuthenticated: false, }); + // For invalid tokens, we don't include encrypted auth object const req = mockRequest({ url: '/api/protected', headers: new Headers({ [constants.Headers.Authorization]: `Bearer ${token}`, }), + machineAuthObject: mockAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); @@ -193,7 +267,7 @@ describe('getAuthDataFromRequest', () => { expect(auth.isAuthenticated).toBe(false); }); - it('falls back to session token handling', () => { + it('falls back to session token handling when no encrypted auth object is present', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ @@ -207,7 +281,7 @@ describe('getAuthDataFromRequest', () => { expect(auth.isAuthenticated).toBe(false); }); - it('only accepts session tokens', () => { + it('only accepts session tokens when encrypted auth object is not present', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ From 56a7c86d83c39ca3b04ab573be2a4c1ed31a9c9f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 23 Jul 2025 12:03:09 -0700 Subject: [PATCH 06/10] chore: fix types in tests --- .../src/server/__tests__/getAuthDataFromRequest.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 9410b9e523e..dd099e619dd 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -21,8 +21,8 @@ type MockRequestParams = { url: string; appendDevBrowserCookie?: boolean; method?: string; - headers?: any; - machineAuthObject?: any; // Allow any auth object type for testing + headers?: Headers; + machineAuthObject?: Partial>; }; const mockRequest = (params: MockRequestParams) => { @@ -38,7 +38,8 @@ const mockRequest = (params: MockRequestParams) => { const encryptedData = encryptClerkRequestData( {}, // requestData {}, // keylessModeKeys - machineAuthObject, // authObject + // @ts-expect-error - mock machine auth object + machineAuthObject, ); if (encryptedData) { headersWithCookie.set(constants.Headers.ClerkRequestData, encryptedData); From bbdc5caa8fdb48c9b2569d3ad84d60face343f96 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 23 Jul 2025 12:08:53 -0700 Subject: [PATCH 07/10] chore: fix types in tests --- .../nextjs/src/server/data/getAuthDataFromRequest.ts | 7 ++++++- packages/nextjs/src/server/utils.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index a221117e070..01c1f40cf32 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -109,7 +109,12 @@ export const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRe // Handle machine tokens first (from encrypted request data) // Machine tokens are passed via x-clerk-request-data header from middleware - const machineAuthObject = handleMachineToken(bearerToken, decryptedRequestData.authObject, acceptsToken, options); + const machineAuthObject = handleMachineToken( + bearerToken, + decryptedRequestData.machineAuthObject, + acceptsToken, + options, + ); if (machineAuthObject) { return machineAuthObject; } diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 90ff680e710..155788b0164 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -54,7 +54,7 @@ export function decorateRequest( requestState: RequestState, requestData: AuthenticateRequestOptions, keylessMode: Pick, - authObject: AuthObject | null, + machineAuthObject: AuthObject | null, ): Response { const { reason, message, status, token } = requestState; // pass-through case, convert to next() @@ -89,7 +89,7 @@ export function decorateRequest( } if (rewriteURL) { - const clerkRequestData = encryptClerkRequestData(requestData, keylessMode, authObject); + const clerkRequestData = encryptClerkRequestData(requestData, keylessMode, machineAuthObject); setRequestHeadersOnNextResponse(res, req, { [constants.Headers.AuthStatus]: status, @@ -186,7 +186,7 @@ const KEYLESS_ENCRYPTION_KEY = 'clerk_keyless_dummy_key'; export function encryptClerkRequestData( requestData: Partial, keylessModeKeys: Pick, - authObject: AuthObject | null, + machineAuthObject: AuthObject | null, ) { const isEmpty = (obj: Record | undefined) => { if (!obj) { @@ -195,7 +195,7 @@ export function encryptClerkRequestData( return !Object.values(obj).some(v => v !== undefined); }; - if (isEmpty(requestData) && isEmpty(keylessModeKeys) && !authObject) { + if (isEmpty(requestData) && isEmpty(keylessModeKeys) && !machineAuthObject) { return; } @@ -213,7 +213,7 @@ export function encryptClerkRequestData( : ENCRYPTION_KEY || SECRET_KEY || KEYLESS_ENCRYPTION_KEY; return AES.encrypt( - JSON.stringify({ ...keylessModeKeys, ...requestData, authObject: authObject ?? undefined }), + JSON.stringify({ ...keylessModeKeys, ...requestData, machineAuthObject: machineAuthObject ?? undefined }), maybeKeylessEncryptionKey, ).toString(); } @@ -224,7 +224,7 @@ export function encryptClerkRequestData( */ export function decryptClerkRequestData( encryptedRequestData?: string | undefined | null, -): Partial & { authObject?: AuthObject } { +): Partial & { machineAuthObject?: AuthObject } { if (!encryptedRequestData) { return {}; } From 83e00ed91c9f27c1b03cf75af6386b8a4878807f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 23 Jul 2025 12:13:08 -0700 Subject: [PATCH 08/10] chore: remove duplications --- .../__tests__/getAuthDataFromRequest.test.ts | 32 ++++----- .../src/server/data/getAuthDataFromRequest.ts | 71 +++++++++---------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index dd099e619dd..91c5144086e 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -58,7 +58,7 @@ describe('getAuthDataFromRequest', () => { }); it('returns invalid token auth object when token type does not match any in acceptsToken array', () => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', isAuthenticated: true, }); @@ -68,7 +68,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { @@ -80,7 +80,7 @@ describe('getAuthDataFromRequest', () => { }); it('handles mixed token types in acceptsToken array', () => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', isAuthenticated: true, id: 'ak_id123', @@ -91,7 +91,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { @@ -103,7 +103,7 @@ describe('getAuthDataFromRequest', () => { }); it('falls back to session logic when machine token is not accepted', () => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', isAuthenticated: true, }); @@ -113,7 +113,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { @@ -125,7 +125,7 @@ describe('getAuthDataFromRequest', () => { }); it('returns unauthenticated auth object when token type does not match single acceptsToken', () => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', isAuthenticated: true, }); @@ -135,7 +135,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: 'oauth_token' }); @@ -145,7 +145,7 @@ describe('getAuthDataFromRequest', () => { }); it('returns authenticated auth object for any valid token type', () => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', id: 'ak_id123', isAuthenticated: true, @@ -156,7 +156,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: 'any' }); @@ -167,7 +167,7 @@ describe('getAuthDataFromRequest', () => { }); it('returns authenticated object when token type exists in acceptsToken array', () => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', id: 'ak_id123', subject: 'user_12345', @@ -179,7 +179,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { @@ -210,7 +210,7 @@ describe('getAuthDataFromRequest', () => { ])( 'returns authenticated $tokenType object when token is valid and acceptsToken is $tokenType', ({ tokenType, token, data }) => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType, isAuthenticated: true, ...data, @@ -221,7 +221,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: `Bearer ${token}`, }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); @@ -248,7 +248,7 @@ describe('getAuthDataFromRequest', () => { data: undefined, }, ])('returns unauthenticated $tokenType object when token is invalid', ({ tokenType, token }) => { - const mockAuthObject = createMockMachineAuthObject({ + const machineAuthObject = createMockMachineAuthObject({ tokenType, isAuthenticated: false, }); @@ -259,7 +259,7 @@ describe('getAuthDataFromRequest', () => { headers: new Headers({ [constants.Headers.Authorization]: `Bearer ${token}`, }), - machineAuthObject: mockAuthObject, + machineAuthObject, }); const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index 01c1f40cf32..70430419ae1 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -33,30 +33,50 @@ export type GetAuthDataFromRequestOptions = { } & PendingSessionOptions; /** - * Given a request object, builds an auth object from the request data. Used in server-side environments to get access - * to auth data for a given request. + * Extracts auth headers from the request */ -export const getSessionAuthDataFromRequest = ( - req: RequestLike, - { treatPendingAsSignedOut = true, ...opts }: GetAuthDataFromRequestOptions = {}, -): SignedInAuthObject | SignedOutAuthObject => { - const { authStatus, authMessage, authReason, authToken, authSignature } = getAuthHeaders(req); - - opts.logger?.debug('headers', { authStatus, authMessage, authReason }); +const getAuthHeaders = (req: RequestLike) => { + return { + authStatus: getAuthKeyFromRequest(req, 'AuthStatus'), + authToken: getAuthKeyFromRequest(req, 'AuthToken'), + authMessage: getAuthKeyFromRequest(req, 'AuthMessage'), + authReason: getAuthKeyFromRequest(req, 'AuthReason'), + authSignature: getAuthKeyFromRequest(req, 'AuthSignature'), + }; +}; +/** + * Creates auth options object with fallbacks from encrypted request data + */ +const createAuthOptions = (req: RequestLike, opts: GetAuthDataFromRequestOptions, treatPendingAsSignedOut = true) => { const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - const options = { + return { secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY, apiUrl: API_URL, apiVersion: API_VERSION, - authStatus, - authMessage, - authReason, + authStatus: getAuthKeyFromRequest(req, 'AuthStatus'), + authMessage: getAuthKeyFromRequest(req, 'AuthMessage'), + authReason: getAuthKeyFromRequest(req, 'AuthReason'), treatPendingAsSignedOut, }; +}; + +/** + * Given a request object, builds an auth object from the request data. Used in server-side environments to get access + * to auth data for a given request. + */ +export const getSessionAuthDataFromRequest = ( + req: RequestLike, + { treatPendingAsSignedOut = true, ...opts }: GetAuthDataFromRequestOptions = {}, +): SignedInAuthObject | SignedOutAuthObject => { + const { authStatus, authMessage, authReason, authToken, authSignature } = getAuthHeaders(req); + + opts.logger?.debug('headers', { authStatus, authMessage, authReason }); + + const options = createAuthOptions(req, opts, treatPendingAsSignedOut); // Only accept session tokens in this function. // Machine tokens are not supported and will result in a signed-out state. @@ -98,14 +118,7 @@ export const getAuthDataFromRequest = (req: RequestLike, opts: GetAuthDataFromRe const bearerToken = getHeader(req, constants.Headers.Authorization)?.replace('Bearer ', ''); const acceptsToken = opts.acceptsToken || TokenType.SessionToken; - const options = { - secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, - publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY, - apiUrl: API_URL, - authStatus, - authMessage, - authReason, - }; + const options = createAuthOptions(req, opts); // Handle machine tokens first (from encrypted request data) // Machine tokens are passed via x-clerk-request-data header from middleware @@ -158,19 +171,3 @@ const handleMachineToken = ( return null; }; - -const getAuthHeaders = (req: RequestLike) => { - const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); - const authToken = getAuthKeyFromRequest(req, 'AuthToken'); - const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); - const authReason = getAuthKeyFromRequest(req, 'AuthReason'); - const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); - - return { - authStatus, - authToken, - authMessage, - authReason, - authSignature, - }; -}; From 0c7e51fb0820f126d3fe73c3408b8a4992827a85 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 23 Jul 2025 12:15:36 -0700 Subject: [PATCH 09/10] chore: add changeset --- .changeset/eight-impalas-fetch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-impalas-fetch.md diff --git a/.changeset/eight-impalas-fetch.md b/.changeset/eight-impalas-fetch.md new file mode 100644 index 00000000000..94a18526ab4 --- /dev/null +++ b/.changeset/eight-impalas-fetch.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": patch +--- + +Improved machine auth verification within API routes From cc4358b3e13ae9c7dfec16e8e5ff1659c80aa0c2 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 23 Jul 2025 12:37:44 -0700 Subject: [PATCH 10/10] chore: fix imports --- packages/nextjs/src/server/data/getAuthDataFromRequest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index 70430419ae1..82b949d3ef3 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -18,7 +18,6 @@ import { } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import type { PendingSessionOptions } from '@clerk/types'; -import type { AuthenticateContext } from 'node_modules/@clerk/backend/dist/tokens/authenticateContext'; import type { LoggerNoCommit } from '../../utils/debugLogger'; import { API_URL, API_VERSION, PUBLISHABLE_KEY, SECRET_KEY } from '../constants'; @@ -146,7 +145,7 @@ const handleMachineToken = ( bearerToken: string | undefined, rawAuthObject: AuthObject | undefined, acceptsToken: NonNullable, - options: Partial, + options: Record, ): MachineAuthObject | null => { const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken);