diff --git a/README.md b/README.md
index 57fc3e1063..efe86caede 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,12 @@ Available JWT validation profiles
- Generic JWT
- OIDC ID Token (`id_token`) - [OpenID Connect Core 1.0][spec-oidc-id_token]
-- OAuth 2.0 JWT Access Tokens (`at+JWT`) - [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt]
-- OIDC Logout Token (`logout_token`) - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token]
+- (draft 04) OIDC Logout Token (`logout_token`) - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token]
+- (draft 06) OAuth 2.0 JWT Access Tokens (`at+JWT`) - [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt]
+
+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.
## Sponsor
@@ -305,12 +309,13 @@ jose.JWE.decrypt(
| AES_CBC_HMAC_SHA2 | ✓ | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 |
| (X)ChaCha | ✓ via [plugin][plugin-chacha] | C20P, XC20P |
-| JWT profile validation | Supported | profile option value |
-| -- | -- | -- |
-| ID Token - [OpenID Connect Core 1.0][spec-oidc-id_token] | ✓ | `id_token` |
-| JWT Access Tokens [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt] | ✓ | `at+JWT` |
-| Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token] | ✓ | `logout_token` |
-| JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] | ◯ ||
+| JWT profile validation | Supported | Stable profile | profile option value |
+| -- | -- | -- | -- |
+| ID Token - [OpenID Connect Core 1.0][spec-oidc-id_token] | ✓ | ✓ | `id_token` |
+| JWT Access Tokens [JWT Profile for OAuth 2.0 Access Tokens][draft-ietf-oauth-access-token-jwt] | ✓ | ✕5 | `at+JWT` |
+| Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token] | ✓ | ✕5 | `logout_token` |
+| JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] | ◯ |||
+| [JWT Response for OAuth Token Introspection][draft-jwtintrospection] | ◯ |||
Legend:
- **✓** Implemented
@@ -322,7 +327,10 @@ Legend:
operations but it is an entirely opt-in behaviour, downgrade attacks are prevented by the required
use of a special `JWK.Key`-like object that cannot be instantiated through the key import API
3 RSAES OAEP using SHA-2 and MGF1 with SHA-2 is only supported when Node.js >= 12.9.0 runtime is detected
-4 ECDH-ES with X25519 and X448 keys is only supported when Node.js >= 13.9.0 runtime is detected
+4 ECDH-ES with X25519 and X448 keys is only supported when Node.js >= 13.9.0 runtime is detected
+5 Draft specification 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.
## FAQ
@@ -389,11 +397,12 @@ in terms of performance and API (not having well defined errors).
[spec-jwt]: https://tools.ietf.org/html/rfc7519
[spec-okp]: https://tools.ietf.org/html/rfc8037
[draft-secp256k1]: https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms-04
-[draft-ietf-oauth-access-token-jwt]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt
+[draft-ietf-oauth-access-token-jwt]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-06
[draft-jarm]: https://openid.net/specs/openid-financial-api-jarm.html
+[draft-jwtintrospection]: https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response
[spec-thumbprint]: https://tools.ietf.org/html/rfc7638
[spec-oidc-id_token]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
-[spec-oidc-logout_token]: https://openid.net/specs/openid-connect-backchannel-1_0.html#LogoutToken
+[spec-oidc-logout_token]: https://openid.net/specs/openid-connect-backchannel-1_0-04.html#LogoutToken
[oidc-token-hash]: https://www.npmjs.com/package/oidc-token-hash
[support-sponsor]: https://github.com/sponsors/panva
[sponsor-auth0]: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=panva-jose&utm_content=auth
diff --git a/docs/README.md b/docs/README.md
index 9f8e92319e..caed72e846 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -888,9 +888,11 @@ Verifies the claims and signature of a JSON Web Token.
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', and 'logout_token'. **Default:** 'undefined'
+ 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.
+ `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
diff --git a/lib/jwt/verify.js b/lib/jwt/verify.js
index b16b2f81b5..04cd5f7f11 100644
--- a/lib/jwt/verify.js
+++ b/lib/jwt/verify.js
@@ -153,11 +153,11 @@ const validateOptions = ({
const validateTypes = ({ header, payload }, profile, options) => {
isPayloadString(header.alg, '"alg" header parameter', 'alg', true)
- isTimestamp(payload.iat, 'iat', profile === IDTOKEN || profile === LOGOUTTOKEN || !!options.maxTokenAge)
+ 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.nbf, 'nbf')
- isPayloadString(payload.jti, '"jti" claim', 'jti', profile === LOGOUTTOKEN || !!options.jti)
+ 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.iss, '"iss" claim', 'iss', !!options.issuer)
diff --git a/test/jwt/verify.test.js b/test/jwt/verify.test.js
index 99d5828f90..3d53742f75 100644
--- a/test/jwt/verify.test.js
+++ b/test/jwt/verify.test.js
@@ -723,7 +723,7 @@ test('must be a supported value', t => {
}
{
- const token = JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', header: { typ: 'at+JWT' } })
+ 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' })
@@ -752,7 +752,7 @@ test('must be a supported value', t => {
test('profile=at+JWT mandates exp to be present', t => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ client_id: 'client_id' }, key, { subject: 'subject', issuer: 'issuer', audience: 'RS', header: { typ: 'at+JWT' } }),
+ 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' }
)
@@ -764,7 +764,7 @@ test('must be a supported value', t => {
test('profile=at+JWT mandates client_id to be present', t => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', header: { typ: 'at+JWT' } }),
+ 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' }
)
@@ -773,10 +773,34 @@ test('must be a supported value', t => {
t.is(err.reason, 'missing')
})
+ test('profile=at+JWT mandates jti to be present', t => {
+ const err = t.throws(() => {
+ JWT.verify(
+ JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS' }),
+ key,
+ { profile: 'at+JWT', 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 => {
+ const err = t.throws(() => {
+ JWT.verify(
+ JWT.sign({ }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', audience: 'RS', iat: false }),
+ key,
+ { profile: 'at+JWT', 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 => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', issuer: 'issuer', audience: 'RS', header: { typ: 'at+JWT' } }),
+ 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' }
)
@@ -788,7 +812,7 @@ test('must be a supported value', t => {
test('profile=at+JWT mandates iss to be present', t => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', header: { typ: 'at+JWT' } }),
+ 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' }
)
@@ -800,7 +824,7 @@ test('must be a supported value', t => {
test('profile=at+JWT mandates aud to be present', t => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', issuer: 'issuer', header: { typ: 'at+JWT' } }),
+ 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' }
)
@@ -812,7 +836,7 @@ test('must be a supported value', t => {
test('profile=at+JWT mandates header typ to be present', t => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', issuer: 'issuer' }),
+ 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' }
)
@@ -824,7 +848,7 @@ test('must be a supported value', t => {
test('profile=at+JWT mandates header typ to be present and of the right value', t => {
const err = t.throws(() => {
JWT.verify(
- JWT.sign({ client_id: 'client_id' }, key, { expiresIn: '10m', subject: 'subject', audience: 'RS', issuer: 'issuer', header: { typ: 'JWT' } }),
+ 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' }
)