Skip to content

Commit

Permalink
feat: add requiredClaims JWT validation option
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Apr 14, 2023
1 parent 3108ade commit eeea91d
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 7 deletions.
23 changes: 16 additions & 7 deletions src/lib/jwt_claims_set.ts
Expand Up @@ -51,17 +51,27 @@ export default (
throw new JWTInvalid('JWT Claims Set must be a top-level JSON object')
}

const { issuer } = options
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options

if (maxTokenAge !== undefined) requiredClaims.push('iat')
if (audience !== undefined) requiredClaims.push('aud')
if (subject !== undefined) requiredClaims.push('sub')
if (issuer !== undefined) requiredClaims.push('iss')

for (const claim of new Set(requiredClaims.reverse())) {
if (!(claim in payload)) {
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, claim, 'missing')
}
}

if (issuer && !(<unknown[]>(Array.isArray(issuer) ? issuer : [issuer])).includes(payload.iss!)) {
throw new JWTClaimValidationFailed('unexpected "iss" claim value', 'iss', 'check_failed')
}

const { subject } = options
if (subject && payload.sub !== subject) {
throw new JWTClaimValidationFailed('unexpected "sub" claim value', 'sub', 'check_failed')
}

const { audience } = options
if (
audience &&
!checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)
Expand All @@ -87,7 +97,7 @@ export default (
const { currentDate } = options
const now = epoch(currentDate || new Date())

if ((payload.iat !== undefined || options.maxTokenAge) && typeof payload.iat !== 'number') {
if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {
throw new JWTClaimValidationFailed('"iat" claim must be a number', 'iat', 'invalid')
}

Expand All @@ -113,10 +123,9 @@ export default (
}
}

if (options.maxTokenAge) {
if (maxTokenAge) {
const age = now - payload.iat!
const max =
typeof options.maxTokenAge === 'number' ? options.maxTokenAge : secs(options.maxTokenAge)
const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge)

if (age - tolerance > max) {
throw new JWTExpired(
Expand Down
10 changes: 10 additions & 0 deletions src/types.d.ts
Expand Up @@ -457,6 +457,16 @@ export interface JWTClaimVerificationOptions {

/** Date to use when comparing NumericDate claims, defaults to `new Date()`. */
currentDate?: Date

/**
* Array of required Claim Names that must be present in the JWT Claims Set. Default is that: if
* the {@link JWTClaimVerificationOptions.issuer issuer option} is set, then "iss" must be present;
* if the {@link JWTClaimVerificationOptions.audience audience option} is set, then "aud" must be
* present; if the {@link JWTClaimVerificationOptions.subject subject option} is set, then "sub"
* must be present; if the {@link JWTClaimVerificationOptions.maxTokenAge maxTokenAge option} is
* set, then "iat" must be present.
*/
requiredClaims?: string[]
}

/** JWS Verification options. */
Expand Down
31 changes: 31 additions & 0 deletions test/jwt/verify.test.mjs
Expand Up @@ -410,3 +410,34 @@ test('signatures are compared before claim set', async (t) => {
code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
})
})

test('requiredClaims claims check', async (t) => {
const jwt = await new SignJWT({
...t.context.payload,
})
.setProtectedHeader({ alg: 'HS256' })
.sign(t.context.secret)

for (const [claim, option] of [
['iss', 'issuer'],
['aud', 'audience'],
['iat', 'maxTokenAge'],
['sub', 'subject'],
]) {
await t.throwsAsync(jwtVerify(jwt, t.context.secret, { [option]: 'foo' }), {
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: `missing required "${claim}" claim`,
})
await t.throwsAsync(
jwtVerify(jwt, t.context.secret, { [option]: 'foo', requiredClaims: ['nbf'] }),
{
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: `missing required "${claim}" claim`,
},
)
}
await t.throwsAsync(jwtVerify(jwt, t.context.secret, { requiredClaims: ['nbf'] }), {
code: 'ERR_JWT_CLAIM_VALIDATION_FAILED',
message: `missing required "nbf" claim`,
})
})

0 comments on commit eeea91d

Please sign in to comment.