From 5f57241dc8fc96047238e5f270b1beddafa6c31c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 20:35:07 +0000 Subject: [PATCH 1/9] fix(shared): type JWT aud as string or string array Add the RFC 7519 audience claim to JwtPayload and deprecated ClerkJWTClaims so decodeJwt and verifyJwt expose string | string[] | undefined instead of unknown. Co-authored-by: Kevin Wang --- .changeset/type-jwt-aud-rfc-8707.md | 6 ++ .../src/jwt/__tests__/jwtPayload.type.test.ts | 21 +++++++ .../src/tokens/__tests__/verify.test.ts | 58 ++++++++++++++++++- .../src/types/__tests__/jwtv2.type.test.ts | 45 ++++++++++++++ packages/shared/src/types/jwt.ts | 5 ++ packages/shared/src/types/jwtv2.ts | 5 ++ 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 .changeset/type-jwt-aud-rfc-8707.md create mode 100644 packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts create mode 100644 packages/shared/src/types/__tests__/jwtv2.type.test.ts diff --git a/.changeset/type-jwt-aud-rfc-8707.md b/.changeset/type-jwt-aud-rfc-8707.md new file mode 100644 index 00000000000..69cbd6a730a --- /dev/null +++ b/.changeset/type-jwt-aud-rfc-8707.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +'@clerk/backend': patch +--- + +Type the JWT `aud` claim as an optional `string | string[]` per RFC 7519. Clerk-issued OAuth access tokens may include a single RFC 8707 resource URI as a string. diff --git a/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts b/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts new file mode 100644 index 00000000000..d6ad81b8685 --- /dev/null +++ b/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts @@ -0,0 +1,21 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import type { JwtPayload } from '@clerk/shared/types'; + +import { decodeJwt, verifyJwt } from '../index'; +import { decodeJwt as decodeJwtResult, verifyJwt as verifyJwtResult } from '../verifyJwt'; + +describe('decodeJwt and verifyJwt audience claim types', () => { + it('exposes the same optional RFC 7519 aud type on JwtPayload', () => { + type PublicDecodeAud = ReturnType['payload']['aud']; + type PublicVerifyAud = Awaited>['aud']; + type ResultDecodeAud = NonNullable['data']>['payload']['aud']; + type ResultVerifyAud = NonNullable>['data']>['aud']; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); +}); diff --git a/packages/backend/src/tokens/__tests__/verify.test.ts b/packages/backend/src/tokens/__tests__/verify.test.ts index 6a12115c01a..3cf4ea44c81 100644 --- a/packages/backend/src/tokens/__tests__/verify.test.ts +++ b/packages/backend/src/tokens/__tests__/verify.test.ts @@ -22,7 +22,7 @@ import { JWT_CATEGORY_M2M_TOKEN } from '../jwtCategories'; import { verifyMachineAuthToken, verifyToken } from '../verify'; async function createSignedOAuthJwt( - payload = mockOAuthAccessTokenJwtPayload, + payload: Record = mockOAuthAccessTokenJwtPayload, typ: 'at+jwt' | 'application/at+jwt' | 'JWT' = 'at+jwt', ) { const { data } = await signJwt(payload, signingJwks, { @@ -558,7 +558,7 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { payload.sub = sub; } - const oauthJwt = await createSignedOAuthJwt(payload as typeof mockOAuthAccessTokenJwtPayload, 'at+jwt'); + const oauthJwt = await createSignedOAuthJwt(payload, 'at+jwt'); const result = await verifyMachineAuthToken(oauthJwt, { apiUrl: 'https://api.clerk.test', @@ -569,6 +569,60 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { expect(result.tokenType).toBe('oauth_token'); }, ); + + it('verifies OAuth JWT with a matching RFC 8707 resource audience', async () => { + server.use( + http.get( + 'https://api.clerk.test/v1/jwks', + validateHeaders(() => { + return HttpResponse.json(mockJwks); + }), + ), + ); + + const audience = 'https://my-resource.example.com'; + const oauthJwt = await createSignedOAuthJwt({ + ...mockOAuthAccessTokenJwtPayload, + aud: audience, + }); + + const result = await verifyMachineAuthToken(oauthJwt, { + apiUrl: 'https://api.clerk.test', + secretKey: 'a-valid-key', + audience, + }); + + expect(result.tokenType).toBe('oauth_token'); + expect(result.data).toBeDefined(); + expect(result.errors).toBeUndefined(); + }); + + it('rejects OAuth JWT with a mismatched RFC 8707 resource audience', async () => { + server.use( + http.get( + 'https://api.clerk.test/v1/jwks', + validateHeaders(() => { + return HttpResponse.json(mockJwks); + }), + ), + ); + + const oauthJwt = await createSignedOAuthJwt({ + ...mockOAuthAccessTokenJwtPayload, + aud: 'https://attacker.example.com', + }); + + const result = await verifyMachineAuthToken(oauthJwt, { + apiUrl: 'https://api.clerk.test', + secretKey: 'a-valid-key', + audience: 'https://my-resource.example.com', + }); + + expect(result.tokenType).toBe('oauth_token'); + expect(result.data).toBeUndefined(); + expect(result.errors).toBeDefined(); + expect(result.errors?.[0].message).toContain('Invalid JWT audience claim'); + }); }); describe('verifyM2MToken with JWT', () => { diff --git a/packages/shared/src/types/__tests__/jwtv2.type.test.ts b/packages/shared/src/types/__tests__/jwtv2.type.test.ts new file mode 100644 index 00000000000..3f7a34f9d72 --- /dev/null +++ b/packages/shared/src/types/__tests__/jwtv2.type.test.ts @@ -0,0 +1,45 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import type { ClerkJWTClaims, JWTClaims } from '../jwt'; +import type { Jwt, JwtPayload } from '../jwtv2'; + +describe('JwtPayload aud claim', () => { + it('is an optional RFC 7519 string or string array', () => { + expectTypeOf().toEqualTypeOf(); + }); + + it('allows omitting the claim', () => { + expectTypeOf().toExtend(); + }); + + it('allows a string resource audience', () => { + expectTypeOf().toExtend(); + }); + + it('allows a string-array audience', () => { + expectTypeOf().toExtend(); + }); + + it('is exposed on decodeJwt payload via Jwt', () => { + expectTypeOf().toEqualTypeOf(); + }); +}); + +describe('deprecated ClerkJWTClaims aud claim', () => { + it('stays consistent with JwtPayload', () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); + + it('allows omitting the claim', () => { + expectTypeOf().toExtend(); + }); + + it('allows a string resource audience', () => { + expectTypeOf().toExtend(); + }); + + it('allows a string-array audience', () => { + expectTypeOf().toExtend(); + }); +}); diff --git a/packages/shared/src/types/jwt.ts b/packages/shared/src/types/jwt.ts index f2b47790a75..67295ca1946 100644 --- a/packages/shared/src/types/jwt.ts +++ b/packages/shared/src/types/jwt.ts @@ -77,6 +77,11 @@ export interface ClerkJWTClaims { */ azp?: string; + /** + * JWT Audience - [RFC7519#section-4.1.3](https://tools.ietf.org/html/rfc7519#section-4.1.3). + */ + aud?: string | string[]; + /** * JWT Actor - [RFC8693](https://www.rfc-editor.org/rfc/rfc8693.html#name-act-actor-claim). */ diff --git a/packages/shared/src/types/jwtv2.ts b/packages/shared/src/types/jwtv2.ts index da7e1de736a..cb2e288d20a 100644 --- a/packages/shared/src/types/jwtv2.ts +++ b/packages/shared/src/types/jwtv2.ts @@ -82,6 +82,11 @@ type JWTPayloadBase = { */ azp?: string; + /** + * JWT Audience - [RFC7519#section-4.1.3](https://tools.ietf.org/html/rfc7519#section-4.1.3). + */ + aud?: string | string[]; + /** * JWT Actor - [RFC8693](https://www.rfc-editor.org/rfc/rfc8693.html#name-act-actor-claim). */ From 4cef5282499df0bc2f9467a00da95e0d0464ffff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 20:39:03 +0000 Subject: [PATCH 2/9] fix(backend): accept RFC 7519 string aud on M2M JWT payloads Widen the internal M2M JWT audience type so JwtPayload remains assignable after aud is typed as string | string[]. --- packages/backend/src/api/resources/M2MToken.ts | 5 +++-- .../src/api/resources/__tests__/M2MToken.test.ts | 15 +++++++++++++++ .../src/jwt/__tests__/jwtPayload.type.test.ts | 7 +++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/api/resources/M2MToken.ts b/packages/backend/src/api/resources/M2MToken.ts index 5b19fdd9603..02e37e93b08 100644 --- a/packages/backend/src/api/resources/M2MToken.ts +++ b/packages/backend/src/api/resources/M2MToken.ts @@ -7,7 +7,7 @@ type M2MJwtPayload = { exp: number; iat: number; jti?: string; - aud?: string[]; + aud?: string | string[]; scopes?: string; [key: string]: unknown; }; @@ -82,10 +82,11 @@ export class M2MToken { } static fromJwtPayload(payload: M2MJwtPayload, clockSkewInMs = 5000): M2MToken { + const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : []; return new M2MToken( payload.jti ?? '', // jti should always be present in Clerk-issued M2M JWTs payload.sub, - payload.scopes?.split(' ') ?? payload.aud ?? [], + payload.scopes?.split(' ') ?? audience, extractCustomClaims(payload), false, null, diff --git a/packages/backend/src/api/resources/__tests__/M2MToken.test.ts b/packages/backend/src/api/resources/__tests__/M2MToken.test.ts index a0b440430f8..8e4bffd275b 100644 --- a/packages/backend/src/api/resources/__tests__/M2MToken.test.ts +++ b/packages/backend/src/api/resources/__tests__/M2MToken.test.ts @@ -105,6 +105,21 @@ describe('M2MToken', () => { expect(token.scopes).toEqual(['scope1', 'scope2', 'scope3']); }); + it('seeds scopes from a string aud claim', () => { + const payload = { + sub: 'mch_test', + exp: 1666648550, + iat: 1666648250, + jti: 'mt_test', + aud: 'https://my-resource.example.com', + }; + + const token = M2MToken.fromJwtPayload(payload); + + expect(token.scopes).toEqual(['https://my-resource.example.com']); + expect(token.claims).toEqual({ aud: 'https://my-resource.example.com' }); + }); + it('returns empty scopes when neither aud nor scopes present', () => { const payload = { sub: 'mch_test', diff --git a/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts b/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts index d6ad81b8685..9cb62ff7ea6 100644 --- a/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts +++ b/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts @@ -1,9 +1,8 @@ -import { describe, expectTypeOf, it } from 'vitest'; - import type { JwtPayload } from '@clerk/shared/types'; +import { describe, expectTypeOf, it } from 'vitest'; -import { decodeJwt, verifyJwt } from '../index'; -import { decodeJwt as decodeJwtResult, verifyJwt as verifyJwtResult } from '../verifyJwt'; +import type { decodeJwt, verifyJwt } from '../index'; +import type { decodeJwt as decodeJwtResult, verifyJwt as verifyJwtResult } from '../verifyJwt'; describe('decodeJwt and verifyJwt audience claim types', () => { it('exposes the same optional RFC 7519 aud type on JwtPayload', () => { From 0c52c1b3b30fa7b5e24a1cb30824828ff910c4e2 Mon Sep 17 00:00:00 2001 From: thiskevinwang Date: Wed, 26 Aug 2026 20:45:50 +0000 Subject: [PATCH 3/9] chore(repo): bump @clerk/shared changeset to minor Co-authored-by: Codesmith --- .changeset/type-jwt-aud-rfc-8707.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/type-jwt-aud-rfc-8707.md b/.changeset/type-jwt-aud-rfc-8707.md index 69cbd6a730a..d132f87aaea 100644 --- a/.changeset/type-jwt-aud-rfc-8707.md +++ b/.changeset/type-jwt-aud-rfc-8707.md @@ -1,5 +1,5 @@ --- -'@clerk/shared': patch +'@clerk/shared': minor '@clerk/backend': patch --- From 0f9e70673e18a6ff4e77ac3cdc32086234d81e7e Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 26 Aug 2026 17:08:18 -0400 Subject: [PATCH 4/9] trim extra tests --- .../src/jwt/__tests__/jwtPayload.type.test.ts | 20 --------- .../src/types/__tests__/jwtv2.type.test.ts | 45 ------------------- 2 files changed, 65 deletions(-) delete mode 100644 packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts delete mode 100644 packages/shared/src/types/__tests__/jwtv2.type.test.ts diff --git a/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts b/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts deleted file mode 100644 index 9cb62ff7ea6..00000000000 --- a/packages/backend/src/jwt/__tests__/jwtPayload.type.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { JwtPayload } from '@clerk/shared/types'; -import { describe, expectTypeOf, it } from 'vitest'; - -import type { decodeJwt, verifyJwt } from '../index'; -import type { decodeJwt as decodeJwtResult, verifyJwt as verifyJwtResult } from '../verifyJwt'; - -describe('decodeJwt and verifyJwt audience claim types', () => { - it('exposes the same optional RFC 7519 aud type on JwtPayload', () => { - type PublicDecodeAud = ReturnType['payload']['aud']; - type PublicVerifyAud = Awaited>['aud']; - type ResultDecodeAud = NonNullable['data']>['payload']['aud']; - type ResultVerifyAud = NonNullable>['data']>['aud']; - - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - }); -}); diff --git a/packages/shared/src/types/__tests__/jwtv2.type.test.ts b/packages/shared/src/types/__tests__/jwtv2.type.test.ts deleted file mode 100644 index 3f7a34f9d72..00000000000 --- a/packages/shared/src/types/__tests__/jwtv2.type.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expectTypeOf, it } from 'vitest'; - -import type { ClerkJWTClaims, JWTClaims } from '../jwt'; -import type { Jwt, JwtPayload } from '../jwtv2'; - -describe('JwtPayload aud claim', () => { - it('is an optional RFC 7519 string or string array', () => { - expectTypeOf().toEqualTypeOf(); - }); - - it('allows omitting the claim', () => { - expectTypeOf().toExtend(); - }); - - it('allows a string resource audience', () => { - expectTypeOf().toExtend(); - }); - - it('allows a string-array audience', () => { - expectTypeOf().toExtend(); - }); - - it('is exposed on decodeJwt payload via Jwt', () => { - expectTypeOf().toEqualTypeOf(); - }); -}); - -describe('deprecated ClerkJWTClaims aud claim', () => { - it('stays consistent with JwtPayload', () => { - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - }); - - it('allows omitting the claim', () => { - expectTypeOf().toExtend(); - }); - - it('allows a string resource audience', () => { - expectTypeOf().toExtend(); - }); - - it('allows a string-array audience', () => { - expectTypeOf().toExtend(); - }); -}); From 0769cad8b66eff9c218689c3ad51654037e94c5f Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 26 Aug 2026 17:13:56 -0400 Subject: [PATCH 5/9] make logic more human friendly --- packages/backend/src/api/resources/M2MToken.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/api/resources/M2MToken.ts b/packages/backend/src/api/resources/M2MToken.ts index 02e37e93b08..2132ffc5f33 100644 --- a/packages/backend/src/api/resources/M2MToken.ts +++ b/packages/backend/src/api/resources/M2MToken.ts @@ -82,7 +82,15 @@ export class M2MToken { } static fromJwtPayload(payload: M2MJwtPayload, clockSkewInMs = 5000): M2MToken { - const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : []; + let audience: string[] = []; + // If audience is an array, use it directly; + // If it's a string, wrap it in an array; + // If it's undefined, leave it as an empty array. + if (Array.isArray(payload.aud)) { + audience = payload.aud; + } else if (payload.aud) { + audience = [payload.aud]; + } return new M2MToken( payload.jti ?? '', // jti should always be present in Clerk-issued M2M JWTs payload.sub, From 91cdb88f0c77e4e9d1e7c2a4c9f0b3bdbe8fa348 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 26 Aug 2026 17:19:09 -0400 Subject: [PATCH 6/9] toMatchInlineSnapshot --- .../src/tokens/__tests__/verify.test.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/tokens/__tests__/verify.test.ts b/packages/backend/src/tokens/__tests__/verify.test.ts index 3cf4ea44c81..03670182711 100644 --- a/packages/backend/src/tokens/__tests__/verify.test.ts +++ b/packages/backend/src/tokens/__tests__/verify.test.ts @@ -593,7 +593,24 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { }); expect(result.tokenType).toBe('oauth_token'); - expect(result.data).toBeDefined(); + expect(result.data).toMatchInlineSnapshot(` + IdPOAuthAccessToken { + "clientId": "client_2VTWUzvGC5UhdJCNx6xG1D98edc", + "createdAt": 1666648250000, + "expiration": 1666648550000, + "expired": false, + "id": "oat_2xKa9Bgv7NxMRDFyQw8LpZ3cTmU1vHjE", + "revocationReason": null, + "revoked": false, + "scopes": [ + "read:foo", + "write:bar", + ], + "subject": "user_2vYVtestTESTtestTESTtestTESTtest", + "type": "oauth_token", + "updatedAt": 1666648250000, + } + `); expect(result.errors).toBeUndefined(); }); @@ -620,8 +637,10 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { expect(result.tokenType).toBe('oauth_token'); expect(result.data).toBeUndefined(); - expect(result.errors).toBeDefined(); - expect(result.errors?.[0].message).toContain('Invalid JWT audience claim'); + expect(result.errors).toHaveLength(1); + expect(result.errors![0]).toMatchInlineSnapshot( + `[MachineTokenVerificationError: Invalid JWT audience claim (aud) "https://attacker.example.com". Is not included in "["https://my-resource.example.com"]".]`, + ); }); }); From b40da6d1435532f3acd17d31a7c9efb90ecf6d1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 21:33:17 +0000 Subject: [PATCH 7/9] chore(ci): retrigger unit tests after unrelated checkout timeout From 0d3bfad73d92cbbe2cbd7a9abbe95163abcb5009 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 26 Aug 2026 17:44:49 -0400 Subject: [PATCH 8/9] Ensure aud is in IdPOAuthAccessToken --- .changeset/type-jwt-aud-rfc-8707.md | 4 ++-- .../backend/src/api/resources/IdPOAuthAccessToken.ts | 11 +++++++---- packages/backend/src/tokens/__tests__/verify.test.ts | 2 ++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.changeset/type-jwt-aud-rfc-8707.md b/.changeset/type-jwt-aud-rfc-8707.md index d132f87aaea..e9db961804b 100644 --- a/.changeset/type-jwt-aud-rfc-8707.md +++ b/.changeset/type-jwt-aud-rfc-8707.md @@ -1,6 +1,6 @@ --- '@clerk/shared': minor -'@clerk/backend': patch +'@clerk/backend': minor --- -Type the JWT `aud` claim as an optional `string | string[]` per RFC 7519. Clerk-issued OAuth access tokens may include a single RFC 8707 resource URI as a string. +Type the JWT `aud` claim as an optional `string | string[]` per RFC 7519 and expose it on verified OAuth JWT access tokens. Clerk-issued OAuth access tokens may include a single RFC 8707 resource URI as a string. diff --git a/packages/backend/src/api/resources/IdPOAuthAccessToken.ts b/packages/backend/src/api/resources/IdPOAuthAccessToken.ts index 8399057e59b..286e6c2bfaf 100644 --- a/packages/backend/src/api/resources/IdPOAuthAccessToken.ts +++ b/packages/backend/src/api/resources/IdPOAuthAccessToken.ts @@ -25,6 +25,8 @@ export class IdPOAuthAccessToken { readonly createdAt: number, /** The Unix timestamp (in milliseconds) when the access token was last updated. */ readonly updatedAt: number, + /** The intended audience for the access token. */ + readonly aud?: string | string[], ) {} static fromJSON(data: IdPOAuthAccessTokenJSON) { @@ -59,10 +61,11 @@ export class IdPOAuthAccessToken { oauthPayload.scp ?? oauthPayload.scope?.split(' ') ?? [], false, null, - payload.exp * 1000 <= Date.now() - clockSkewInMs, - payload.exp * 1000, // milliseconds: expiration, converted from JWT exp claim - payload.iat * 1000, // milliseconds: createdAt, converted from JWT iat claim - payload.iat * 1000, // milliseconds: updatedAt, no JWT equivalent, defaults to iat + oauthPayload.exp * 1000 <= Date.now() - clockSkewInMs, + oauthPayload.exp * 1000, // milliseconds: expiration, converted from JWT exp claim + oauthPayload.iat * 1000, // milliseconds: createdAt, converted from JWT iat claim + oauthPayload.iat * 1000, // milliseconds: updatedAt, no JWT equivalent, defaults to iat + oauthPayload.aud, ); } } diff --git a/packages/backend/src/tokens/__tests__/verify.test.ts b/packages/backend/src/tokens/__tests__/verify.test.ts index 03670182711..603b858a090 100644 --- a/packages/backend/src/tokens/__tests__/verify.test.ts +++ b/packages/backend/src/tokens/__tests__/verify.test.ts @@ -595,6 +595,7 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { expect(result.tokenType).toBe('oauth_token'); expect(result.data).toMatchInlineSnapshot(` IdPOAuthAccessToken { + "aud": "https://my-resource.example.com", "clientId": "client_2VTWUzvGC5UhdJCNx6xG1D98edc", "createdAt": 1666648250000, "expiration": 1666648550000, @@ -611,6 +612,7 @@ describe('tokens.verifyMachineAuthToken(token, options)', () => { "updatedAt": 1666648250000, } `); + expect((result.data as IdPOAuthAccessToken).aud).toBe(audience); expect(result.errors).toBeUndefined(); }); From 8c62389e12f2754b5dfcd0bcbd3796315de92a2c Mon Sep 17 00:00:00 2001 From: thiskevinwang Date: Wed, 26 Aug 2026 21:50:28 +0000 Subject: [PATCH 9/9] fix(backend): preserve aud when hydrating IdPOAuthAccessToken from JSON Co-authored-by: Codesmith --- packages/backend/src/api/resources/IdPOAuthAccessToken.ts | 1 + packages/backend/src/api/resources/JSON.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/backend/src/api/resources/IdPOAuthAccessToken.ts b/packages/backend/src/api/resources/IdPOAuthAccessToken.ts index 286e6c2bfaf..71cc9361b4c 100644 --- a/packages/backend/src/api/resources/IdPOAuthAccessToken.ts +++ b/packages/backend/src/api/resources/IdPOAuthAccessToken.ts @@ -42,6 +42,7 @@ export class IdPOAuthAccessToken { data.expiration, data.created_at, data.updated_at, + data.aud, ); } diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index e1ff98e2ee1..69ee1e82b4d 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -962,6 +962,7 @@ export interface IdPOAuthAccessTokenJSON extends ClerkResourceJSON { expiration: number | null; created_at: number; updated_at: number; + aud?: string | string[]; } export interface BillingPayerJSON extends ClerkResourceJSON {