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 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..91c5144086e 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 { getAuthDataFromRequestAsync, getAuthDataFromRequestSync } from '../data/getAuthDataFromRequest'; +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', }; }); @@ -17,42 +21,57 @@ type MockRequestParams = { url: string; appendDevBrowserCookie?: boolean; method?: string; - headers?: any; + headers?: Headers; + machineAuthObject?: Partial>; }; 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 + // @ts-expect-error - mock machine auth object + machineAuthObject, + ); + 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', - }, -]; - -describe('getAuthDataFromRequestAsync', () => { +// Helper function to create mock machine auth objects +const createMockMachineAuthObject = (data: Partial>) => data; + +describe('getAuthDataFromRequest', () => { beforeEach(() => { vi.clearAllMocks(); }); - it('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 machineAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + }); + const req = mockRequest({ url: '/api/protected', headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject, }); - const auth = await getAuthDataFromRequestAsync(req, { + const auth = getAuthDataFromRequest(req, { acceptsToken: ['machine_token', 'oauth_token', 'session_token'], }); @@ -60,25 +79,76 @@ describe('getAuthDataFromRequestAsync', () => { expect(auth.isAuthenticated).toBe(false); }); - it('returns unauthenticated auth object when token type does not match single acceptsToken', async () => { + it('handles mixed token types in acceptsToken array', () => { + const machineAuthObject = 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, + }); + + 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 machineAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + }); + + const req = mockRequest({ + url: '/api/protected', + headers: new Headers({ + [constants.Headers.Authorization]: 'Bearer ak_xxx', + }), + machineAuthObject, + }); + + 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 machineAuthObject = createMockMachineAuthObject({ + tokenType: 'api_key', + isAuthenticated: true, + }); + const req = mockRequest({ url: '/api/protected', headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject, }); - 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 () => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: { id: 'ak_id123', subject: 'user_12345' } as any, + it('returns authenticated auth object for any valid token type', () => { + const machineAuthObject = createMockMachineAuthObject({ tokenType: 'api_key', - errors: undefined, + id: 'ak_id123', + isAuthenticated: true, }); const req = mockRequest({ @@ -86,30 +156,33 @@ describe('getAuthDataFromRequestAsync', () => { headers: new Headers({ [constants.Headers.Authorization]: 'Bearer ak_xxx', }), + machineAuthObject, }); - 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 () => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: { id: 'ak_id123', subject: 'user_12345' } as any, + it('returns authenticated object when token type exists in acceptsToken array', () => { + const machineAuthObject = 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, }); - const auth = await getAuthDataFromRequestAsync(req, { + const auth = getAuthDataFromRequest(req, { acceptsToken: ['api_key', 'machine_token'], }); @@ -136,11 +209,11 @@ describe('getAuthDataFromRequestAsync', () => { }, ])( 'returns authenticated $tokenType object when token is valid and acceptsToken is $tokenType', - async ({ tokenType, token, data }) => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: data as any, + ({ tokenType, token, data }) => { + const machineAuthObject = createMockMachineAuthObject({ tokenType, - errors: undefined, + isAuthenticated: true, + ...data, }); const req = mockRequest({ @@ -148,9 +221,10 @@ describe('getAuthDataFromRequestAsync', () => { headers: new Headers({ [constants.Headers.Authorization]: `Bearer ${token}`, }), + machineAuthObject, }); - const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType }); + const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType }); expect(auth.tokenType).toBe(tokenType); expect(auth.isAuthenticated).toBe(true); @@ -173,27 +247,28 @@ describe('getAuthDataFromRequestAsync', () => { token: 'mt_123', data: undefined, }, - ])('returns unauthenticated $tokenType object when token is invalid', async ({ tokenType, token, data }) => { - vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: data as any, + ])('returns unauthenticated $tokenType object when token is invalid', ({ tokenType, token }) => { + const machineAuthObject = 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, }); - 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 when no encrypted auth object is present', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ @@ -201,15 +276,13 @@ 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', () => { + it('only accepts session tokens when encrypted auth object is not present', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ @@ -217,12 +290,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/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index da58ce560c6..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'; @@ -265,7 +266,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 : makeAuthObjectSerializable(authObject), + ); return handlerResult; }); 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 34da3ab3ab5..82b949d3ef3 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -1,21 +1,20 @@ -import type { AuthObject } from '@clerk/backend'; +import type { AuthObject, MachineAuthObject } from '@clerk/backend'; +import type { + AuthenticateRequestOptions, + MachineTokenType, + SignedInAuthObject, + SignedOutAuthObject, +} from '@clerk/backend/internal'; import { - authenticatedMachineObject, - type AuthenticateRequestOptions, AuthStatus, constants, getAuthObjectForAcceptedToken, getAuthObjectFromJwt, - getMachineTokenType, invalidTokenAuthObject, isMachineTokenByPrefix, isTokenTypeAccepted, - type SignedInAuthObject, - type SignedOutAuthObject, signedOutAuthObject, TokenType, - unauthenticatedMachineObject, - verifyMachineAuthToken, } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import type { PendingSessionOptions } from '@clerk/types'; @@ -33,33 +32,53 @@ 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 getAuthDataFromRequestSync = ( - 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, }; +}; - // 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. +/** + * 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. if (!isTokenTypeAccepted(TokenType.SessionToken, opts.acceptsToken || TokenType.SessionToken)) { return signedOutAuthObject(options); } @@ -80,64 +99,34 @@ export const getAuthDataFromRequestSync = ( return authObject; }; -const handleMachineToken = async ( - bearerToken: string | undefined, - acceptsToken: NonNullable, - options: GetAuthDataFromRequestOptions, -): Promise => { - 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 }); - } - - const { data, errors } = await verifyMachineAuthToken(bearerToken, options); - const authObject = errors - ? unauthenticatedMachineObject(machineTokenType, options) - : authenticatedMachineObject(machineTokenType, bearerToken, data); - return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); - } - - 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 }); + 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, - apiUrl: API_URL, - authStatus, - authMessage, - authReason, - }; - // If the request has a machine token in header, handle it first. - const machineAuthObject = await handleMachineToken(bearerToken, acceptsToken, options); + 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 + const machineAuthObject = handleMachineToken( + bearerToken, + decryptedRequestData.machineAuthObject, + acceptsToken, + options, + ); if (machineAuthObject) { return machineAuthObject; } @@ -149,22 +138,35 @@ 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 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'); +const handleMachineToken = ( + bearerToken: string | undefined, + rawAuthObject: AuthObject | undefined, + acceptsToken: NonNullable, + options: Record, +): MachineAuthObject | null => { + const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken); - return { - authStatus, - authToken, - authMessage, - authReason, - authSignature, - }; + 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; }; diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 43bbe3e5ab4..155788b0164 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, + machineAuthObject: 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, machineAuthObject); 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, + machineAuthObject: AuthObject | null, ) { const isEmpty = (obj: Record | undefined) => { if (!obj) { @@ -192,7 +195,7 @@ export function encryptClerkRequestData( return !Object.values(obj).some(v => v !== undefined); }; - if (isEmpty(requestData) && isEmpty(keylessModeKeys)) { + if (isEmpty(requestData) && isEmpty(keylessModeKeys) && !machineAuthObject) { return; } @@ -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, machineAuthObject: machineAuthObject ?? undefined }), + maybeKeylessEncryptionKey, + ).toString(); } /** @@ -218,7 +224,7 @@ export function encryptClerkRequestData( */ export function decryptClerkRequestData( encryptedRequestData?: string | undefined | null, -): Partial { +): Partial & { machineAuthObject?: AuthObject } { if (!encryptedRequestData) { return {}; }