From e8716ed5b37dd4724aea18458bb54c59e17352e6 Mon Sep 17 00:00:00 2001 From: Jeff Escalante Date: Fri, 28 Nov 2025 22:52:03 -0500 Subject: [PATCH] error if azp is missing on a cookie-based token --- packages/backend/src/errors.ts | 1 + .../src/tokens/__tests__/request_azp.test.ts | 135 ++++++++++++++++++ packages/backend/src/tokens/request.ts | 7 + 3 files changed, 143 insertions(+) create mode 100644 packages/backend/src/tokens/__tests__/request_azp.test.ts diff --git a/packages/backend/src/errors.ts b/packages/backend/src/errors.ts index 34b2d67d19c..daad2283678 100644 --- a/packages/backend/src/errors.ts +++ b/packages/backend/src/errors.ts @@ -11,6 +11,7 @@ export const TokenVerificationErrorReason = { TokenInvalid: 'token-invalid', TokenInvalidAlgorithm: 'token-invalid-algorithm', TokenInvalidAuthorizedParties: 'token-invalid-authorized-parties', + TokenMissingAzp: 'token-missing-azp', TokenInvalidSignature: 'token-invalid-signature', TokenNotActiveYet: 'token-not-active-yet', TokenIatInTheFuture: 'token-iat-in-the-future', diff --git a/packages/backend/src/tokens/__tests__/request_azp.test.ts b/packages/backend/src/tokens/__tests__/request_azp.test.ts new file mode 100644 index 00000000000..b2349df05b2 --- /dev/null +++ b/packages/backend/src/tokens/__tests__/request_azp.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, test, vi } from 'vitest'; + +import { TokenVerificationErrorReason } from '../../errors'; +import { decodeJwt } from '../../jwt/verifyJwt'; +import { authenticateRequest } from '../request'; +import { TokenType } from '../tokenTypes'; +import { verifyToken } from '../verify'; + +vi.mock('../verify', () => ({ + verifyToken: vi.fn(), + verifyMachineAuthToken: vi.fn(), +})); + +vi.mock('../../jwt/verifyJwt', () => ({ + decodeJwt: vi.fn(), +})); + +describe('authenticateRequest with cookie token', () => { + test('throws TokenMissingAzp when azp claim is missing', async () => { + const payload = { + sub: 'user_123', + sid: 'sess_123', + iat: 1234567891, + exp: 1234567991, + // azp is missing + }; + + // Mock verifyToken to return a payload without azp + vi.mocked(verifyToken).mockResolvedValue({ + data: payload as any, + errors: undefined, + }); + + // Mock decodeJwt to return the same payload + vi.mocked(decodeJwt).mockReturnValue({ + data: { payload } as any, + errors: undefined, + }); + + const request = new Request('http://localhost:3000', { + headers: { + cookie: '__session=mock_token; __client_uat=1234567890', + }, + }); + + const options = { + publishableKey: 'pk_live_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA', + secretKey: 'sk_live_deadbeef', + }; + + const result = await authenticateRequest(request, options); + + expect(result.status).toBe('signed-out'); + // @ts-ignore + expect(result.reason).toBe(TokenVerificationErrorReason.TokenMissingAzp); + // @ts-ignore + expect(result.message).toBe( + 'Session tokens from cookies must have an azp claim. (reason=token-missing-azp, token-carrier=cookie)', + ); + }); + + test('succeeds when azp claim is present', async () => { + const payload = { + sub: 'user_123', + sid: 'sess_123', + iat: 1234567891, + exp: 1234567991, + azp: 'http://localhost:3000', + }; + + // Mock verifyToken to return a payload with azp + vi.mocked(verifyToken).mockResolvedValue({ + data: payload as any, + errors: undefined, + }); + + // Mock decodeJwt to return the same payload + vi.mocked(decodeJwt).mockReturnValue({ + data: { payload } as any, + errors: undefined, + }); + + const request = new Request('http://localhost:3000', { + headers: { + cookie: '__session=mock_token; __client_uat=1234567890', + }, + }); + + const options = { + publishableKey: 'pk_live_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA', + secretKey: 'sk_live_deadbeef', + }; + + const result = await authenticateRequest(request, options); + expect(result.isSignedIn).toBe(true); + }); +}); + +describe('authenticateRequest with header token', () => { + test('succeeds when azp claim is missing', async () => { + const payload = { + sub: 'user_123', + sid: 'sess_123', + iat: 1234567891, + exp: 1234567991, + // azp is missing + }; + + // Mock verifyToken to return a payload without azp + vi.mocked(verifyToken).mockResolvedValue({ + data: payload as any, + errors: undefined, + }); + + // Mock decodeJwt to return the same payload + vi.mocked(decodeJwt).mockReturnValue({ + data: { payload } as any, + errors: undefined, + }); + + const request = new Request('http://localhost:3000', { + headers: { + authorization: 'Bearer mock_token', + }, + }); + + const options = { + publishableKey: 'pk_live_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA', + secretKey: 'sk_live_deadbeef', + }; + + const result = await authenticateRequest(request, options); + expect(result.isSignedIn).toBe(true); + }); +}); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 1d2aaaa6d1e..a88bea408cd 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -565,6 +565,13 @@ export const authenticateRequest: AuthenticateRequest = (async ( throw errors[0]; } + if (!data.azp) { + throw new TokenVerificationError({ + reason: TokenVerificationErrorReason.TokenMissingAzp, + message: 'Session tokens from cookies must have an azp claim.', + }); + } + const signedInRequestState = signedIn({ tokenType: TokenType.SessionToken, authenticateContext,