diff --git a/.changeset/twenty-beds-serve.md b/.changeset/twenty-beds-serve.md new file mode 100644 index 00000000000..70b563d1baf --- /dev/null +++ b/.changeset/twenty-beds-serve.md @@ -0,0 +1,8 @@ +--- +'@clerk/backend': minor +'@clerk/nextjs': minor +--- + +- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array. +- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list. + diff --git a/packages/backend/src/tokens/__tests__/request.test-d.ts b/packages/backend/src/tokens/__tests__/request.test-d.ts index dcfc3f972a7..8825ed00c49 100644 --- a/packages/backend/src/tokens/__tests__/request.test-d.ts +++ b/packages/backend/src/tokens/__tests__/request.test-d.ts @@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted // Array of token types expectTypeOf( authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }), - ).toMatchTypeOf>>(); + ).toMatchTypeOf>>(); // Any token type expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf>>(); diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 26b26fec511..8cb17ed8beb 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus'; import { AuthErrorReason, AuthStatus } from '../authStatus'; import { OrganizationMatcher } from '../organizationMatcher'; import { authenticateRequest, RefreshTokenErrorReason } from '../request'; -import type { MachineTokenType } from '../tokenTypes'; +import { type MachineTokenType, TokenType } from '../tokenTypes'; import type { AuthenticateRequestOptions } from '../types'; const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA'; @@ -236,7 +236,7 @@ expect.extend({ toBeMachineUnauthenticated( received, expected: { - tokenType: MachineTokenType; + tokenType: MachineTokenType | null; reason: AuthReason; message: string; }, @@ -246,6 +246,7 @@ expect.extend({ received.tokenType === expected.tokenType && received.reason === expected.reason && received.message === expected.message && + !received.isAuthenticated && !received.token; if (pass) { @@ -264,15 +265,11 @@ expect.extend({ toBeMachineUnauthenticatedToAuth( received, expected: { - tokenType: MachineTokenType; + tokenType: MachineTokenType | null; }, ) { const pass = - received.tokenType === expected.tokenType && - !received.claims && - !received.subject && - !received.name && - !received.id; + received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id; if (pass) { return { @@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => { }); // Test each token type with parameterized tests - const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const; + const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken]; describe.each(tokenTypes)('%s Authentication', tokenType => { const mockToken = mockTokens[tokenType]; @@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => { }); expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({ tokenType, + isAuthenticated: false, }); }); }); @@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => { }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ tokenType: 'api_key', + isAuthenticated: false, }); }); @@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => { }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ tokenType: 'oauth_token', + isAuthenticated: false, }); }); @@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => { }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ tokenType: 'machine_token', + isAuthenticated: false, }); }); @@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => { tokenType: 'machine_token', reason: AuthErrorReason.TokenTypeMismatch, message: '', + isAuthenticated: false, }); expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({ tokenType: 'machine_token', + isAuthenticated: false, }); }); }); @@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => { ); expect(requestState).toBeMachineUnauthenticated({ - tokenType: 'machine_token', + tokenType: null, reason: AuthErrorReason.TokenTypeMismatch, message: '', }); expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({ - tokenType: 'machine_token', + tokenType: null, + isAuthenticated: false, }); }); }); diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index a1ec6892b0e..9c5682c3e01 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -428,31 +428,33 @@ export const getAuthObjectFromJwt = ( * Returns an auth object matching the requested token type(s). * * If the parsed token type does not match any in acceptsToken, returns: - * - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or + * - an invalid token auth object if the token is not in the accepted array + * - an unauthenticated machine object for machine tokens, or * - a signed-out session object otherwise. * * This ensures the returned object always matches the developer's intent. */ -export function getAuthObjectForAcceptedToken({ +export const getAuthObjectForAcceptedToken = ({ authObject, acceptsToken = TokenType.SessionToken, }: { authObject: AuthObject; acceptsToken: AuthenticateRequestOptions['acceptsToken']; -}): AuthObject { +}): AuthObject => { + // 1. any token: return as-is if (acceptsToken === 'any') { return authObject; } + // 2. array of tokens: must match one of the accepted types if (Array.isArray(acceptsToken)) { if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) { - // If the token is not in the accepted array, return invalid token auth object return invalidTokenAuthObject(); } return authObject; } - // Single value: Intent based + // 3. single token: must match exactly, else return appropriate unauthenticated object if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) { if (isMachineTokenType(acceptsToken)) { return unauthenticatedMachineObject(acceptsToken, authObject.debug); @@ -460,5 +462,6 @@ export function getAuthObjectForAcceptedToken({ return signedOutAuthObject(authObject.debug); } + // 4. default: return as-is return authObject; -} +}; diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 0699933e81c..d0c60eaecbd 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors'; import type { AuthenticateContext } from './authenticateContext'; import type { AuthenticatedMachineObject, + InvalidTokenAuthObject, SignedInAuthObject, SignedOutAuthObject, UnauthenticatedMachineObject, } from './authObjects'; import { authenticatedMachineObject, + invalidTokenAuthObject, signedInAuthObject, signedOutAuthObject, unauthenticatedMachineObject, @@ -27,13 +29,15 @@ export const AuthStatus = { export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus]; -type ToAuth = T extends SessionTokenType - ? Authenticated extends true - ? (opts?: PendingSessionOptions) => SignedInAuthObject - : () => SignedOutAuthObject - : Authenticated extends true - ? () => AuthenticatedMachineObject> - : () => UnauthenticatedMachineObject>; +type ToAuth = T extends null + ? () => InvalidTokenAuthObject + : T extends SessionTokenType + ? Authenticated extends true + ? (opts?: PendingSessionOptions) => SignedInAuthObject + : () => SignedOutAuthObject + : Authenticated extends true + ? () => AuthenticatedMachineObject> + : () => UnauthenticatedMachineObject>; export type AuthenticatedState = { status: typeof AuthStatus.SignedIn; @@ -58,7 +62,7 @@ export type AuthenticatedState = { toAuth: ToAuth; }; -export type UnauthenticatedState = { +export type UnauthenticatedState = { status: typeof AuthStatus.SignedOut; reason: AuthReason; message: string; @@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea export type AuthReason = AuthErrorReason | TokenVerificationErrorReason; -export type RequestState = - | AuthenticatedState +export type RequestState = + | AuthenticatedState | UnauthenticatedState | (T extends SessionTokenType ? HandshakeState : never); @@ -240,6 +244,29 @@ export function handshake( }); } +export function signedOutInvalidToken(): UnauthenticatedState { + const authObject = invalidTokenAuthObject(); + return withDebugHeaders({ + status: AuthStatus.SignedOut, + reason: AuthErrorReason.TokenTypeMismatch, + message: '', + proxyUrl: '', + publishableKey: '', + isSatellite: false, + domain: '', + signInUrl: '', + signUpUrl: '', + afterSignInUrl: '', + afterSignUpUrl: '', + isSignedIn: false, + isAuthenticated: false, + tokenType: null, + toAuth: () => authObject, + headers: new Headers(), + token: null, + }); +} + const withDebugHeaders = ( requestState: T, ): T => { diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 9b7e5f6c12a..d212f568432 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext'; import { createAuthenticateContext } from './authenticateContext'; import type { SignedInAuthObject } from './authObjects'; import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus'; -import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus'; +import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus'; import { createClerkRequest } from './clerkRequest'; import { getCookieName, getCookieValue } from './cookie'; import { HandshakeService } from './handshake'; @@ -88,6 +88,20 @@ function checkTokenTypeMismatch( return null; } +function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean { + let parsedTokenType: TokenType | null = null; + const { tokenInHeader } = authenticateContext; + if (tokenInHeader) { + if (isMachineTokenByPrefix(tokenInHeader)) { + parsedTokenType = getMachineTokenType(tokenInHeader); + } else { + parsedTokenType = TokenType.SessionToken; + } + } + const typeToCheck = parsedTokenType ?? TokenType.SessionToken; + return isTokenTypeAccepted(typeToCheck, acceptsToken); +} + export interface AuthenticateRequest { /** * @example @@ -96,7 +110,7 @@ export interface AuthenticateRequest { ( request: Request, options: AuthenticateRequestOptions & { acceptsToken: T }, - ): Promise>; + ): Promise>; /** * @example @@ -123,7 +137,7 @@ export interface AuthenticateRequest { export const authenticateRequest: AuthenticateRequest = (async ( request: Request, options: AuthenticateRequestOptions, -): Promise> => { +): Promise | UnauthenticatedState> => { const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options); assertValidSecretKey(authenticateContext.secretKey); @@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async ( // Handle case where tokenType is any and the token is not a machine token if (!isMachineTokenByPrefix(tokenInHeader)) { return signedOut({ - tokenType: acceptsToken as MachineTokenType, + tokenType: acceptsToken as TokenType, authenticateContext, reason: AuthErrorReason.TokenTypeMismatch, message: '', @@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async ( }); } + // If acceptsToken is an array, early check if the token is in the accepted array + // to avoid unnecessary verification calls + if (Array.isArray(acceptsToken)) { + if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) { + return signedOutInvalidToken(); + } + } + if (authenticateContext.tokenInHeader) { if (acceptsToken === 'any') { return authenticateAnyRequestWithTokenInHeader(); } - if (acceptsToken === TokenType.SessionToken) { return authenticateRequestWithTokenInHeader(); } - return authenticateMachineRequestWithTokenInHeader(); } diff --git a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts index 238a77e7cdc..9912dbed6f1 100644 --- a/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts +++ b/packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts @@ -477,7 +477,7 @@ describe('clerkMiddleware(params)', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer api_key_xxxxxxxxxxxxxxxxxx', + [constants.Headers.Authorization]: 'Bearer ak_123', }), }); @@ -485,7 +485,7 @@ describe('clerkMiddleware(params)', () => { publishableKey, status: AuthStatus.SignedIn, headers: new Headers(), - toAuth: () => ({ tokenType: TokenType.ApiKey, id: 'api_key_xxxxxxxxxxxxxxxxxx' }), + toAuth: () => ({ tokenType: TokenType.ApiKey, id: 'ak_123', isAuthenticated: true }), }); const resp = await clerkMiddleware(async auth => { @@ -525,7 +525,7 @@ describe('clerkMiddleware(params)', () => { const req = mockRequest({ url: '/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer api_key_xxxxxxxxxxxxxxxxxx', + [constants.Headers.Authorization]: 'Bearer ak_123', }), }); @@ -552,7 +552,7 @@ describe('clerkMiddleware(params)', () => { const req = mockRequest({ url: '/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer oauth_token_xxxxxxxxxxxxxxxxxx', + [constants.Headers.Authorization]: 'Bearer oat_123', }), }); @@ -658,7 +658,7 @@ describe('clerkMiddleware(params)', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer m2m_xxxxxxxxxxxxxxxxxx', + [constants.Headers.Authorization]: 'Bearer m2m_123', }), }); @@ -681,7 +681,7 @@ describe('clerkMiddleware(params)', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer api_key_xxx', + [constants.Headers.Authorization]: 'Bearer ak_123', }), }); @@ -689,7 +689,7 @@ describe('clerkMiddleware(params)', () => { publishableKey, status: AuthStatus.SignedIn, headers: new Headers(), - toAuth: () => ({ tokenType: TokenType.ApiKey, id: 'api_key_xxxxxxxxxxxxxxxxxx' }), + toAuth: () => ({ tokenType: TokenType.ApiKey, id: 'ak_123', isAuthenticated: true }), }); const resp = await clerkMiddleware(async auth => { @@ -705,7 +705,7 @@ describe('clerkMiddleware(params)', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer api_key_xxx', + [constants.Headers.Authorization]: 'Bearer ak_123', }), }); diff --git a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts index 3f17613ba50..82135d519d3 100644 --- a/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts +++ b/packages/nextjs/src/server/__tests__/getAuthDataFromRequest.test.ts @@ -1,7 +1,7 @@ import type { AuthenticatedMachineObject, SignedOutAuthObject } from '@clerk/backend/internal'; import { constants, verifyMachineAuthToken } from '@clerk/backend/internal'; import { NextRequest } from 'next/server'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getAuthDataFromRequestAsync, getAuthDataFromRequestSync } from '../data/getAuthDataFromRequest'; @@ -29,8 +29,22 @@ const mockRequest = (params: MockRequestParams) => { 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', () => { - it('returns unauthenticated machine object when token type does not match', async () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('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({ @@ -39,14 +53,14 @@ describe('getAuthDataFromRequestAsync', () => { }); const auth = await getAuthDataFromRequestAsync(req, { - acceptsToken: 'machine_token', + acceptsToken: ['machine_token', 'oauth_token', 'session_token'], }); - expect(auth.tokenType).toBe('machine_token'); - expect((auth as AuthenticatedMachineObject<'machine_token'>).machineId).toBeNull(); + expect(auth.tokenType).toBeNull(); + expect(auth.isAuthenticated).toBe(false); }); - it('returns invalid token auth object when token type does not match any in acceptsToken array', async () => { + it('returns unauthenticated auth object when token type does not match single acceptsToken', async () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ @@ -54,17 +68,15 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { - acceptsToken: ['machine_token', 'oauth_token', 'session_token'], - }); + const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: 'oauth_token' }); - expect(auth.tokenType).toBeNull(); + expect(auth.tokenType).toBe('oauth_token'); expect(auth.isAuthenticated).toBe(false); }); - it('returns authenticated api_key object when array contains only api_key and token is ak_xxx and verification passes', async () => { + it('returns authenticated auth object for any valid token type', async () => { vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: { id: 'ak_123', subject: 'user_12345' } as any, + data: { id: 'ak_id123', subject: 'user_12345' } as any, tokenType: 'api_key', errors: undefined, }); @@ -76,17 +88,16 @@ describe('getAuthDataFromRequestAsync', () => { }), }); - const auth = await getAuthDataFromRequestAsync(req, { - acceptsToken: ['api_key'], - }); + const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: 'any' }); expect(auth.tokenType).toBe('api_key'); - expect((auth as AuthenticatedMachineObject<'api_key'>).id).toBe('ak_123'); + expect((auth as AuthenticatedMachineObject<'api_key'>).id).toBe('ak_id123'); + expect(auth.isAuthenticated).toBe(true); }); - it('returns authenticated machine object when token type matches', async () => { + it('returns authenticated object when token type exists in acceptsToken array', async () => { vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({ - data: { id: 'ak_123', subject: 'user_12345' } as any, + data: { id: 'ak_id123', subject: 'user_12345' } as any, tokenType: 'api_key', errors: undefined, }); @@ -94,16 +105,92 @@ describe('getAuthDataFromRequestAsync', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer ak_xxx', + [constants.Headers.Authorization]: 'Bearer ak_secret123', }), }); const auth = await getAuthDataFromRequestAsync(req, { - acceptsToken: 'api_key', + acceptsToken: ['api_key', 'machine_token'], }); expect(auth.tokenType).toBe('api_key'); - expect((auth as AuthenticatedMachineObject<'api_key'>).id).toBe('ak_123'); + expect((auth as AuthenticatedMachineObject<'api_key'>).id).toBe('ak_id123'); + expect(auth.isAuthenticated).toBe(true); + }); + + it.each([ + { + tokenType: 'api_key' as const, + token: 'ak_123', + data: { id: 'ak_123', subject: 'user_12345' }, + }, + { + tokenType: 'oauth_token' as const, + token: 'oat_secret123', + data: { id: 'oat_id123', subject: 'user_12345' }, + }, + { + tokenType: 'machine_token' as const, + token: 'mt_123', + data: { id: 'm2m_123', subject: 'mch_123' }, + }, + ])( + '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, + errors: undefined, + }); + + const req = mockRequest({ + url: '/api/protected', + headers: new Headers({ + [constants.Headers.Authorization]: `Bearer ${token}`, + }), + }); + + const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType }); + + expect(auth.tokenType).toBe(tokenType); + expect(auth.isAuthenticated).toBe(true); + }, + ); + + it.each([ + { + tokenType: 'api_key' as const, + token: 'ak_123', + data: undefined, + }, + { + tokenType: 'oauth_token' as const, + token: 'oat_secret123', + data: undefined, + }, + { + tokenType: 'machine_token' as const, + 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, + tokenType, + errors: machineTokenErrorMock as any, + }); + + const req = mockRequest({ + url: '/api/protected', + headers: new Headers({ + [constants.Headers.Authorization]: `Bearer ${token}`, + }), + }); + + const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType }); + + expect(auth.tokenType).toBe(tokenType); + expect(auth.isAuthenticated).toBe(false); }); it('falls back to session token handling', async () => { @@ -117,6 +204,7 @@ describe('getAuthDataFromRequestAsync', () => { const auth = await getAuthDataFromRequestAsync(req); expect(auth.tokenType).toBe('session_token'); expect((auth as SignedOutAuthObject).userId).toBeNull(); + expect(auth.isAuthenticated).toBe(false); }); }); @@ -125,7 +213,7 @@ describe('getAuthDataFromRequestSync', () => { const req = mockRequest({ url: '/api/protected', headers: new Headers({ - [constants.Headers.Authorization]: 'Bearer api_key_xxx', + [constants.Headers.Authorization]: 'Bearer ak_123', }), }); @@ -135,5 +223,6 @@ describe('getAuthDataFromRequestSync', () => { expect(auth.tokenType).toBe('session_token'); expect(auth.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 6e7bfb40814..32090af25a5 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -4,11 +4,11 @@ import { type AuthenticateRequestOptions, AuthStatus, constants, + getAuthObjectForAcceptedToken, getAuthObjectFromJwt, getMachineTokenType, invalidTokenAuthObject, isMachineTokenByPrefix, - isMachineTokenType, isTokenTypeAccepted, type MachineTokenType, type SignedInAuthObject, @@ -33,6 +33,10 @@ export type GetAuthDataFromRequestOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken']; } & 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. + */ export const getAuthDataFromRequestSync = ( req: RequestLike, { treatPendingAsSignedOut = true, ...opts }: GetAuthDataFromRequestOptions = {}, @@ -78,10 +82,6 @@ export const getAuthDataFromRequestSync = ( }; /** - * Note: We intentionally avoid using interface/function overloads here since these functions - * are used internally. The complex type overloads are more valuable at the public API level - * (like in auth.protect(), auth()) where users interact directly with the types. - * * 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. */ @@ -103,46 +103,36 @@ export const getAuthDataFromRequestAsync = async ( authReason, }; - if (bearerToken) { - const isMachine = isMachineTokenByPrefix(bearerToken); - const tokenType = isMachine ? getMachineTokenType(bearerToken) : undefined; - - if (Array.isArray(acceptsToken)) { - if (isMachine) { - return handleMachineToken({ - bearerToken, - tokenType: tokenType as MachineTokenType, - acceptsToken, - options, - }); - } - } else { - let intendedType: TokenType | undefined; - if (isMachineTokenType(acceptsToken)) { - intendedType = acceptsToken; - } - const result = await handleIntentBased({ - isMachine, - tokenType, - intendedType, - bearerToken, - acceptsToken, - options, - }); - if (result) { - return result; - } - } - } + const hasMachineToken = bearerToken && isMachineTokenByPrefix(bearerToken); + if (hasMachineToken) { + const machineTokenType = getMachineTokenType(bearerToken); - if (Array.isArray(acceptsToken)) { - if (!isTokenTypeAccepted(TokenType.SessionToken, acceptsToken)) { + // 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 as MachineTokenType, 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 }); + } + + // If a random token is present and acceptsToken is an array that does NOT include session_token, + // return invalidTokenAuthObject. + if (bearerToken && Array.isArray(acceptsToken) && !acceptsToken.includes(TokenType.SessionToken)) { + return invalidTokenAuthObject(); } - // Fall through to session logic - return getAuthDataFromRequestSync(req, opts); + // Fallback to session logic (sync version) for all other cases + const authObject = getAuthDataFromRequestSync(req, opts); + return getAuthObjectForAcceptedToken({ authObject, acceptsToken }); }; const getAuthHeaders = (req: RequestLike) => { @@ -160,76 +150,3 @@ const getAuthHeaders = (req: RequestLike) => { authSignature, }; }; - -/** - * Handles verification and response shaping for machine tokens. - * Returns an authenticated or unauthenticated machine object based on verification and type acceptance. - */ -async function handleMachineToken({ - bearerToken, - tokenType, - acceptsToken, - options, -}: { - bearerToken: string; - tokenType: MachineTokenType; - acceptsToken: AuthenticateRequestOptions['acceptsToken']; - options: Record; -}) { - if (Array.isArray(acceptsToken)) { - // If the token is not in the accepted array, return invalid token auth object - if (!isTokenTypeAccepted(tokenType, acceptsToken)) { - return invalidTokenAuthObject(); - } - } - - if (!isTokenTypeAccepted(tokenType, acceptsToken ?? TokenType.SessionToken)) { - return unauthenticatedMachineObject(tokenType, options); - } - const { data, errors } = await verifyMachineAuthToken(bearerToken, options); - if (errors) { - return unauthenticatedMachineObject(tokenType, options); - } - return authenticatedMachineObject(tokenType, bearerToken, data); -} - -/** - * Handles intent-based fallback for single-value acceptsToken. - * Returns an unauthenticated object for the intended type, or falls back to session logic if not applicable. - */ -async function handleIntentBased({ - isMachine, - tokenType, - intendedType, - bearerToken, - acceptsToken, - options, -}: { - isMachine: boolean; - tokenType: TokenType | undefined; - intendedType: TokenType | undefined; - bearerToken: string; - acceptsToken: AuthenticateRequestOptions['acceptsToken']; - options: Record; -}) { - if (isMachine) { - if (!tokenType) { - return signedOutAuthObject(options); - } - if (!isTokenTypeAccepted(tokenType, acceptsToken ?? TokenType.SessionToken)) { - if (intendedType && isMachineTokenType(intendedType)) { - return unauthenticatedMachineObject(intendedType, options); - } - return signedOutAuthObject(options); - } - const { data, errors } = await verifyMachineAuthToken(bearerToken, options); - if (errors) { - return unauthenticatedMachineObject(tokenType as MachineTokenType, options); - } - return authenticatedMachineObject(tokenType as MachineTokenType, bearerToken, data); - } else if (intendedType && isMachineTokenType(intendedType)) { - return unauthenticatedMachineObject(intendedType, options); - } - // else: fall through to session logic - return null; -} diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index f74918893a8..b5372c766a5 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -145,14 +145,10 @@ export function createProtect(opts: { return handleUnauthorized(); } - if (authObject.tokenType === null) { - return handleUnauthorized(); - } - if (authObject.tokenType !== TokenType.SessionToken) { // For machine tokens, we only check if they're authenticated // They don't have session status or organization permissions - if (!authObject.id) { + if (!authObject.isAuthenticated) { return handleUnauthorized(); } return authObject;