169 lines
6.2 KiB
JavaScript
169 lines
6.2 KiB
JavaScript
const { JWTClaimInvalid } = require('../errors')
|
|
const secs = require('../help/secs')
|
|
const epoch = require('../help/epoch')
|
|
const isObject = require('../help/is_object')
|
|
|
|
const verify = require('./verify')
|
|
const {
|
|
isString,
|
|
isRequired,
|
|
isTimestamp,
|
|
isStringOrArrayOfStrings
|
|
} = require('./shared_validations')
|
|
|
|
const isPayloadRequired = isRequired.bind(undefined, JWTClaimInvalid)
|
|
const isPayloadString = isString.bind(undefined, JWTClaimInvalid)
|
|
const isOptionString = isString.bind(undefined, TypeError)
|
|
|
|
const defineLazyExportWithWarning = (obj, property, name, definition) => {
|
|
Object.defineProperty(obj, property, {
|
|
enumerable: true,
|
|
configurable: true,
|
|
value (...args) {
|
|
process.emitWarning(
|
|
`The ${name} API implements an IETF draft. Breaking draft implementations are included as minor versions of the jose library, therefore, the ~ semver operator should be used and close attention be payed to library changelog as well as the drafts themselves.`,
|
|
'DraftWarning'
|
|
)
|
|
Object.defineProperty(obj, property, {
|
|
enumerable: true,
|
|
configurable: true,
|
|
value: definition
|
|
})
|
|
return obj[property](...args)
|
|
}
|
|
})
|
|
}
|
|
|
|
const validateCommonOptions = (options, profile) => {
|
|
if (!isObject(options)) {
|
|
throw new TypeError('options must be an object')
|
|
}
|
|
|
|
if (!options.issuer) {
|
|
throw new TypeError(`"issuer" option is required to validate ${profile}`)
|
|
}
|
|
|
|
if (!options.audience) {
|
|
throw new TypeError(`"audience" option is required to validate ${profile}`)
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
IdToken: {
|
|
verify: (token, key, options = {}) => {
|
|
validateCommonOptions(options, 'an ID Token')
|
|
|
|
if ('maxAuthAge' in options) {
|
|
isOptionString(options.maxAuthAge, 'options.maxAuthAge')
|
|
}
|
|
if ('nonce' in options) {
|
|
isOptionString(options.nonce, 'options.nonce')
|
|
}
|
|
|
|
const unix = epoch(options.now || new Date())
|
|
const result = verify(token, key, { ...options })
|
|
const payload = options.complete ? result.payload : result
|
|
|
|
if (Array.isArray(payload.aud) && payload.aud.length > 1) {
|
|
isPayloadRequired(payload.azp, '"azp" claim', 'azp')
|
|
}
|
|
isPayloadRequired(payload.iat, '"iat" claim', 'iat')
|
|
isPayloadRequired(payload.sub, '"sub" claim', 'sub')
|
|
isPayloadRequired(payload.exp, '"exp" claim', 'exp')
|
|
isTimestamp(payload.auth_time, 'auth_time', !!options.maxAuthAge)
|
|
isPayloadString(payload.nonce, '"nonce" claim', 'nonce', !!options.nonce)
|
|
isPayloadString(payload.acr, '"acr" claim', 'acr')
|
|
isStringOrArrayOfStrings(payload.amr, 'amr')
|
|
|
|
if (options.nonce && payload.nonce !== options.nonce) {
|
|
throw new JWTClaimInvalid('unexpected "nonce" claim value', 'nonce', 'check_failed')
|
|
}
|
|
|
|
const tolerance = options.clockTolerance ? secs(options.clockTolerance) : 0
|
|
|
|
if (options.maxAuthAge) {
|
|
const maxAuthAgeSeconds = secs(options.maxAuthAge)
|
|
if (payload.auth_time + maxAuthAgeSeconds < unix - tolerance) {
|
|
throw new JWTClaimInvalid('"auth_time" claim timestamp check failed (too much time has elapsed since the last End-User authentication)', 'auth_time', 'check_failed')
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(payload.aud) && payload.aud.length > 1 && payload.azp !== options.audience) {
|
|
throw new JWTClaimInvalid('unexpected "azp" claim value', 'azp', 'check_failed')
|
|
}
|
|
|
|
return result
|
|
}
|
|
},
|
|
LogoutToken: {},
|
|
AccessToken: {}
|
|
}
|
|
|
|
defineLazyExportWithWarning(module.exports.LogoutToken, 'verify', 'jose.JWT.LogoutToken.verify', (token, key, options = {}) => {
|
|
validateCommonOptions(options, 'a Logout Token')
|
|
|
|
const result = verify(token, key, { ...options })
|
|
const payload = options.complete ? result.payload : result
|
|
|
|
isPayloadRequired(payload.iat, '"iat" claim', 'iat')
|
|
isPayloadRequired(payload.jti, '"jti" claim', 'jti')
|
|
isPayloadString(payload.sid, '"sid" claim', 'sid')
|
|
|
|
if (!('sid' in payload) && !('sub' in payload)) {
|
|
throw new JWTClaimInvalid('either "sid" or "sub" (or both) claims must be present')
|
|
}
|
|
|
|
if ('nonce' in payload) {
|
|
throw new JWTClaimInvalid('"nonce" claim is prohibited', 'nonce', 'prohibited')
|
|
}
|
|
|
|
if (!('events' in payload)) {
|
|
throw new JWTClaimInvalid('"events" claim is missing', 'events', 'missing')
|
|
}
|
|
|
|
if (!isObject(payload.events)) {
|
|
throw new JWTClaimInvalid('"events" claim must be an object', 'events', 'invalid')
|
|
}
|
|
|
|
if (!('http://schemas.openid.net/event/backchannel-logout' in payload.events)) {
|
|
throw new JWTClaimInvalid('"http://schemas.openid.net/event/backchannel-logout" member is missing in the "events" claim', 'events', 'invalid')
|
|
}
|
|
|
|
if (!isObject(payload.events['http://schemas.openid.net/event/backchannel-logout'])) {
|
|
throw new JWTClaimInvalid('"http://schemas.openid.net/event/backchannel-logout" member in the "events" claim must be an object', 'events', 'invalid')
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
defineLazyExportWithWarning(module.exports.AccessToken, 'verify', 'jose.JWT.AccessToken.verify', (token, key, options = {}) => {
|
|
validateCommonOptions(options, 'a JWT Access Token')
|
|
|
|
isOptionString(options.maxAuthAge, 'options.maxAuthAge')
|
|
|
|
const unix = epoch(options.now || new Date())
|
|
const typ = 'at+JWT'
|
|
const result = verify(token, key, { ...options, typ })
|
|
const payload = options.complete ? result.payload : result
|
|
|
|
isPayloadRequired(payload.iat, '"iat" claim', 'iat')
|
|
isPayloadRequired(payload.exp, '"exp" claim', 'exp')
|
|
isPayloadRequired(payload.sub, '"sub" claim', 'sub')
|
|
isPayloadRequired(payload.jti, '"jti" claim', 'jti')
|
|
isPayloadString(payload.client_id, '"client_id" claim', 'client_id', true)
|
|
isTimestamp(payload.auth_time, 'auth_time', !!options.maxAuthAge)
|
|
isPayloadString(payload.acr, '"acr" claim', 'acr')
|
|
isStringOrArrayOfStrings(payload.amr, 'amr')
|
|
|
|
const tolerance = options.clockTolerance ? secs(options.clockTolerance) : 0
|
|
|
|
if (options.maxAuthAge) {
|
|
const maxAuthAgeSeconds = secs(options.maxAuthAge)
|
|
if (payload.auth_time + maxAuthAgeSeconds < unix - tolerance) {
|
|
throw new JWTClaimInvalid('"auth_time" claim timestamp check failed (too much time has elapsed since the last End-User authentication)', 'auth_time', 'check_failed')
|
|
}
|
|
}
|
|
|
|
return result
|
|
})
|