Comparing version 1.18.0 to 1.18.1
@@ -5,2 +5,11 @@ # Change Log | ||
## [1.18.1](https://github.com/panva/jose/compare/v1.18.0...v1.18.1) (2020-01-01) | ||
### Bug Fixes | ||
* force iat past check when maxTokenAge option is used + JWT refactor ([828ad5a](https://github.com/panva/jose/commit/828ad5a33dc0cc0049923b69f43f97463295456e)) | ||
# [1.18.0](https://github.com/panva/jose/compare/v1.17.2...v1.18.0) (2019-12-31) | ||
@@ -7,0 +16,0 @@ |
@@ -41,57 +41,61 @@ const isObject = require('../help/is_object') | ||
const validateOptions = (options) => { | ||
isOptionString(options.profile, 'options.profile') | ||
const validateOptions = ({ | ||
algorithms, audience, clockTolerance, complete = false, crit, ignoreExp = false, | ||
ignoreIat = false, ignoreNbf = false, issuer, jti, maxAuthAge, maxTokenAge, nonce, now = new Date(), | ||
profile, subject | ||
}) => { | ||
isOptionString(profile, 'options.profile') | ||
if (typeof options.complete !== 'boolean') { | ||
if (typeof complete !== 'boolean') { | ||
throw new TypeError('options.complete must be a boolean') | ||
} | ||
if (typeof options.ignoreExp !== 'boolean') { | ||
if (typeof ignoreExp !== 'boolean') { | ||
throw new TypeError('options.ignoreExp must be a boolean') | ||
} | ||
if (typeof options.ignoreNbf !== 'boolean') { | ||
if (typeof ignoreNbf !== 'boolean') { | ||
throw new TypeError('options.ignoreNbf must be a boolean') | ||
} | ||
if (typeof options.ignoreIat !== 'boolean') { | ||
if (typeof ignoreIat !== 'boolean') { | ||
throw new TypeError('options.ignoreIat must be a boolean') | ||
} | ||
isOptionString(options.maxTokenAge, 'options.maxTokenAge') | ||
isOptionString(options.subject, 'options.subject') | ||
isOptionString(options.issuer, 'options.issuer') | ||
isOptionString(options.maxAuthAge, 'options.maxAuthAge') | ||
isOptionString(options.jti, 'options.jti') | ||
isOptionString(options.clockTolerance, 'options.clockTolerance') | ||
isOptionString(maxTokenAge, 'options.maxTokenAge') | ||
isOptionString(subject, 'options.subject') | ||
isOptionString(issuer, 'options.issuer') | ||
isOptionString(maxAuthAge, 'options.maxAuthAge') | ||
isOptionString(jti, 'options.jti') | ||
isOptionString(clockTolerance, 'options.clockTolerance') | ||
if (options.audience !== undefined && (isNotString(options.audience) && isNotArrayOfStrings(options.audience))) { | ||
if (audience !== undefined && (isNotString(audience) && isNotArrayOfStrings(audience))) { | ||
throw new TypeError('options.audience must be a string or an array of strings') | ||
} | ||
if (options.algorithms !== undefined && isNotArrayOfStrings(options.algorithms)) { | ||
if (algorithms !== undefined && isNotArrayOfStrings(algorithms)) { | ||
throw new TypeError('options.algorithms must be an array of strings') | ||
} | ||
isOptionString(options.nonce, 'options.nonce') | ||
isOptionString(nonce, 'options.nonce') | ||
if (!(options.now instanceof Date) || !options.now.getTime()) { | ||
if (!(now instanceof Date) || !now.getTime()) { | ||
throw new TypeError('options.now must be a valid Date object') | ||
} | ||
if (options.ignoreIat && options.maxTokenAge !== undefined) { | ||
if (ignoreIat && maxTokenAge !== undefined) { | ||
throw new TypeError('options.ignoreIat and options.maxTokenAge cannot used together') | ||
} | ||
if (options.crit !== undefined && isNotArrayOfStrings(options.crit)) { | ||
if (crit !== undefined && isNotArrayOfStrings(crit)) { | ||
throw new TypeError('options.crit must be an array of strings') | ||
} | ||
switch (options.profile) { | ||
switch (profile) { | ||
case IDTOKEN: | ||
if (!options.issuer) { | ||
if (!issuer) { | ||
throw new TypeError('"issuer" option is required to validate an ID Token') | ||
} | ||
if (!options.audience) { | ||
if (!audience) { | ||
throw new TypeError('"audience" option is required to validate an ID Token') | ||
@@ -102,7 +106,7 @@ } | ||
case ATJWT: | ||
if (!options.issuer) { | ||
if (!issuer) { | ||
throw new TypeError('"issuer" option is required to validate a JWT Access Token') | ||
} | ||
if (!options.audience) { | ||
if (!audience) { | ||
throw new TypeError('"audience" option is required to validate a JWT Access Token') | ||
@@ -113,7 +117,7 @@ } | ||
case LOGOUTTOKEN: | ||
if (!options.issuer) { | ||
if (!issuer) { | ||
throw new TypeError('"issuer" option is required to validate a Logout Token') | ||
} | ||
if (!options.audience) { | ||
if (!audience) { | ||
throw new TypeError('"audience" option is required to validate a Logout Token') | ||
@@ -126,19 +130,38 @@ } | ||
default: | ||
throw new TypeError(`unsupported options.profile value "${options.profile}"`) | ||
throw new TypeError(`unsupported options.profile value "${profile}"`) | ||
} | ||
return { | ||
algorithms, | ||
audience, | ||
clockTolerance, | ||
complete, | ||
crit, | ||
ignoreExp, | ||
ignoreIat, | ||
ignoreNbf, | ||
issuer, | ||
jti, | ||
maxAuthAge, | ||
maxTokenAge, | ||
nonce, | ||
now, | ||
profile, | ||
subject | ||
} | ||
} | ||
const validateTypes = ({ header, payload }, profile) => { | ||
const validateTypes = ({ header, payload }, profile, options) => { | ||
isPayloadString(header.alg, '"alg" header parameter', true) | ||
isTimestamp(payload.iat, 'iat', profile === IDTOKEN || profile === LOGOUTTOKEN) | ||
isTimestamp(payload.iat, 'iat', profile === IDTOKEN || profile === LOGOUTTOKEN || !!options.maxTokenAge) | ||
isTimestamp(payload.exp, 'exp', profile === IDTOKEN || profile === ATJWT) | ||
isTimestamp(payload.auth_time, 'auth_time') | ||
isTimestamp(payload.auth_time, 'auth_time', !!options.maxAuthAge) | ||
isTimestamp(payload.nbf, 'nbf') | ||
isPayloadString(payload.jti, '"jti" claim', profile === LOGOUTTOKEN) | ||
isPayloadString(payload.jti, '"jti" claim', profile === LOGOUTTOKEN || !!options.jti) | ||
isPayloadString(payload.acr, '"acr" claim') | ||
isPayloadString(payload.nonce, '"nonce" claim') | ||
isPayloadString(payload.iss, '"iss" claim', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN) | ||
isPayloadString(payload.sub, '"sub" claim', profile === IDTOKEN || profile === ATJWT) | ||
isStringOrArrayOfStrings(payload.aud, 'aud', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN) | ||
isPayloadString(payload.nonce, '"nonce" claim', !!options.nonce) | ||
isPayloadString(payload.iss, '"iss" claim', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN || !!options.issuer) | ||
isPayloadString(payload.sub, '"sub" claim', profile === IDTOKEN || profile === ATJWT || !!options.subject) | ||
isStringOrArrayOfStrings(payload.aud, 'aud', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN || !!options.audience) | ||
isPayloadString(payload.azp, '"azp" claim', profile === IDTOKEN && Array.isArray(payload.aud) && payload.aud.length > 1) | ||
@@ -205,49 +228,29 @@ isStringOrArrayOfStrings(payload.amr, 'amr') | ||
const { | ||
algorithms, audience, clockTolerance, complete = false, crit, ignoreExp = false, | ||
ignoreIat = false, ignoreNbf = false, issuer, jti, maxAuthAge, maxTokenAge, nonce, now = new Date(), | ||
subject, profile | ||
} = options | ||
algorithms, audience, clockTolerance, complete, crit, ignoreExp, ignoreIat, ignoreNbf, issuer, | ||
jti, maxAuthAge, maxTokenAge, nonce, now, profile, subject | ||
} = options = validateOptions(options) | ||
validateOptions({ | ||
algorithms, | ||
audience, | ||
clockTolerance, | ||
complete, | ||
crit, | ||
ignoreExp, | ||
ignoreIat, | ||
ignoreNbf, | ||
issuer, | ||
jti, | ||
maxAuthAge, | ||
maxTokenAge, | ||
nonce, | ||
now, | ||
profile, | ||
subject | ||
}) | ||
const unix = epoch(now) | ||
const decoded = decode(token, { complete: true }) | ||
validateTypes(decoded, profile) | ||
validateTypes(decoded, profile, options) | ||
if (issuer && decoded.payload.iss !== issuer) { | ||
throw new JWTClaimInvalid('issuer mismatch') | ||
throw new JWTClaimInvalid('unexpected "iss" claim value') | ||
} | ||
if (nonce && decoded.payload.nonce !== nonce) { | ||
throw new JWTClaimInvalid('nonce mismatch') | ||
throw new JWTClaimInvalid('unexpected "nonce" claim value') | ||
} | ||
if (subject && decoded.payload.sub !== subject) { | ||
throw new JWTClaimInvalid('subject mismatch') | ||
throw new JWTClaimInvalid('unexpected "sub" claim value') | ||
} | ||
if (jti && decoded.payload.jti !== jti) { | ||
throw new JWTClaimInvalid('jti mismatch') | ||
throw new JWTClaimInvalid('unexpected "jti" claim value') | ||
} | ||
if (audience && !checkAudiencePresence(decoded.payload.aud, typeof audience === 'string' ? [audience] : audience, profile)) { | ||
throw new JWTClaimInvalid('audience mismatch') | ||
throw new JWTClaimInvalid('unexpected "aud" claim value') | ||
} | ||
@@ -258,9 +261,5 @@ | ||
if (maxAuthAge) { | ||
if (!('auth_time' in decoded.payload)) { | ||
throw new JWTClaimInvalid('missing auth_time') | ||
} | ||
const maxAuthAgeSeconds = secs(maxAuthAge) | ||
if (decoded.payload.auth_time + maxAuthAgeSeconds < unix - tolerance) { | ||
throw new JWTClaimInvalid('too much time has elapsed since the last End-User authentication') | ||
throw new JWTClaimInvalid('"auth_time" claim timestamp check failed (too much time has elapsed since the last End-User authentication)') | ||
} | ||
@@ -270,20 +269,23 @@ } | ||
if (!ignoreIat && !('exp' in decoded.payload) && 'iat' in decoded.payload && decoded.payload.iat > unix + tolerance) { | ||
throw new JWTClaimInvalid('token issued in the future') | ||
throw new JWTClaimInvalid('"iat" claim timestamp check failed (it should be in the past)') | ||
} | ||
if (!ignoreNbf && 'nbf' in decoded.payload && decoded.payload.nbf > unix + tolerance) { | ||
throw new JWTClaimInvalid('token is not active yet') | ||
throw new JWTClaimInvalid('"nbf" claim timestamp check failed') | ||
} | ||
if (!ignoreExp && 'exp' in decoded.payload && decoded.payload.exp <= unix - tolerance) { | ||
throw new JWTClaimInvalid('token is expired') | ||
throw new JWTClaimInvalid('"exp" claim timestamp check failed') | ||
} | ||
if (maxTokenAge) { | ||
if (!('iat' in decoded.payload)) { | ||
throw new JWTClaimInvalid('missing iat claim') | ||
const age = unix - decoded.payload.iat | ||
const max = secs(maxTokenAge) | ||
if (age - tolerance > max) { | ||
throw new JWTClaimInvalid('"iat" claim timestamp check failed (too far in the past)') | ||
} | ||
if (decoded.payload.iat + secs(maxTokenAge) < unix + tolerance) { | ||
throw new JWTClaimInvalid('maxTokenAge exceeded') | ||
if (age - tolerance > max || age < 0 - tolerance) { | ||
throw new JWTClaimInvalid('"iat" claim timestamp check failed (it should be in the past)') | ||
} | ||
@@ -293,3 +295,3 @@ } | ||
if (profile === IDTOKEN && Array.isArray(decoded.payload.aud) && decoded.payload.aud.length > 1 && decoded.payload.azp !== audience) { | ||
throw new JWTClaimInvalid('azp mismatch') | ||
throw new JWTClaimInvalid('unexpected "azp" claim value') | ||
} | ||
@@ -296,0 +298,0 @@ |
{ | ||
"name": "jose", | ||
"version": "1.18.0", | ||
"version": "1.18.1", | ||
"description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
220878
4746