From fd69d7f5093d0b3a231d7d79aa3bca3a8a64464c Mon Sep 17 00:00:00 2001
From: Filip Skokan <panva.ip@gmail.com>
Date: Fri, 4 Sep 2020 15:33:24 +0200
Subject: [PATCH] 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`
---
 .github/workflows/test.yml    |   4 +
 README.md                     |  12 +-
 docs/README.md                |  50 +++--
 lib/jwt/profiles.js           | 167 +++++++++++++++-
 lib/jwt/shared_validations.js |  41 +++-
 lib/jwt/verify.js             | 154 ++-------------
 test/jwt/verify.test.js       | 362 +++++++++++++++++++---------------
 types/index.d.ts              |  36 ++--
 8 files changed, 494 insertions(+), 332 deletions(-)

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(
   <summary><em><strong>Verifying OIDC ID Tokens</strong></em> (Click to expand)</summary><br>
 
 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`: `<string>` 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>` &vert; `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`: `<string>` 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>` &vert; `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`: `<string>` Expected jti value. An exact match must be found in the payload.
-  - `maxAuthAge`: `<string>` 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`: `<string>` 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`: `<string>` Expected nonce value. An exact match must be found in the payload. See
-    [OpenID Connect Core 1.0][connect-core] for details.
   - `now`: `<Date>` Date object to be used instead of the current unix epoch timestamp.
     **Default:** 'new Date()'
   - `subject`: `<string>` 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`: `<string>` REQUIRED
+- `audience`: `<string>` REQUIRED
+- `maxAuthAge`: `<string>` 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.
 
 <details>
 <summary><em><strong>Example</strong></em> (Click to expand)</summary>
@@ -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`: `<string>` REQUIRED
+- `audience`: `<string>` REQUIRED
+- `maxAuthAge`: `<string>` 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`: `<string>` Expected nonce value. An exact match must be found in the payload. See
+  [OpenID Connect Core 1.0][connect-core] for details.
+
 <details>
 <summary><em><strong>Example</strong></em> (Click to expand)</summary>
 
@@ -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`: `<string>` REQUIRED
+- `audience`: `<string>` REQUIRED
 
 <details>
 <summary><em><strong>Example</strong></em> (Click to expand)</summary>
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<NoneKey>;
@@ -476,28 +472,38 @@ export namespace JWT {
 
   function sign(payload: object, key: ProduceKeyInputWithNone, options?: SignOptions): string;
 
-  interface VerifyProfileOptions<profile> {
+  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<NoneKey>;
-    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<NoneKey>;
+    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<NoneKey>;
-    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<NoneKey>;
+    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<NoneKey>;
-    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<NoneKey>;
+    function verify(jwt: string, key: ConsumeKeyInputWithNone | EmbeddedVerifyKeys, options: VerifyOptions & AccessTokenVerifyOptions): object;
   }
 }