diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3914d597a9..11d61225c4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,6 +38,8 @@ jobs:
- run: npm run lint-ts
test:
+ env:
+ NODE_NO_WARNINGS: 1
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -81,6 +83,8 @@ jobs:
run: npx codecov
test-electron:
+ env:
+ NODE_NO_WARNINGS: 1
runs-on: ${{ matrix.os }}
strategy:
matrix:
diff --git a/README.md b/README.md
index 4a067f7c63..898aff039d 100644
--- a/README.md
+++ b/README.md
@@ -142,8 +142,8 @@ jose.JWT.verify(
Verifying OIDC ID Tokens (Click to expand)
ID Token is a JWT, but profiled, there are additional requirements to a JWT to be accepted as an
-ID Token and it is pretty easy to omit some, use the `profile` option of `JWT.verify` or the
-`JWT.IdToken.verify` shorthand to make sure what you're accepting is really an ID Token meant to
+ID Token and it is pretty easy to omit some, use the
+`JWT.IdToken.verify` API to make sure what you're accepting is really an ID Token meant to
your Client. This will then perform all doable validations given the input. See the
[documentation][documentation-jwt] for more.
@@ -175,8 +175,8 @@ attention to changelog and the drafts themselves.
When accepting a JWT-formatted OAuth 2.0 Access Token there are additional requirements for the JWT
to be accepted as an Access Token according to the [specification][draft-ietf-oauth-access-token-jwt]
-and it is pretty easy to omit some. Use the `profile` option of `JWT.verify` or the
-`JWT.AccessToken.verify` shorthand to make sure what you're accepting is really a JWT Access Token
+and it is pretty easy to omit some. Use the
+`JWT.AccessToken.verify` API to make sure what you're accepting is really a JWT Access Token
meant for your Resource Server. This will then perform all doable validations given the input. See
the [documentation][documentation-jwt] for more.
@@ -202,8 +202,8 @@ since they may have breaking changes use the `~` semver operator when using thes
attention to changelog and the drafts themselves.
Logout Token is a JWT, but profiled, there are additional requirements to a JWT to be accepted as an
-Logout Token and it is pretty easy to omit some, use the `profile` option of `JWT.verify` or the
-`JWT.LogoutToken.verify` to make sure what you're accepting is really an Logout Token meant to your
+Logout Token and it is pretty easy to omit some, use the
+`JWT.LogoutToken.verify` API to make sure what you're accepting is really an Logout Token meant to your
Client. This will then perform all doable validations given the input. See the
[documentation][documentation-jwt] for more.
diff --git a/docs/README.md b/docs/README.md
index 5d31cff7c3..f72153d3a9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -908,12 +908,6 @@ Verifies the claims and signature of a JSON Web Token.
- `algorithms`: `string[]` Array of expected signing algorithms. JWT signed with an algorithm not
found in this option will be rejected. **Default:** accepts all algorithms available on the
passed key (or keys in the keystore)
- - `profile`: `` To validate a JWT according to a specific profile, e.g. as an ID Token.
- Supported values are 'id_token', 'at+JWT' (draft), and 'logout_token' (draft). **Default:** 'undefined'
- (generic JWT). Combine this option with the other ones like `maxAuthAge` and `nonce` or
- `subject` depending on the use-case. Draft profiles are updated as minor versions of the library,
- therefore, since they may have breaking changes use the `~` semver operator when using these and
- pay close attention to changelog and the drafts themselves.
- `audience`: `` | `string[]` Expected audience value(s). When string an exact match must
be found in the payload, when array at least one must be matched.
- `typ`: `` Expected JWT "typ" Header Parameter value. An exact match must be found in the
@@ -935,14 +929,9 @@ Verifies the claims and signature of a JSON Web Token.
- `issuer`: `` | `string[]` Expected issuer value(s). When string an exact match must
be found in the payload, when array at least one must be matched.
- `jti`: `` Expected jti value. An exact match must be found in the payload.
- - `maxAuthAge`: `` When provided the payload is checked to have the "auth_time" claim and
- its value is validated, provided as timespan string e.g. `30m`, `24 hours`. See
- [OpenID Connect Core 1.0][connect-core] for details. Do not confuse with maxTokenAge option.
- `maxTokenAge`: `` When provided the payload is checked to have the "iat" claim and its
value is validated not to be older than the provided timespan string e.g. `30m`, `24 hours`.
Do not confuse with maxAuthAge option.
- - `nonce`: `` Expected nonce value. An exact match must be found in the payload. See
- [OpenID Connect Core 1.0][connect-core] for details.
- `now`: `` Date object to be used instead of the current unix epoch timestamp.
**Default:** 'new Date()'
- `subject`: `` Expected subject value. An exact match must be found in the payload.
@@ -1014,7 +1003,22 @@ JWT.decode(token, { complete: true })
#### `JWT.AccessToken.verify(token, keyOrStore, options])`
-A shorthand for [`JWT.verify`](#jwtverifytoken-keyorstore-options) with the `profile` option set to `at+JWT`.
+A shorthand for [`JWT.verify`](#jwtverifytoken-keyorstore-options) with additional constraints and options
+to verify an Access Token according to
+[JWT Profile for OAuth 2.0 Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-06).
+This is an IETF **draft** implementation. 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.
+
+The function arguments are the same as for [`JWT.verify`](#jwtverifytoken-keyorstore-options), only difference
+is that `issuer` and `audience` options are required and the additional option:
+
+- see [`JWT.verify`](#jwtverifytoken-keyorstore-options)
+- `issuer`: `` REQUIRED
+- `audience`: `` REQUIRED
+- `maxAuthAge`: `` When provided the payload is checked to have the "auth_time" claim and
+ its value is validated, provided as timespan string e.g. `30m`, `24 hours`. See
+ [OpenID Connect Core 1.0][connect-core] for details. Do not confuse with maxTokenAge option.
Example (Click to expand)
@@ -1038,6 +1042,18 @@ jose.JWT.AccessToken.verify(
A shorthand for [`JWT.verify`](#jwtverifytoken-keyorstore-options) with the `profile` option set to `id_token`.
+The function arguments are the same as for [`JWT.verify`](#jwtverifytoken-keyorstore-options), only difference
+is that `issuer` and `audience` options are required and the additional options:
+
+- see [`JWT.verify`](#jwtverifytoken-keyorstore-options)
+- `issuer`: `` REQUIRED
+- `audience`: `` REQUIRED
+- `maxAuthAge`: `` When provided the payload is checked to have the "auth_time" claim and
+ its value is validated, provided as timespan string e.g. `30m`, `24 hours`. See
+ [OpenID Connect Core 1.0][connect-core] for details. Do not confuse with maxTokenAge option.
+- `nonce`: `` Expected nonce value. An exact match must be found in the payload. See
+ [OpenID Connect Core 1.0][connect-core] for details.
+
Example (Click to expand)
@@ -1060,6 +1076,16 @@ jose.JWT.IdToken.verify(
#### `JWT.LogoutToken.verify(token, keyOrStore, options])`
A shorthand for [`JWT.verify`](#jwtverifytoken-keyorstore-options) with the `profile` option set to `logout_token`.
+This is an OIDF **draft** implementation. 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.
+
+The function arguments are the same as for [`JWT.verify`](#jwtverifytoken-keyorstore-options), only difference
+is that `issuer` and `audience` options are required.
+
+- see [`JWT.verify`](#jwtverifytoken-keyorstore-options)
+- `issuer`: `` REQUIRED
+- `audience`: `` REQUIRED
Example (Click to expand)
diff --git a/lib/jwt/profiles.js b/lib/jwt/profiles.js
index 8ef18c4322..9b3b743b66 100644
--- a/lib/jwt/profiles.js
+++ b/lib/jwt/profiles.js
@@ -1,7 +1,168 @@
+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) => verify(token, key, { ...options, profile: 'id_token' }) },
- LogoutToken: { verify: (token, key, options) => verify(token, key, { ...options, profile: 'logout_token' }) },
- AccessToken: { verify: (token, key, options) => verify(token, key, { ...options, profile: 'at+JWT' }) }
+ 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
+})
diff --git a/lib/jwt/shared_validations.js b/lib/jwt/shared_validations.js
index 276d187024..fffc4eee5d 100644
--- a/lib/jwt/shared_validations.js
+++ b/lib/jwt/shared_validations.js
@@ -1,12 +1,45 @@
-const isNotString = val => typeof val !== 'string' || val.length === 0
+const { JWTClaimInvalid } = require('../errors')
-module.exports.isNotString = isNotString
-module.exports.isString = function isString (Err, value, label, claim, required = false) {
- if (required && value === undefined) {
+const isNotString = val => typeof val !== 'string' || val.length === 0
+const isNotArrayOfStrings = val => !Array.isArray(val) || val.length === 0 || val.some(isNotString)
+const isRequired = (Err, value, label, claim) => {
+ if (value === undefined) {
throw new Err(`${label} is missing`, claim, 'missing')
}
+}
+const isString = (Err, value, label, claim, required = false) => {
+ if (required) {
+ isRequired(Err, value, label, claim)
+ }
if (value !== undefined && isNotString(value)) {
throw new Err(`${label} must be a string`, claim, 'invalid')
}
}
+const isTimestamp = (value, label, required = false) => {
+ if (required && value === undefined) {
+ throw new JWTClaimInvalid(`"${label}" claim is missing`, label, 'missing')
+ }
+
+ if (value !== undefined && (typeof value !== 'number')) {
+ throw new JWTClaimInvalid(`"${label}" claim must be a JSON numeric value`, label, 'invalid')
+ }
+}
+const isStringOrArrayOfStrings = (value, label, required = false) => {
+ if (required && value === undefined) {
+ throw new JWTClaimInvalid(`"${label}" claim is missing`, label, 'missing')
+ }
+
+ if (value !== undefined && (isNotString(value) && isNotArrayOfStrings(value))) {
+ throw new JWTClaimInvalid(`"${label}" claim must be a string or array of strings`, label, 'invalid')
+ }
+}
+
+module.exports = {
+ isNotArrayOfStrings,
+ isRequired,
+ isNotString,
+ isString,
+ isTimestamp,
+ isStringOrArrayOfStrings
+}
diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js
index 5941efe155..db341603e0 100644
--- a/lib/jwt/verify.js
+++ b/lib/jwt/verify.js
@@ -5,46 +5,25 @@ const getKey = require('../help/get_key')
const { bare: verify } = require('../jws/verify')
const { JWTClaimInvalid, JWTExpired } = require('../errors')
-const { isString, isNotString } = require('./shared_validations')
+const {
+ isString,
+ isNotString,
+ isNotArrayOfStrings,
+ isTimestamp,
+ isStringOrArrayOfStrings
+} = require('./shared_validations')
const decode = require('./decode')
const isPayloadString = isString.bind(undefined, JWTClaimInvalid)
const isOptionString = isString.bind(undefined, TypeError)
-const IDTOKEN = 'id_token'
-const LOGOUTTOKEN = 'logout_token'
-const ATJWT = 'at+JWT'
-
-const isTimestamp = (value, label, required = false) => {
- if (required && value === undefined) {
- throw new JWTClaimInvalid(`"${label}" claim is missing`, label, 'missing')
- }
-
- if (value !== undefined && (typeof value !== 'number')) {
- throw new JWTClaimInvalid(`"${label}" claim must be a JSON numeric value`, label, 'invalid')
- }
-}
-
-const isStringOrArrayOfStrings = (value, label, required = false) => {
- if (required && value === undefined) {
- throw new JWTClaimInvalid(`"${label}" claim is missing`, label, 'missing')
- }
-
- if (value !== undefined && (isNotString(value) && isNotArrayOfStrings(value))) {
- throw new JWTClaimInvalid(`"${label}" claim must be a string or array of strings`, label, 'invalid')
- }
-}
-
-const isNotArrayOfStrings = val => !Array.isArray(val) || val.length === 0 || val.some(isNotString)
const normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, '')
const validateOptions = ({
algorithms, audience, clockTolerance, complete = false, crit, ignoreExp = false,
- ignoreIat = false, ignoreNbf = false, issuer, jti, maxAuthAge, maxTokenAge, nonce, now = new Date(),
- profile, subject, typ
+ ignoreIat = false, ignoreNbf = false, issuer, jti, maxTokenAge, now = new Date(),
+ subject, typ
}) => {
- isOptionString(profile, 'options.profile')
-
if (typeof complete !== 'boolean') {
throw new TypeError('options.complete must be a boolean')
}
@@ -63,7 +42,6 @@ const validateOptions = ({
isOptionString(maxTokenAge, 'options.maxTokenAge')
isOptionString(subject, 'options.subject')
- isOptionString(maxAuthAge, 'options.maxAuthAge')
isOptionString(jti, 'options.jti')
isOptionString(clockTolerance, 'options.clockTolerance')
isOptionString(typ, 'options.typ')
@@ -80,8 +58,6 @@ const validateOptions = ({
throw new TypeError('options.algorithms must be an array of strings')
}
- isOptionString(nonce, 'options.nonce')
-
if (!(now instanceof Date) || !now.getTime()) {
throw new TypeError('options.now must be a valid Date object')
}
@@ -94,45 +70,6 @@ const validateOptions = ({
throw new TypeError('options.crit must be an array of strings')
}
- switch (profile) {
- case IDTOKEN:
- if (!issuer) {
- throw new TypeError('"issuer" option is required to validate an ID Token')
- }
-
- if (!audience) {
- throw new TypeError('"audience" option is required to validate an ID Token')
- }
-
- break
- case ATJWT:
- if (!issuer) {
- throw new TypeError('"issuer" option is required to validate a JWT Access Token')
- }
-
- if (!audience) {
- throw new TypeError('"audience" option is required to validate a JWT Access Token')
- }
-
- typ = ATJWT
-
- break
- case LOGOUTTOKEN:
- if (!issuer) {
- throw new TypeError('"issuer" option is required to validate a Logout Token')
- }
-
- if (!audience) {
- throw new TypeError('"audience" option is required to validate a Logout Token')
- }
-
- break
- case undefined:
- break
- default:
- throw new TypeError(`unsupported options.profile value "${profile}"`)
- }
-
return {
algorithms,
audience,
@@ -144,67 +81,27 @@ const validateOptions = ({
ignoreNbf,
issuer,
jti,
- maxAuthAge,
maxTokenAge,
- nonce,
now,
- profile,
subject,
typ
}
}
-const validateTypes = ({ header, payload }, profile, options) => {
+const validateTypes = ({ header, payload }, options) => {
isPayloadString(header.alg, '"alg" header parameter', 'alg', true)
- isTimestamp(payload.iat, 'iat', profile === IDTOKEN || profile === LOGOUTTOKEN || profile === ATJWT || !!options.maxTokenAge)
- isTimestamp(payload.exp, 'exp', profile === IDTOKEN || profile === ATJWT)
- isTimestamp(payload.auth_time, 'auth_time', !!options.maxAuthAge)
+ isTimestamp(payload.iat, 'iat', !!options.maxTokenAge)
+ isTimestamp(payload.exp, 'exp')
isTimestamp(payload.nbf, 'nbf')
- isPayloadString(payload.jti, '"jti" claim', 'jti', profile === LOGOUTTOKEN || profile === ATJWT || !!options.jti)
- isPayloadString(payload.acr, '"acr" claim', 'acr')
- isPayloadString(payload.nonce, '"nonce" claim', 'nonce', !!options.nonce)
+ isPayloadString(payload.jti, '"jti" claim', 'jti', !!options.jti)
isStringOrArrayOfStrings(payload.iss, 'iss', !!options.issuer)
- isPayloadString(payload.sub, '"sub" claim', 'sub', profile === IDTOKEN || profile === ATJWT || !!options.subject)
+ isPayloadString(payload.sub, '"sub" claim', 'sub', !!options.subject)
isStringOrArrayOfStrings(payload.aud, 'aud', !!options.audience)
- isPayloadString(payload.azp, '"azp" claim', 'azp', profile === IDTOKEN && Array.isArray(payload.aud) && payload.aud.length > 1)
- isStringOrArrayOfStrings(payload.amr, 'amr')
isPayloadString(header.typ, '"typ" header parameter', 'typ', !!options.typ)
-
- if (profile === ATJWT) {
- isPayloadString(payload.client_id, '"client_id" claim', 'client_id', true)
- }
-
- if (profile === LOGOUTTOKEN) {
- 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')
- }
- }
}
-const checkAudiencePresence = (audPayload, audOption, profile) => {
+const checkAudiencePresence = (audPayload, audOption) => {
if (typeof audPayload === 'string') {
return audOption.includes(audPayload)
}
@@ -222,7 +119,7 @@ module.exports = (token, key, options = {}) => {
const {
algorithms, audience, clockTolerance, complete, crit, ignoreExp, ignoreIat, ignoreNbf, issuer,
- jti, maxAuthAge, maxTokenAge, nonce, now, profile, subject, typ
+ jti, maxTokenAge, now, subject, typ
} = options = validateOptions(options)
const decoded = decode(token, { complete: true })
@@ -236,16 +133,12 @@ module.exports = (token, key, options = {}) => {
}
const unix = epoch(now)
- validateTypes(decoded, profile, options)
+ validateTypes(decoded, options)
if (issuer && (typeof decoded.payload.iss !== 'string' || !(typeof issuer === 'string' ? [issuer] : issuer).includes(decoded.payload.iss))) {
throw new JWTClaimInvalid('unexpected "iss" claim value', 'iss', 'check_failed')
}
- if (nonce && decoded.payload.nonce !== nonce) {
- throw new JWTClaimInvalid('unexpected "nonce" claim value', 'nonce', 'check_failed')
- }
-
if (subject && decoded.payload.sub !== subject) {
throw new JWTClaimInvalid('unexpected "sub" claim value', 'sub', 'check_failed')
}
@@ -254,7 +147,7 @@ module.exports = (token, key, options = {}) => {
throw new JWTClaimInvalid('unexpected "jti" claim value', 'jti', 'check_failed')
}
- if (audience && !checkAudiencePresence(decoded.payload.aud, typeof audience === 'string' ? [audience] : audience, profile)) {
+ if (audience && !checkAudiencePresence(decoded.payload.aud, typeof audience === 'string' ? [audience] : audience)) {
throw new JWTClaimInvalid('unexpected "aud" claim value', 'aud', 'check_failed')
}
@@ -264,13 +157,6 @@ module.exports = (token, key, options = {}) => {
const tolerance = clockTolerance ? secs(clockTolerance) : 0
- if (maxAuthAge) {
- const maxAuthAgeSeconds = secs(maxAuthAge)
- if (decoded.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 (!ignoreIat && !('exp' in decoded.payload) && 'iat' in decoded.payload && decoded.payload.iat > unix + tolerance) {
throw new JWTClaimInvalid('"iat" claim timestamp check failed (it should be in the past)', 'iat', 'check_failed')
}
@@ -296,9 +182,5 @@ module.exports = (token, key, options = {}) => {
}
}
- if (profile === IDTOKEN && Array.isArray(decoded.payload.aud) && decoded.payload.aud.length > 1 && decoded.payload.azp !== audience) {
- throw new JWTClaimInvalid('unexpected "azp" claim value', 'azp', 'check_failed')
- }
-
return complete ? decoded : decoded.payload
}
diff --git a/test/jwt/verify.test.js b/test/jwt/verify.test.js
index 9bd13aa602..5e030fa813 100644
--- a/test/jwt/verify.test.js
+++ b/test/jwt/verify.test.js
@@ -5,10 +5,10 @@ const { JWS, JWT, JWK, JWKS, errors } = require('../..')
const key = JWK.generateSync('oct')
const token = JWT.sign({}, key, { iat: false })
-const string = (t, option) => {
+const string = (t, option, method = JWT.verify, opts) => {
;['', false, [], {}, Buffer, Buffer.from('foo'), 0, Infinity].forEach((val) => {
t.throws(() => {
- JWT.verify(token, key, { [option]: val })
+ method(token, key, { ...opts, [option]: val })
}, { instanceOf: TypeError, message: `options.${option} must be a string` })
})
}
@@ -36,10 +36,7 @@ test('options must be an object', t => {
test('options.clockTolerance must be a string', string, 'clockTolerance')
test('options.jti must be a string', string, 'jti')
-test('options.profile must be a string', string, 'profile')
-test('options.maxAuthAge must be a string', string, 'maxAuthAge')
test('options.maxTokenAge must be a string', string, 'maxTokenAge')
-test('options.nonce must be a string', string, 'nonce')
test('options.subject must be a string', string, 'subject')
const boolean = (t, option) => {
@@ -112,7 +109,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => {
}, { instanceOf: TypeError, message: 'options.ignoreIat and options.maxTokenAge cannot used together' })
})
-;['iat', 'exp', 'auth_time', 'nbf'].forEach((claim) => {
+;['iat', 'exp', 'nbf'].forEach((claim) => {
test(`"${claim} must be a timestamp when provided"`, t => {
;['', 'foo', true, null, [], {}].forEach((val) => {
const err = t.throws(() => {
@@ -126,7 +123,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => {
})
})
-;['jti', 'acr', 'nonce', 'sub', 'azp'].forEach((claim) => {
+;['jti', 'sub'].forEach((claim) => {
test(`"${claim} must be a string when provided"`, t => {
;['', 0, 1, true, null, [], {}].forEach((val) => {
const err = t.throws(() => {
@@ -140,7 +137,7 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => {
})
})
-;['aud', 'amr', 'iss'].forEach((claim) => {
+;['aud', 'iss'].forEach((claim) => {
test(`"${claim} must be a string when provided"`, t => {
;['', 0, 1, true, null, [], {}].forEach((val) => {
let err
@@ -164,7 +161,6 @@ test('options.ignoreIat & options.maxTokenAge may not be used together', t => {
Object.entries({
issuer: 'iss',
jti: 'jti',
- nonce: 'nonce',
subject: 'sub'
}).forEach(([option, claim]) => {
test(`option.${option} validation fails`, t => {
@@ -266,33 +262,9 @@ test('option.audience validation success', t => {
t.pass()
})
-test('option.maxAuthAge requires iat to be in the payload', t => {
- const err = t.throws(() => {
- const invalid = JWS.sign({}, key)
- JWT.verify(invalid, key, { maxAuthAge: '30s' })
- }, { instanceOf: errors.JWTClaimInvalid, message: '"auth_time" claim is missing' })
- t.is(err.claim, 'auth_time')
- t.is(err.reason, 'missing')
-})
-
const epoch = 1265328501
const now = new Date(epoch * 1000)
-test('option.maxAuthAge checks auth_time', t => {
- const err = t.throws(() => {
- const invalid = JWS.sign({ auth_time: epoch - 31 }, key)
- JWT.verify(invalid, key, { maxAuthAge: '30s', now })
- }, { 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('option.maxAuthAge checks auth_time (with tolerance)', t => {
- const token = JWT.sign({ auth_time: epoch - 31 }, key, { now })
- JWT.verify(token, key, { maxAuthAge: '30s', now, clockTolerance: '1s' })
- t.pass()
-})
-
test('option.maxTokenAge requires iat to be in the payload', t => {
const err = t.throws(() => {
const invalid = JWS.sign({}, key)
@@ -449,133 +421,175 @@ test('nbf check (passed because of ignoreIat)', t => {
t.pass()
})
-// JWT options.profile
-test('must be a supported value', t => {
- t.throws(() => {
- JWT.verify('foo', key, { profile: 'foo' })
- }, { instanceOf: TypeError, message: 'unsupported options.profile value "foo"' })
-})
-
{
const token = JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'client_id' })
- test('profile=id_token', t => {
- JWT.verify(token, key, { profile: 'id_token', 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('profile=id_token requires issuer option too', t => {
- t.throws(() => {
- JWT.verify(token, key, { profile: 'id_token' })
- }, { instanceOf: TypeError, message: '"issuer" option is required to validate an ID Token' })
+ 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('profile=id_token requires audience option too', t => {
- t.throws(() => {
- JWT.verify(token, key, { profile: 'id_token', issuer: 'issuer' })
- }, { instanceOf: TypeError, message: '"audience" 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('profile=id_token mandates exp to be present', t => {
+ test('IdToken.verify mandates exp to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ }, key, { subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"exp" claim is missing' })
t.is(err.claim, 'exp')
t.is(err.reason, 'missing')
})
- test('profile=id_token mandates iat to be present', t => {
+ test('IdToken.verify mandates iat to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', iat: false, subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
- test('profile=id_token mandates sub to be present', t => {
+ test('IdToken.verify mandates sub to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"sub" claim is missing' })
t.is(err.claim, 'sub')
t.is(err.reason, 'missing')
})
- test('profile=id_token mandates iss to be present', t => {
+ test('IdToken.verify mandates iss to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', audience: 'client_id' }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iss" claim is missing' })
t.is(err.claim, 'iss')
t.is(err.reason, 'missing')
})
- test('profile=id_token mandates aud to be present', t => {
+ test('IdToken.verify mandates aud to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer' }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"aud" claim is missing' })
t.is(err.claim, 'aud')
t.is(err.reason, 'missing')
})
- test('profile=id_token mandates azp to be present when multiple audiences are used', t => {
+ test('IdToken.verify mandates azp to be present when multiple audiences are used', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: ['client_id', 'another audience'] }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"azp" claim is missing' })
t.is(err.claim, 'azp')
t.is(err.reason, 'missing')
})
- test('profile=id_token mandates azp to match the audience when required', t => {
+ test('IdToken.verify mandates azp to match the audience when required', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ azp: 'mismatched' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: ['client_id', 'another audience'] }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { 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('profile=id_token validates full id tokens', t => {
+ test('IdToken.verify validates full id tokens', t => {
t.notThrows(() => {
- JWT.verify(
+ JWT.IdToken.verify(
JWT.sign({ azp: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: ['client_id', 'another audience'] }),
key,
- { profile: 'id_token', issuer: 'issuer', audience: 'client_id' }
+ { 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')
+ })
}
{
@@ -585,164 +599,163 @@ test('must be a supported value', t => {
}
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' })
- test('profile=logout_token', t => {
- JWT.verify(token, key, { profile: 'logout_token', 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('profile=logout_token requires issuer option too', t => {
- t.throws(() => {
- JWT.verify(token, key, { profile: 'logout_token' })
- }, { instanceOf: TypeError, message: '"issuer" option is required to validate a Logout Token' })
+ 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('profile=logout_token requires audience option too', t => {
- t.throws(() => {
- JWT.verify(token, key, { profile: 'logout_token', issuer: 'issuer' })
- }, { instanceOf: TypeError, message: '"audience" 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('profile=logout_token mandates jti to be present', t => {
+ test('LogoutToken.verify mandates jti to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ }, key, { subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"jti" claim is missing' })
t.is(err.claim, 'jti')
t.is(err.reason, 'missing')
})
- test('profile=logout_token mandates events to be present', t => {
+ test('LogoutToken.verify mandates events to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"events" claim is missing' })
t.is(err.claim, 'events')
t.is(err.reason, 'missing')
})
- test('profile=logout_token mandates events to be an object', t => {
+ test('LogoutToken.verify mandates events to be an object', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({
events: []
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { 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('profile=logout_token mandates events to have the backchannel logout member', t => {
+ test('LogoutToken.verify mandates events to have the backchannel logout member', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({
events: {}
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { 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('profile=logout_token mandates events to have the backchannel logout member thats an object', t => {
+ test('LogoutToken.verify mandates events to have the backchannel logout member thats an object', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({
events: {
'http://schemas.openid.net/event/backchannel-logout': []
}
}, key, { jti: 'foo', subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { 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('profile=logout_token mandates iat to be present', t => {
+ test('LogoutToken.verify mandates iat to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', iat: false, subject: 'subject', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
- test('profile=logout_token mandates sub or sid to be present', t => {
+ test('LogoutToken.verify mandates sub or sid to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { 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('profile=logout_token mandates sid to be a string when present', t => {
+ test('LogoutToken.verify mandates sid to be a string when present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ sid: true }, key, { jti: 'foo', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { 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('profile=logout_token prohibits nonce', t => {
+ test('LogoutToken.verify prohibits nonce', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ nonce: 'foo' }, key, { subject: 'subject', jti: 'foo', issuer: 'issuer', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"nonce" claim is prohibited' })
t.is(err.claim, 'nonce')
t.is(err.reason, 'prohibited')
})
- test('profile=logout_token mandates iss to be present', t => {
+ test('LogoutToken.verify mandates iss to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', subject: 'subject', audience: 'client_id' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iss" claim is missing' })
t.is(err.claim, 'iss')
t.is(err.reason, 'missing')
})
- test('profile=logout_token mandates aud to be present', t => {
+ test('LogoutToken.verify mandates aud to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.LogoutToken.verify(
JWT.sign({ }, key, { jti: 'foo', subject: 'subject', issuer: 'issuer' }),
key,
- { profile: 'logout_token', issuer: 'issuer', audience: 'client_id' }
+ { issuer: 'issuer', audience: 'client_id' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"aud" claim is missing' })
t.is(err.claim, 'aud')
@@ -753,135 +766,172 @@ test('must be a supported value', t => {
{
const token = JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } })
- test('profile=at+JWT', t => {
- JWT.verify(token, key, { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' })
+ 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('profile=at+JWT requires issuer option too', t => {
- t.throws(() => {
- JWT.verify(token, key, { profile: 'at+JWT' })
- }, { instanceOf: TypeError, message: '"issuer" option is required to validate a JWT Access Token' })
+ 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('profile=at+JWT requires audience option too', t => {
- t.throws(() => {
- JWT.verify(token, key, { profile: 'at+JWT', issuer: 'issuer' })
- }, { instanceOf: TypeError, message: '"audience" 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('profile=at+JWT mandates exp to be present', t => {
+ test('AccessToken.verify mandates exp to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"exp" claim is missing' })
t.is(err.claim, 'exp')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates client_id to be present', t => {
+ test('AccessToken.verify mandates client_id to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { 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('profile=at+JWT mandates jti to be present', t => {
+ test('AccessToken.verify mandates jti to be present', t => {
const err = t.throws(() => {
- JWT.verify(
- JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS' }),
+ JWT.AccessToken.verify(
+ JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"jti" claim is missing' })
t.is(err.claim, 'jti')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates iat to be present', t => {
+ test('AccessToken.verify mandates iat to be present', t => {
const err = t.throws(() => {
- JWT.verify(
- JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', iat: false }),
+ JWT.AccessToken.verify(
+ JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', iat: false, header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iat" claim is missing' })
t.is(err.claim, 'iat')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates sub to be present', t => {
+ test('AccessToken.verify mandates sub to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', issuer: 'issuer', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"sub" claim is missing' })
t.is(err.claim, 'sub')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates iss to be present', t => {
+ test('AccessToken.verify mandates iss to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', jti: 'random', header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"iss" claim is missing' })
t.is(err.claim, 'iss')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates aud to be present', t => {
+ test('AccessToken.verify mandates aud to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', jti: 'random', header: { typ: 'at+JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"aud" claim is missing' })
t.is(err.claim, 'aud')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates header typ to be present', t => {
+ test('AccessToken.verify mandates header typ to be present', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', jti: 'random', issuer: 'issuer' }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { issuer: 'issuer', audience: 'RS' }
)
}, { instanceOf: errors.JWTClaimInvalid, message: '"typ" header parameter is missing' })
t.is(err.claim, 'typ')
t.is(err.reason, 'missing')
})
- test('profile=at+JWT mandates header typ to be present and of the right value', t => {
+ test('AccessToken.verify mandates header typ to be present and of the right value', t => {
const err = t.throws(() => {
- JWT.verify(
+ JWT.AccessToken.verify(
JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', jti: 'random', issuer: 'issuer', header: { typ: 'JWT' } }),
key,
- { profile: 'at+JWT', issuer: 'issuer', audience: 'RS' }
+ { 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')
+ })
+ })
}
diff --git a/types/index.d.ts b/types/index.d.ts
index 1eaf284183..f7f8929587 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -22,7 +22,6 @@ export type Curves = OKPCurve | ECCurve;
export type keyType = 'RSA' | 'EC' | 'OKP' | 'oct';
export type asymmetricKeyObjectTypes = 'private' | 'public';
export type keyObjectTypes = asymmetricKeyObjectTypes | 'secret';
-export type JWTProfiles = 'id_token' | 'at+JWT' | 'logout_token';
export type KeyInput = PrivateKeyInput | PublicKeyInput | string | Buffer;
export type ProduceKeyInput = JWK.Key | KeyObject | KeyInput | JWKOctKey | JWKRSAKey | JWKECKey | JWKOKPKey;
export type ConsumeKeyInput = ProduceKeyInput | JWKS.KeyStore;
@@ -444,16 +443,13 @@ export namespace JWT {
maxTokenAge?: string;
subject?: string;
issuer?: string | string[];
- maxAuthAge?: string;
jti?: string;
clockTolerance?: string;
audience?: string | string[];
algorithms?: string[];
- nonce?: string;
typ?: string;
now?: Date;
crit?: string[];
- profile?: JWTProfiles;
}
function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true }): completeResult;
@@ -476,28 +472,38 @@ export namespace JWT {
function sign(payload: object, key: ProduceKeyInputWithNone, options?: SignOptions): string;
- interface VerifyProfileOptions {
+ interface ProfiledVerifyOptions {
issuer: string | string[];
audience: string | string[];
- profile?: profile;
}
+ interface IdTokenVerifyOptions extends ProfiledVerifyOptions {
+ nonce?: string;
+ maxAuthAge?: string;
+ }
+
+ interface AccessTokenVerifyOptions extends ProfiledVerifyOptions {
+ maxAuthAge?: string;
+ }
+
+ interface LogoutTokenVerifyOptions extends ProfiledVerifyOptions {}
+
namespace IdToken {
- function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & { complete: true } & VerifyProfileOptions<'id_token'>): completeResult;
- function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true } & VerifyProfileOptions<'id_token'>): completeResult;
- function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'id_token'>): object;
+ function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & { complete: true } & IdTokenVerifyOptions): completeResult;
+ function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true } & IdTokenVerifyOptions): completeResult;
+ function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & IdTokenVerifyOptions): object;
}
namespace LogoutToken {
- function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & { complete: true } & VerifyProfileOptions<'logout_token'>): completeResult;
- function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true } & VerifyProfileOptions<'logout_token'>): completeResult;
- function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'logout_token'>): object;
+ function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & { complete: true } & LogoutTokenVerifyOptions): completeResult;
+ function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true } & LogoutTokenVerifyOptions): completeResult;
+ function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & LogoutTokenVerifyOptions): object;
}
namespace AccessToken {
- function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & { complete: true } & VerifyProfileOptions<'at+JWT'>): completeResult;
- function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true } & VerifyProfileOptions<'at+JWT'>): completeResult;
- function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & VerifyProfileOptions<'at+JWT'>): object;
+ function verify(jwt: string, key: ConsumeKeyInput | EmbeddedVerifyKeys, options: VerifyOptions & { complete: true } & AccessTokenVerifyOptions): completeResult;
+ function verify(jwt: string, key: NoneKey, options: VerifyOptions & { complete: true } & AccessTokenVerifyOptions): completeResult;
+ function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & AccessTokenVerifyOptions): object;
}
}