Skip to content
Merged
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
14 changes: 13 additions & 1 deletion src/verifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ function ensureStringClaimMatcher(raw) {
return raw
.filter(r => r)
.map(r => {
if (r instanceof RegExp) {
return {
test: v => {
r.lastIndex = 0
return r.test(v)
}
}
}

if (r && typeof r.test === 'function') {
return r
}
Expand Down Expand Up @@ -510,7 +519,10 @@ module.exports = function createVerifier(options) {
throw new TokenError(TokenError.codes.invalidOption, 'The requiredClaims option must be an array.')
}

if (allowedCritHeaders !== undefined && !Array.isArray(allowedCritHeaders)) {
if (
allowedCritHeaders !== undefined &&
(!Array.isArray(allowedCritHeaders) || allowedCritHeaders.some(h => typeof h !== 'string' || h.length === 0))
) {
throw new TokenError(TokenError.codes.invalidOption, 'The allowedCritHeaders option must be an array of strings.')
}

Expand Down
74 changes: 74 additions & 0 deletions test/verifier.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,73 @@ test('default errorCacheTTL should not cache errors when sub millisecond executi
t.mock.timers.reset()
})

test('stateful RegExp /g flag must not cause non-deterministic claim validation - allowedAud', t => {
t.mock.timers.enable({ now: 100000 })
const sign = createSigner({ key: 'secret' })
const token = sign({ aud: 'admin', iss: 'issuer', sub: 'subject', jti: 'id-123', nonce: 'nonce-xyz' })
const verifier = createVerifier({ key: 'secret', allowedAud: /^admin$/g })

// All 8 successive calls with the same valid token must succeed
for (let i = 0; i < 8; i++) {
t.assert.doesNotThrow(() => verifier(token), `call ${i} should pass with /g flag on allowedAud`)
}
})

test('stateful RegExp /y flag must not cause non-deterministic claim validation - allowedAud', t => {
t.mock.timers.enable({ now: 100000 })
const sign = createSigner({ key: 'secret' })
const token = sign({ aud: 'admin', iss: 'issuer', sub: 'subject', jti: 'id-123', nonce: 'nonce-xyz' })
const verifier = createVerifier({ key: 'secret', allowedAud: /^admin$/y })

for (let i = 0; i < 8; i++) {
t.assert.doesNotThrow(() => verifier(token), `call ${i} should pass with /y flag on allowedAud`)
}
})

test('stateful RegExp /g flag must not cause non-deterministic claim validation - allowedIss', t => {
t.mock.timers.enable({ now: 100000 })
const sign = createSigner({ key: 'secret' })
const token = sign({ aud: 'admin', iss: 'issuer', sub: 'subject', jti: 'id-123', nonce: 'nonce-xyz' })
const verifier = createVerifier({ key: 'secret', allowedIss: /^issuer$/g })

for (let i = 0; i < 8; i++) {
t.assert.doesNotThrow(() => verifier(token), `call ${i} should pass with /g flag on allowedIss`)
}
})

test('stateful RegExp /g flag must not cause non-deterministic claim validation - allowedSub', t => {
t.mock.timers.enable({ now: 100000 })
const sign = createSigner({ key: 'secret' })
const token = sign({ aud: 'admin', iss: 'issuer', sub: 'subject', jti: 'id-123', nonce: 'nonce-xyz' })
const verifier = createVerifier({ key: 'secret', allowedSub: /^subject$/g })

for (let i = 0; i < 8; i++) {
t.assert.doesNotThrow(() => verifier(token), `call ${i} should pass with /g flag on allowedSub`)
}
})

test('stateful RegExp /g flag must not cause non-deterministic claim validation - allowedJti', t => {
t.mock.timers.enable({ now: 100000 })
const sign = createSigner({ key: 'secret' })
const token = sign({ aud: 'admin', iss: 'issuer', sub: 'subject', jti: 'id-123', nonce: 'nonce-xyz' })
const verifier = createVerifier({ key: 'secret', allowedJti: /^id-123$/g })

for (let i = 0; i < 8; i++) {
t.assert.doesNotThrow(() => verifier(token), `call ${i} should pass with /g flag on allowedJti`)
}
})

test('stateful RegExp /g flag must not cause non-deterministic claim validation - allowedNonce', t => {
t.mock.timers.enable({ now: 100000 })
const sign = createSigner({ key: 'secret' })
const token = sign({ aud: 'admin', iss: 'issuer', sub: 'subject', jti: 'id-123', nonce: 'nonce-xyz' })
const verifier = createVerifier({ key: 'secret', allowedNonce: /^nonce-xyz$/g })

for (let i = 0; i < 8; i++) {
t.assert.doesNotThrow(() => verifier(token), `call ${i} should pass with /g flag on allowedNonce`)
}
})

// --- crit header validation (RFC 7515 §4.1.11) ---

test('crit: rejects token with unknown critical extension (secure-by-default, no allowedCritHeaders)', t => {
Expand Down Expand Up @@ -1874,3 +1941,10 @@ test('crit: throws on invalid allowedCritHeaders option (not an array)', t => {
message: 'The allowedCritHeaders option must be an array of strings.'
})
})

test('crit: throws on invalid allowedCritHeaders option (empty string)', t => {
t.assert.throws(() => createVerifier({ key: 'secret', allowedCritHeaders: [''] }), {
code: 'FAST_JWT_INVALID_OPTION',
message: 'The allowedCritHeaders option must be an array of strings.'
})
})
Loading