jose/test/jwt/verify.test.js
Filip Skokan fd69d7f509 refactor: move JWT profile specifics outside of generic JWT
BREAKING CHANGE: the `JWT.verify` profile option was removed, use e.g.
`JWT.IdToken.verify` instead.

BREAKING CHANGE: removed the `maxAuthAge` `JWT.verify` option, this
option is now only present at the specific JWT profile APIs where the
`auth_time` property applies.

BREAKING CHANGE: removed the `nonce` `JWT.verify` option, this
option is now only present at the specific JWT profile APIs where the
`nonce` property applies.

BREAKING CHANGE: the `acr`, `amr`, `nonce` and `azp` claim value types
will only be checked when verifying a specific JWT profile using its
dedicated API.

BREAKING CHANGE: using the draft implementing APIs will emit a one-time
warning per process using `process.emitWarning`
2020-09-08 14:12:04 +02:00

937 lines
36 KiB
JavaScript

const test = require('ava')
const { JWS, JWT, JWK, JWKS, errors } = require('../..')
const key = JWK.generateSync('oct')
const token = JWT.sign({}, key, { iat: false })
const string = (t, option, method = JWT.verify, opts) => {
;['', false, [], {}, Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
method(token, key, { ...opts, [option]: val })
}, { instanceOf: TypeError, message: `options.${option} must be a string` })
})
}
test('options.complete true', t => {
const token = JWT.sign({}, key)
const completeResult = JWT.verify(token, key, { complete: true })
t.is(completeResult.key, key)
})
test('options.complete with KeyStore', t => {
const ks = new JWKS.KeyStore(JWK.generateSync('oct'), key)
const token = JWT.sign({}, key, { kid: false })
const completeResult = JWT.verify(token, ks, { complete: true })
t.is(completeResult.key, key)
})
test('options must be an object', t => {
;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, val)
}, { instanceOf: TypeError, message: 'options must be an object' })
})
})
test('options.clockTolerance must be a string', string, 'clockTolerance')
test('options.jti must be a string', string, 'jti')
test('options.maxTokenAge must be a string', string, 'maxTokenAge')
test('options.subject must be a string', string, 'subject')
const boolean = (t, option) => {
;['', 'foo', [], {}, Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, { [option]: val })
}, { instanceOf: TypeError, message: `options.${option} must be a boolean` })
})
}
test('options.complete must be boolean', boolean, 'complete')
test('options.ignoreExp must be boolean', boolean, 'ignoreExp')
test('options.ignoreNbf must be boolean', boolean, 'ignoreNbf')
test('options.ignoreIat must be boolean', boolean, 'ignoreIat')
test('options.issuer must be string or array of strings', t => {
;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, { issuer: val })
}, { instanceOf: TypeError, message: 'options.issuer must be a string or an array of strings' })
t.throws(() => {
JWT.verify(token, key, { issuer: [val] })
}, { instanceOf: TypeError, message: 'options.issuer must be a string or an array of strings' })
})
})
test('options.audience must be string or array of strings', t => {
;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, { audience: val })
}, { instanceOf: TypeError, message: 'options.audience must be a string or an array of strings' })
t.throws(() => {
JWT.verify(token, key, { audience: [val] })
}, { instanceOf: TypeError, message: 'options.audience must be a string or an array of strings' })
})
})
test('options.algorithms must be string or array of strings', t => {
;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, { algorithms: val })
}, { instanceOf: TypeError, message: 'options.algorithms must be an array of strings' })
t.throws(() => {
JWT.verify(token, key, { algorithms: [val] })
}, { instanceOf: TypeError, message: 'options.algorithms must be an array of strings' })
})
})
test('options.crit must be string or array of strings', t => {
;['', false, [], Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, { crit: val })
}, { instanceOf: TypeError, message: 'options.crit must be an array of strings' })
t.throws(() => {
JWT.verify(token, key, { crit: [val] })
}, { instanceOf: TypeError, message: 'options.crit must be an array of strings' })
})
})
test('options.now must be a valid date', t => {
;['', 'foo', [], {}, Buffer, Buffer.from('foo'), 0, Infinity, new Date('foo')].forEach((val) => {
t.throws(() => {
JWT.verify(token, key, { now: val })
}, { instanceOf: TypeError, message: 'options.now must be a valid Date object' })
})
})
test('options.ignoreIat & options.maxTokenAge may not be used together', t => {
t.throws(() => {
JWT.verify(token, key, { ignoreIat: true, maxTokenAge: '2d' })
}, { instanceOf: TypeError, message: 'options.ignoreIat and options.maxTokenAge cannot used together' })
})
;['iat', 'exp', 'nbf'].forEach((claim) => {
test(`"${claim} must be a timestamp when provided"`, t => {
;['', 'foo', true, null, [], {}].forEach((val) => {
const err = t.throws(() => {
const invalid = JWS.sign({ [claim]: val }, key)
JWT.verify(invalid, key)
}, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a JSON numeric value` })
t.is(err.claim, claim)
t.is(err.reason, 'invalid')
})
})
})
;['jti', 'sub'].forEach((claim) => {
test(`"${claim} must be a string when provided"`, t => {
;['', 0, 1, true, null, [], {}].forEach((val) => {
const err = t.throws(() => {
const invalid = JWS.sign({ [claim]: val }, key)
JWT.verify(invalid, key)
}, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a string` })
t.is(err.claim, claim)
t.is(err.reason, 'invalid')
})
})
})
;['aud', 'iss'].forEach((claim) => {
test(`"${claim} must be a string when provided"`, t => {
;['', 0, 1, true, null, [], {}].forEach((val) => {
let err
err = t.throws(() => {
const invalid = JWS.sign({ [claim]: val }, key)
JWT.verify(invalid, key)
}, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a string or array of strings` })
t.is(err.claim, claim)
t.is(err.reason, 'invalid')
err = t.throws(() => {
const invalid = JWS.sign({ [claim]: [val] }, key)
JWT.verify(invalid, key)
}, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim must be a string or array of strings` })
t.is(err.claim, claim)
t.is(err.reason, 'invalid')
})
})
})
Object.entries({
issuer: 'iss',
jti: 'jti',
subject: 'sub'
}).forEach(([option, claim]) => {
test(`option.${option} validation fails`, t => {
let err
err = t.throws(() => {
const invalid = JWS.sign({ [claim]: 'foo' }, key)
JWT.verify(invalid, key, { [option]: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: `unexpected "${claim}" claim value` })
t.is(err.claim, claim)
t.is(err.reason, 'check_failed')
err = t.throws(() => {
const invalid = JWS.sign({ [claim]: undefined }, key)
JWT.verify(invalid, key, { [option]: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: `"${claim}" claim is missing` })
t.is(err.claim, claim)
t.is(err.reason, 'missing')
})
test(`option.${option} validation success`, t => {
const token = JWT.sign({ [claim]: 'foo' }, key)
JWT.verify(token, key, { [option]: 'foo' })
t.pass()
})
})
test('option.typ validation fails', t => {
let err
err = t.throws(() => {
const invalid = JWT.sign({}, key, { header: { typ: 'foo' } })
JWT.verify(invalid, key, { typ: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "typ" JWT header value' })
t.is(err.claim, 'typ')
t.is(err.reason, 'check_failed')
err = t.throws(() => {
const invalid = JWS.sign({}, key, { header: { typ: undefined } })
JWT.verify(invalid, key, { typ: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"typ" header parameter is missing' })
t.is(err.claim, 'typ')
t.is(err.reason, 'missing')
})
test('option.typ validation success', t => {
{
const token = JWT.sign({}, key, { header: { typ: 'foo' } })
JWT.verify(token, key, { typ: 'foo' })
}
{
const token = JWT.sign({}, key, { header: { typ: 'application/foo' } })
JWT.verify(token, key, { typ: 'foo' })
}
{
const token = JWT.sign({}, key, { header: { typ: 'foo' } })
JWT.verify(token, key, { typ: 'application/foo' })
}
{
const token = JWT.sign({}, key, { header: { typ: 'foO' } })
JWT.verify(token, key, { typ: 'application/foo' })
}
{
const token = JWT.sign({}, key, { header: { typ: 'application/foo' } })
JWT.verify(token, key, { typ: 'fOo' })
}
t.pass()
})
test('option.audience validation fails', t => {
let err
err = t.throws(() => {
const invalid = JWS.sign({ aud: 'foo' }, key)
JWT.verify(invalid, key, { audience: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "aud" claim value' })
t.is(err.claim, 'aud')
t.is(err.reason, 'check_failed')
err = t.throws(() => {
const invalid = JWS.sign({ aud: ['foo'] }, key)
JWT.verify(invalid, key, { audience: 'bar' })
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "aud" claim value' })
t.is(err.claim, 'aud')
t.is(err.reason, 'check_failed')
})
test('option.audience validation success', t => {
let token = JWT.sign({ aud: 'foo' }, key)
JWT.verify(token, key, { audience: 'foo' })
token = JWT.sign({ aud: 'foo' }, key)
JWT.verify(token, key, { audience: ['foo'] })
token = JWT.sign({ aud: 'foo' }, key)
JWT.verify(token, key, { audience: ['foo', 'bar'] })
token = JWT.sign({ aud: ['foo', 'bar'] }, key)
JWT.verify(token, key, { audience: 'foo' })
token = JWT.sign({ aud: ['foo', 'bar'] }, key)
JWT.verify(token, key, { audience: ['foo'] })
token = JWT.sign({ aud: ['foo', 'bar'] }, key)
JWT.verify(token, key, { audience: ['foo', 'bar'] })
t.pass()
})
const epoch = 1265328501
const now = new Date(epoch * 1000)
test('option.maxTokenAge requires iat to be in the payload', t => {
const err = t.throws(() => {
const invalid = JWS.sign({}, key)
JWT.verify(invalid, key, { maxTokenAge: '30s' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
test('option.maxTokenAge checks iat elapsed time', t => {
const err = t.throws(() => {
const invalid = JWS.sign({ iat: epoch - 31 }, key)
JWT.verify(invalid, key, { maxTokenAge: '30s', now })
}, { instanceOf: errors.JWTExpired, code: 'ERR_JWT_EXPIRED', message: '"iat" claim timestamp check failed (too far in the past)' })
t.true(err instanceof errors.JWTClaimInvalid)
t.is(err.claim, 'iat')
t.is(err.reason, 'check_failed')
})
test('option.maxTokenAge checks iat (with tolerance)', t => {
const token = JWT.sign({ iat: epoch - 31 }, key, { now })
JWT.verify(token, key, { maxTokenAge: '30s', now, clockTolerance: '1s' })
t.pass()
})
test('iat check (pass)', t => {
const token = JWT.sign({ iat: epoch - 30 }, key, { iat: false })
JWT.verify(token, key, { now })
t.pass()
})
test('iat check (pass equal)', t => {
const token = JWT.sign({}, key, { now })
JWT.verify(token, key, { now })
t.pass()
})
test('iat check (pass with tolerance)', t => {
const token = JWT.sign({}, key, { now: new Date((epoch + 1) * 1000) })
JWT.verify(token, key, { now, clockTolerance: '1s' })
t.pass()
})
test('iat check (failed)', t => {
const token = JWT.sign({}, key, { now: new Date((epoch + 1) * 1000) })
const err = t.throws(() => {
JWT.verify(token, key, { now })
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim timestamp check failed (it should be in the past)' })
t.is(err.claim, 'iat')
t.is(err.reason, 'check_failed')
})
test('iat future check (ignored since exp is also present)', t => {
const token = JWT.sign({}, key, { now: new Date((epoch + 1) * 1000), expiresIn: '2h' })
JWT.verify(token, key, { now })
t.pass()
})
test('iat future check (part of maxTokenAge)', t => {
const token = JWT.sign({}, key, { now: new Date((epoch + 1) * 1000), expiresIn: '2h' })
const err = t.throws(() => {
JWT.verify(token, key, { now, maxTokenAge: '30s' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim timestamp check failed (it should be in the past)' })
t.is(err.claim, 'iat')
t.is(err.reason, 'check_failed')
})
test('iat future check with tolerance (part of maxTokenAge)', t => {
const token = JWT.sign({}, key, { now: new Date((epoch + 1) * 1000), expiresIn: '2h' })
JWT.verify(token, key, { now, maxTokenAge: '30s', clockTolerance: '1s' })
t.pass()
})
test('iat check (passed because of ignoreIat)', t => {
const token = JWT.sign({}, key, { now: new Date((epoch + 1) * 1000) })
JWT.verify(token, key, { now, ignoreIat: true })
t.pass()
})
test('exp check (pass)', t => {
const token = JWT.sign({ exp: epoch + 30 }, key, { iat: false })
JWT.verify(token, key, { now })
t.pass()
})
test('exp check (pass equal - 1)', t => {
const token = JWT.sign({ exp: epoch + 1 }, key, { iat: false })
JWT.verify(token, key, { now })
t.pass()
})
test('exp check (pass with tolerance)', t => {
const token = JWT.sign({ exp: epoch }, key, { iat: false })
JWT.verify(token, key, { now, clockTolerance: '1s' })
t.pass()
})
test('exp check (failed equal)', t => {
const token = JWT.sign({ exp: epoch }, key, { iat: false })
const err = t.throws(() => {
JWT.verify(token, key, { now })
}, { instanceOf: errors.JWTExpired, code: 'ERR_JWT_EXPIRED', message: '"exp" claim timestamp check failed' })
t.true(err instanceof errors.JWTClaimInvalid)
t.is(err.claim, 'exp')
t.is(err.reason, 'check_failed')
})
test('exp check (failed normal)', t => {
const token = JWT.sign({ exp: epoch - 1 }, key, { iat: false })
const err = t.throws(() => {
JWT.verify(token, key, { now })
}, { instanceOf: errors.JWTExpired, code: 'ERR_JWT_EXPIRED', message: '"exp" claim timestamp check failed' })
t.true(err instanceof errors.JWTClaimInvalid)
t.is(err.claim, 'exp')
t.is(err.reason, 'check_failed')
})
test('exp check (passed because of ignoreExp)', t => {
const token = JWT.sign({ exp: epoch - 10 }, key, { iat: false })
JWT.verify(token, key, { now, ignoreExp: true })
t.pass()
})
test('nbf check (pass)', t => {
const token = JWT.sign({ nbf: epoch - 30 }, key, { iat: false })
JWT.verify(token, key, { now })
t.pass()
})
test('nbf check (pass equal)', t => {
const token = JWT.sign({ nbf: epoch }, key, { iat: false })
JWT.verify(token, key, { now })
t.pass()
})
test('nbf check (pass with tolerance)', t => {
const token = JWT.sign({ nbf: epoch + 1 }, key, { iat: false })
JWT.verify(token, key, { now, clockTolerance: '1s' })
t.pass()
})
test('nbf check (failed)', t => {
const token = JWT.sign({ nbf: epoch + 10 }, key, { iat: false })
const err = t.throws(() => {
JWT.verify(token, key, { now })
}, { instanceOf: errors.JWTClaimInvalid, message: '"nbf" claim timestamp check failed' })
t.is(err.claim, 'nbf')
t.is(err.reason, 'check_failed')
})
test('nbf check (passed because of ignoreIat)', t => {
const token = JWT.sign({ nbf: epoch + 10 }, key, { iat: false })
JWT.verify(token, key, { now, ignoreNbf: true })
t.pass()
})
{
const token = JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client_id' })
test('IdToken.verify options must be an object', t => {
t.throws(() => {
JWT.IdToken.verify(token, key, [])
}, { instanceOf: TypeError, message: 'options must be an object' })
})
test('IdToken.verify options.maxAuthAge must be a string', string, 'maxAuthAge', JWT.IdToken.verify, { issuer: 'foo', audience: 'bar' })
test('IdToken.verify options.nonce must be a string', string, 'nonce', JWT.IdToken.verify, { issuer: 'foo', audience: 'bar' })
test('IdToken.verify', t => {
JWT.IdToken.verify(token, key, { issuer: 'issuer', audience: 'client_id' })
t.pass()
})
test('IdToken.verify requires issuer option too', t => {
t.throws(() => {
JWT.IdToken.verify(token, key)
}, { instanceOf: TypeError, message: '"issuer" option is required to validate an ID Token' })
})
test('IdToken.verify requires audience option too', t => {
t.throws(() => {
JWT.IdToken.verify(token, key, { issuer: 'issuer' })
}, { instanceOf: TypeError, message: '"audience" option is required to validate an ID Token' })
})
test('IdToken.verify mandates exp to be present', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ }, key, { subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"exp" claim is missing' })
t.is(err.claim, 'exp')
t.is(err.reason, 'missing')
})
test('IdToken.verify mandates iat to be present', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', iat: false, subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
test('IdToken.verify mandates sub to be present', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"sub" claim is missing' })
t.is(err.claim, 'sub')
t.is(err.reason, 'missing')
})
test('IdToken.verify mandates iss to be present', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iss" claim is missing' })
t.is(err.claim, 'iss')
t.is(err.reason, 'missing')
})
test('IdToken.verify mandates aud to be present', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"aud" claim is missing' })
t.is(err.claim, 'aud')
t.is(err.reason, 'missing')
})
test('IdToken.verify mandates azp to be present when multiple audiences are used', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: ['client_id', 'another audience'] }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"azp" claim is missing' })
t.is(err.claim, 'azp')
t.is(err.reason, 'missing')
})
test('IdToken.verify mandates azp to match the audience when required', t => {
const err = t.throws(() => {
JWT.IdToken.verify(
JWT.sign({ azp: 'mismatched' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: ['client_id', 'another audience'] }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "azp" claim value' })
t.is(err.claim, 'azp')
t.is(err.reason, 'check_failed')
})
test('IdToken.verify validates full id tokens', t => {
t.notThrows(() => {
JWT.IdToken.verify(
JWT.sign({ azp: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: ['client_id', 'another audience'] }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
})
})
test('IdToken.verify option.maxAuthAge requires auth_time to be in the payload', t => {
const err = t.throws(() => {
const invalid = JWT.sign({}, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client' })
JWT.IdToken.verify(invalid, key, { maxAuthAge: '30s', issuer: 'issuer', audience: 'client' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim is missing' })
t.is(err.claim, 'auth_time')
t.is(err.reason, 'missing')
})
test('IdToken.verify option.maxAuthAge checks auth_time', t => {
const err = t.throws(() => {
const invalid = JWT.sign({ auth_time: epoch - 31 }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client' })
JWT.IdToken.verify(invalid, key, { maxAuthAge: '30s', now, issuer: 'issuer', audience: 'client' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim timestamp check failed (too much time has elapsed since the last End-User authentication)' })
t.is(err.claim, 'auth_time')
t.is(err.reason, 'check_failed')
})
test('IdToken.verify option.maxAuthAge checks auth_time (with tolerance)', t => {
const token = JWT.sign({ auth_time: epoch - 31 }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client' })
JWT.IdToken.verify(token, key, { maxAuthAge: '30s', now, clockTolerance: '1s', issuer: 'issuer', audience: 'client' })
t.pass()
})
test('IdToken.verify auth_time must be a timestamp when provided', t => {
;['', 'foo', true, null, [], {}].forEach((val) => {
const err = t.throws(() => {
const invalid = JWT.sign({ auth_time: val }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client' })
JWT.IdToken.verify(invalid, key, { issuer: 'issuer', audience: 'client' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim must be a JSON numeric value' })
t.is(err.claim, 'auth_time')
t.is(err.reason, 'invalid')
})
})
test('IdToken.verify option.nonce checks nonce value', t => {
const token = JWT.sign({ nonce: 'foobar' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client' })
JWT.IdToken.verify(token, key, { now, issuer: 'issuer', audience: 'client', nonce: 'foobar' })
const err = t.throws(() => {
JWT.IdToken.verify(token, key, { now, issuer: 'issuer', audience: 'client', nonce: 'baz' })
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "nonce" claim value' })
t.is(err.claim, 'nonce')
t.is(err.reason, 'check_failed')
})
}
{
const token = JWT.sign({
events: {
'http://schemas.openid.net/event/backchannel-logout': {}
}
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' })
test('LogoutToken.verify options must be an object', t => {
t.throws(() => {
JWT.LogoutToken.verify(token, key, [])
}, { instanceOf: TypeError, message: 'options must be an object' })
})
test('LogoutToken.verify', t => {
JWT.LogoutToken.verify(token, key, { issuer: 'issuer', audience: 'client_id' })
t.pass()
})
test('LogoutToken.verify requires issuer option too', t => {
t.throws(() => {
JWT.LogoutToken.verify(token, key)
}, { instanceOf: TypeError, message: '"issuer" option is required to validate a Logout Token' })
})
test('LogoutToken.verify requires audience option too', t => {
t.throws(() => {
JWT.LogoutToken.verify(token, key, { issuer: 'issuer' })
}, { instanceOf: TypeError, message: '"audience" option is required to validate a Logout Token' })
})
test('LogoutToken.verify mandates jti to be present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ }, key, { subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"jti" claim is missing' })
t.is(err.claim, 'jti')
t.is(err.reason, 'missing')
})
test('LogoutToken.verify mandates events to be present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"events" claim is missing' })
t.is(err.claim, 'events')
t.is(err.reason, 'missing')
})
test('LogoutToken.verify mandates events to be an object', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({
events: []
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"events" claim must be an object' })
t.is(err.claim, 'events')
t.is(err.reason, 'invalid')
})
test('LogoutToken.verify mandates events to have the backchannel logout member', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({
events: {}
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"http://schemas.openid.net/event/backchannel-logout" member is missing in the "events" claim' })
t.is(err.claim, 'events')
t.is(err.reason, 'invalid')
})
test('LogoutToken.verify mandates events to have the backchannel logout member thats an object', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({
events: {
'http://schemas.openid.net/event/backchannel-logout': []
}
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"http://schemas.openid.net/event/backchannel-logout" member in the "events" claim must be an object' })
t.is(err.claim, 'events')
t.is(err.reason, 'invalid')
})
test('LogoutToken.verify mandates iat to be present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', iat: false, subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
test('LogoutToken.verify mandates sub or sid to be present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: 'either "sid" or "sub" (or both) claims must be present' })
t.is(err.claim, 'unspecified')
t.is(err.reason, 'unspecified')
})
test('LogoutToken.verify mandates sid to be a string when present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ sid: true }, key, { jti: 'foo', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"sid" claim must be a string' })
t.is(err.claim, 'sid')
t.is(err.reason, 'invalid')
})
test('LogoutToken.verify prohibits nonce', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ nonce: 'foo' }, key, { subject: 'subject', jti: 'foo', issuer: 'issuer', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"nonce" claim is prohibited' })
t.is(err.claim, 'nonce')
t.is(err.reason, 'prohibited')
})
test('LogoutToken.verify mandates iss to be present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', subject: 'subject', audience: 'client_id' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iss" claim is missing' })
t.is(err.claim, 'iss')
t.is(err.reason, 'missing')
})
test('LogoutToken.verify mandates aud to be present', t => {
const err = t.throws(() => {
JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', subject: 'subject', issuer: 'issuer' }),
key,
{ issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"aud" claim is missing' })
t.is(err.claim, 'aud')
t.is(err.reason, 'missing')
})
}
{
const token = JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } })
test('AccessToken.verify options must be an object', t => {
t.throws(() => {
JWT.AccessToken.verify(token, key, [])
}, { instanceOf: TypeError, message: 'options must be an object' })
})
test('AccessToken.verify options.maxAuthAge must be a string', string, 'maxAuthAge', JWT.AccessToken.verify, { issuer: 'foo', audience: 'bar' })
test('AccessToken.verify', t => {
JWT.AccessToken.verify(token, key, { issuer: 'issuer', audience: 'RS' })
t.pass()
})
test('AccessToken.verify requires issuer option too', t => {
t.throws(() => {
JWT.AccessToken.verify(token, key)
}, { instanceOf: TypeError, message: '"issuer" option is required to validate a JWT Access Token' })
})
test('AccessToken.verify requires audience option too', t => {
t.throws(() => {
JWT.AccessToken.verify(token, key, { issuer: 'issuer' })
}, { instanceOf: TypeError, message: '"audience" option is required to validate a JWT Access Token' })
})
test('AccessToken.verify mandates exp to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"exp" claim is missing' })
t.is(err.claim, 'exp')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates client_id to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"client_id" claim is missing' })
t.is(err.claim, 'client_id')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates jti to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"jti" claim is missing' })
t.is(err.claim, 'jti')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates iat to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', iat: false, header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates sub to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"sub" claim is missing' })
t.is(err.claim, 'sub')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates iss to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iss" claim is missing' })
t.is(err.claim, 'iss')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates aud to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', jti: 'random', header: { typ: 'at+JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"aud" claim is missing' })
t.is(err.claim, 'aud')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates header typ to be present', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', jti: 'random', issuer: 'issuer' }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"typ" header parameter is missing' })
t.is(err.claim, 'typ')
t.is(err.reason, 'missing')
})
test('AccessToken.verify mandates header typ to be present and of the right value', t => {
const err = t.throws(() => {
JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', jti: 'random', issuer: 'issuer', header: { typ: 'JWT' } }),
key,
{ issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: 'unexpected "typ" JWT header value' })
t.is(err.claim, 'typ')
t.is(err.reason, 'check_failed')
})
test('AccessToken.verify option.maxAuthAge requires auth_time to be in the payload', t => {
const err = t.throws(() => {
const invalid = JWT.sign({ client_id: 'client' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } })
JWT.AccessToken.verify(invalid, key, { maxAuthAge: '30s', issuer: 'issuer', audience: 'RS' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim is missing' })
t.is(err.claim, 'auth_time')
t.is(err.reason, 'missing')
})
test('AccessToken.verify option.maxAuthAge checks auth_time', t => {
const err = t.throws(() => {
const invalid = JWT.sign({ auth_time: epoch - 31, client_id: 'client' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } })
JWT.AccessToken.verify(invalid, key, { maxAuthAge: '30s', now, issuer: 'issuer', audience: 'RS' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim timestamp check failed (too much time has elapsed since the last End-User authentication)' })
t.is(err.claim, 'auth_time')
t.is(err.reason, 'check_failed')
})
test('AccessToken.verify option.maxAuthAge checks auth_time (with tolerance)', t => {
const token = JWT.sign({ auth_time: epoch - 31, client_id: 'client' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } })
JWT.AccessToken.verify(token, key, { maxAuthAge: '30s', now, clockTolerance: '1s', issuer: 'issuer', audience: 'RS' })
t.pass()
})
test('AccessToken.verify auth_time must be a timestamp when provided', t => {
;['', 'foo', true, null, [], {}].forEach((val) => {
const err = t.throws(() => {
const invalid = JWT.sign({ auth_time: val, client_id: 'client' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } })
JWT.AccessToken.verify(invalid, key, { issuer: 'issuer', audience: 'RS' })
}, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim must be a JSON numeric value' })
t.is(err.claim, 'auth_time')
t.is(err.reason, 'invalid')
})
})
}