Skip to content

Commit ee25cf2

Browse files
jescalanwobsoriano
andauthored
fix(backend): Fix JWT array audience validation (#8470)
Co-authored-by: Robert Soriano <sorianorobertc@gmail.com>
1 parent 2dda5b1 commit ee25cf2

4 files changed

Lines changed: 59 additions & 1 deletion

File tree

.changeset/seven-shoes-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/backend": patch
3+
---
4+
5+
Fix JWT array audience validation

packages/backend/src/jwt/__tests__/assertions.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ describe('assertAudienceClaim(audience?, aud?)', () => {
9393
);
9494
});
9595

96+
it('throws error when audience string[] has no intersection with aud string[]', () => {
97+
expect(() => assertAudienceClaim([audience], [invalidAudience])).toThrow(
98+
`Invalid JWT audience claim array (aud) ${JSON.stringify([audience])}. Is not included in "${JSON.stringify([invalidAudience])}".`,
99+
);
100+
});
101+
96102
it('throws error when aud is a substring of audience', () => {
97103
expect(() => assertAudienceClaim(audience.slice(0, -2), audience)).toThrow(
98104
`Invalid JWT audience claim (aud) "${audience.slice(0, -2)}". Is not included in "${JSON.stringify([audience])}".`,

packages/backend/src/jwt/__tests__/verifyJwt.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import {
66
mockJwt,
77
mockJwtHeader,
88
mockJwtPayload,
9+
mockM2MJwtPayload,
910
mockOAuthAccessTokenJwtPayload,
1011
pemEncodedPublicKey,
12+
pemEncodedSignKey,
1113
publicJwks,
1214
signedJwt,
1315
someOtherPublicKey,
1416
} from '../../fixtures';
1517
import { mockSignedOAuthAccessTokenJwt, mockSignedOAuthAccessTokenJwtApplicationTyp } from '../../fixtures/machine';
18+
import { signJwt } from '../signJwt';
1619
import { decodeJwt, hasValidSignature, verifyJwt } from '../verifyJwt';
1720

1821
const invalidTokenError = {
@@ -234,6 +237,50 @@ describe('verifyJwt(jwt, options)', () => {
234237
expect(error?.message).toContain('Expected "at+jwt, application/at+jwt"');
235238
});
236239

240+
it('verifies JWT when array aud includes the configured audience', async () => {
241+
const audience = 'https://my-resource.example.com';
242+
const { data: jwtWithArrayAud } = await signJwt(
243+
{
244+
...mockM2MJwtPayload,
245+
aud: ['https://other-resource.example.com', audience],
246+
},
247+
pemEncodedSignKey,
248+
{
249+
algorithm: mockJwtHeader.alg,
250+
header: mockJwtHeader,
251+
},
252+
);
253+
254+
const { data } = await verifyJwt(jwtWithArrayAud || '', {
255+
key: pemEncodedPublicKey,
256+
audience,
257+
});
258+
259+
expect(data?.aud).toEqual(['https://other-resource.example.com', audience]);
260+
});
261+
262+
it('rejects JWT when array aud does not include the configured audience', async () => {
263+
const { data: jwtWithArrayAud } = await signJwt(
264+
{
265+
...mockM2MJwtPayload,
266+
aud: ['https://attacker.example.com'],
267+
},
268+
pemEncodedSignKey,
269+
{
270+
algorithm: mockJwtHeader.alg,
271+
header: mockJwtHeader,
272+
},
273+
);
274+
275+
const { errors: [error] = [] } = await verifyJwt(jwtWithArrayAud || '', {
276+
key: pemEncodedPublicKey,
277+
audience: 'https://my-resource.example.com',
278+
});
279+
280+
expect(error).toBeDefined();
281+
expect(error?.message).toContain('Invalid JWT audience claim array');
282+
});
283+
237284
it('rejects an expired JWT when clockSkewInMs is explicitly 0', async () => {
238285
vi.setSystemTime(new Date((mockJwtPayload.exp + 1) * 1000));
239286
const inputVerifyJwtOptions = {

packages/backend/src/jwt/verifyJwt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export async function verifyJwt(
181181
const { azp, sub, aud, iat, exp, nbf } = payload;
182182

183183
assertSubClaim(sub);
184-
assertAudienceClaim([aud], [audience]);
184+
assertAudienceClaim(aud, audience);
185185
assertAuthorizedPartiesClaim(azp, authorizedParties);
186186
assertExpirationClaim(exp, clockSkew);
187187
assertActivationClaim(nbf, clockSkew);

0 commit comments

Comments
 (0)