Skip to content

Make audience checking optional in JWT verification as per RFC7519 4.1.3 #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/access-token-jwt/src/jwt-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export interface JwtVerifierOptions {

/**
* Expected JWT "aud" (Audience) Claim value(s).
* REQUIRED: You can also provide the `AUDIENCE` environment variable.
* Optional: You can also provide the `AUDIENCE` environment variable.
* If not provided, no audience validation will be performed, which aligns with RFC7519 section 4.1.3
* where the "aud" claim is optional.
*/
audience?: string | string[];

Expand Down Expand Up @@ -186,7 +188,6 @@ const jwtVerifier = ({
!(secret && jwksUri),
"You must not provide both a 'secret' and 'jwksUri'"
);
assert(audience, "An 'audience' is required to validate the 'aud' claim");
assert(
!secret || (secret && tokenSigningAlg),
"You must provide a 'tokenSigningAlg' for validating symmetric algorithms"
Expand Down
10 changes: 10 additions & 0 deletions packages/access-token-jwt/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ export const defaultValidators = (
typ.toLowerCase().replace(/^application\//, '') === 'at+jwt'),
iss: (iss) => iss === issuer,
aud: (aud) => {
// If no audience is specified in configuration, skip validation (as per RFC7519 section 4.1.3)
if (!audience) {
return true;
}

// If audience is specified but not present in token, fail validation
if (aud === undefined) {
return false;
}

audience = typeof audience === 'string' ? [audience] : audience;
if (typeof aud === 'string') {
return audience.includes(aud);
Expand Down
63 changes: 61 additions & 2 deletions packages/access-token-jwt/test/jwt-verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ describe('jwt-verifier', () => {
);
});

it('should throw when configured with no audience', async () => {
it('should NOT throw when configured with no audience (since audience is optional)', async () => {
expect(() =>
jwtVerifier({
jwksUri: 'https://issuer.example.com/.well-known/jwks.json',
issuer: 'https://issuer.example.com/',
})
).toThrowError("An 'audience' is required to validate the 'aud' claim");
).not.toThrow();
});

it('should throw when configured with secret and no token signing alg', async () => {
Expand Down Expand Up @@ -108,6 +108,65 @@ describe('jwt-verifier', () => {
await expect(verify(jwt)).rejects.toThrowError(`Unexpected 'aud' value`);
});

it('should accept jwt with no audience when audience is not specified', async () => {
// Create JWT with no audience claim
const jwt = await createJwt({
audience: undefined, // This will omit the audience claim from the JWT
});

const verify = jwtVerifier({
jwksUri: 'https://issuer.example.com/.well-known/jwks.json',
issuer: 'https://issuer.example.com/',
// No audience specified
});
await expect(verify(jwt)).resolves.toHaveProperty('payload', {
iss: 'https://issuer.example.com/',
sub: 'me',
// No aud claim
iat: expect.any(Number),
exp: expect.any(Number),
});
});

it('should reject jwt with no audience when audience is specified', async () => {
// Create JWT with no audience claim
const jwt = await createJwt({
audience: undefined, // This will omit the audience claim from the JWT
});

const verify = jwtVerifier({
jwksUri: 'https://issuer.example.com/.well-known/jwks.json',
issuer: 'https://issuer.example.com/',
audience: 'https://api/', // Audience is specified
});
await expect(verify(jwt)).rejects.toThrowError(`Unexpected 'aud' value`);
});

const verify = jwtVerifier({
jwksUri: 'https://issuer.example.com/.well-known/jwks.json',
issuer: 'https://issuer.example.com/',
// No audience parameter here
});

await expect(verify(jwt)).resolves.toBeTruthy();
});

it('should reject jwt with no audience when audience is specified', async () => {
// Create JWT with no audience claim
const jwt = await createJwt({
audience: undefined, // This will omit the audience claim from the JWT
});

const verify = jwtVerifier({
jwksUri: 'https://issuer.example.com/.well-known/jwks.json',
issuer: 'https://issuer.example.com/',
audience: 'https://api/',
});

// This should fail since the JWT has no audience but config requires one
await expect(verify(jwt)).rejects.toThrowError(`Unexpected 'aud' value`);
});

it('should throw for an expired token', async () => {
const jwt = await createJwt({
exp: now - 10,
Expand Down
20 changes: 20 additions & 0 deletions packages/access-token-jwt/test/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,26 @@ describe('validate', () => {
)
).resolves.not.toThrow();
});

it('should accept missing aud claim when no audience is configured', async () => {
await expect(
validate(
{ ...payload, aud: undefined },
header,
validators({ audience: undefined })
)
).resolves.not.toThrow();
});

it('should reject missing aud claim when audience is configured', async () => {
await expect(
validate(
{ ...payload, aud: undefined },
header,
validators({ audience: 'foo' })
)
).rejects.toThrow(`Unexpected 'aud' value`);
});
it('should throw for invalid exp claim', async () => {
const clock = sinon.useFakeTimers(100 * 1000);
await expect(
Expand Down
5 changes: 3 additions & 2 deletions packages/express-oauth2-jwt-bearer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ declare global {
* }));
* ```
*
* You must provide the `audience` argument (or `AUDIENCE` environment variable)
* used to match against the Access Token's `aud` claim.
* You can optionally provide the `audience` argument (or `AUDIENCE` environment variable)
* used to match against the Access Token's `aud` claim. If not provided, audience validation
* will be skipped which aligns with RFC7519 section 4.1.3 where the "aud" claim is optional.
*
* Successful requests will have the following properties added to them:
*
Expand Down
5 changes: 2 additions & 3 deletions packages/express-oauth2-jwt-bearer/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,8 @@ describe('index', () => {
process.env = Object.assign({}, env, {
ISSUER_BASE_URL: 'foo',
});
expect(auth).toThrow(
"An 'audience' is required to validate the 'aud' claim"
);
// No longer requiring audience since we made it optional
expect(auth).not.toThrow();
process.env = Object.assign({}, env, {
ISSUER_BASE_URL: 'foo',
AUDIENCE: 'baz',
Expand Down
Loading