From b7926d163ac97bfd7605c20f47d216b627e40c16 Mon Sep 17 00:00:00 2001 From: Jeff Escalante Date: Fri, 28 Nov 2025 22:52:03 -0500 Subject: [PATCH 1/5] 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 d9434e711a7..80f9b791acc 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 0061828b026..db9892ef240 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, From 5f85333d4b9a3a1a76b7a11b3e6d628d5030a9ad Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 8 Jan 2026 20:42:06 -0600 Subject: [PATCH 2/5] chore(repo): Add changeset for azp validation in cookie tokens --- .changeset/three-things-jump.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/three-things-jump.md diff --git a/.changeset/three-things-jump.md b/.changeset/three-things-jump.md new file mode 100644 index 00000000000..47c8f7b2aaf --- /dev/null +++ b/.changeset/three-things-jump.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Add validation to require `azp` claim in cookie-based session tokens. Tokens from cookies that are missing the `azp` (authorized party) claim will now return a signed-out state with reason `token-missing-azp`. From b52ac8128b6ba58ab311738d3d6ea62de9629397 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 8 Jan 2026 20:46:26 -0600 Subject: [PATCH 3/5] fix(backend): Fix ESLint errors in request_azp test --- packages/backend/src/tokens/__tests__/request_azp.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/tokens/__tests__/request_azp.test.ts b/packages/backend/src/tokens/__tests__/request_azp.test.ts index b2349df05b2..5196b0066b4 100644 --- a/packages/backend/src/tokens/__tests__/request_azp.test.ts +++ b/packages/backend/src/tokens/__tests__/request_azp.test.ts @@ -3,7 +3,6 @@ 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', () => ({ @@ -51,9 +50,9 @@ describe('authenticateRequest with cookie token', () => { const result = await authenticateRequest(request, options); expect(result.status).toBe('signed-out'); - // @ts-ignore + // @ts-expect-error - reason is only available on signed-out state expect(result.reason).toBe(TokenVerificationErrorReason.TokenMissingAzp); - // @ts-ignore + // @ts-expect-error - message is only available on signed-out state expect(result.message).toBe( 'Session tokens from cookies must have an azp claim. (reason=token-missing-azp, token-carrier=cookie)', ); From e504313fa83a92b95a9203ac0321a69c2b1e1a3a Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 8 Jan 2026 20:49:08 -0600 Subject: [PATCH 4/5] chore(repo): Bump @clerk/backend changeset to major MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The azp validation change is breaking for cookie-based session tokens. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/three-things-jump.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/three-things-jump.md b/.changeset/three-things-jump.md index 47c8f7b2aaf..def616d1a68 100644 --- a/.changeset/three-things-jump.md +++ b/.changeset/three-things-jump.md @@ -1,5 +1,5 @@ --- -'@clerk/backend': patch +'@clerk/backend': major --- Add validation to require `azp` claim in cookie-based session tokens. Tokens from cookies that are missing the `azp` (authorized party) claim will now return a signed-out state with reason `token-missing-azp`. From 854fd493f4187de3c7122f3eac357bf56c24b7c7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 8 Jan 2026 20:56:16 -0600 Subject: [PATCH 5/5] fix(backend): Remove unnecessary @ts-expect-error directives in request_azp test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reason and message properties exist on all variants of the RequestState union type, so TypeScript allows accessing them without errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/backend/src/tokens/__tests__/request_azp.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/tokens/__tests__/request_azp.test.ts b/packages/backend/src/tokens/__tests__/request_azp.test.ts index 5196b0066b4..9ec4b20267c 100644 --- a/packages/backend/src/tokens/__tests__/request_azp.test.ts +++ b/packages/backend/src/tokens/__tests__/request_azp.test.ts @@ -50,9 +50,7 @@ describe('authenticateRequest with cookie token', () => { const result = await authenticateRequest(request, options); expect(result.status).toBe('signed-out'); - // @ts-expect-error - reason is only available on signed-out state expect(result.reason).toBe(TokenVerificationErrorReason.TokenMissingAzp); - // @ts-expect-error - message is only available on signed-out state expect(result.message).toBe( 'Session tokens from cookies must have an azp claim. (reason=token-missing-azp, token-carrier=cookie)', );