Skip to content

Commit

Permalink
feat: update JWT Profile for OAuth 2.0 Access Tokens to latest draft
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `at+JWT` JWT draft profile - in the draft's Section 2.2
the claims `iat` and `jti` are now REQUIRED (was RECOMMENDED).
  • Loading branch information
panva committed Apr 16, 2020
1 parent 2ebba8e commit 8c0a8a9
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 23 deletions.
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -305,12 +309,13 @@ jose.JWE.decrypt(
| AES_CBC_HMAC_SHA2 || A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 |
| (X)ChaCha | ✓ <sup>via [plugin][plugin-chacha]</sup> | 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] || ✕<sup>5</sup> | `at+JWT` |
| Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_token] || ✕<sup>5</sup> | `logout_token` |
| JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] ||||
| [JWT Response for OAuth Token Introspection][draft-jwtintrospection] ||||

Legend:
- **** Implemented
Expand All @@ -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
<sup>3</sup> RSAES OAEP using SHA-2 and MGF1 with SHA-2 is only supported when Node.js >= 12.9.0 runtime is detected
<sup>4</sup> ECDH-ES with X25519 and X448 keys is only supported when Node.js >= 13.9.0 runtime is detected
<sup>4</sup> ECDH-ES with X25519 and X448 keys is only supported when Node.js >= 13.9.0 runtime is detected
<sup>5</sup> 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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`: `<string>` 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>` &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
Expand Down
4 changes: 2 additions & 2 deletions lib/jwt/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 32 additions & 8 deletions test/jwt/verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down Expand Up @@ -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' }
)
Expand All @@ -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' }
)
Expand All @@ -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' }
)
Expand All @@ -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' }
)
Expand All @@ -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' }
)
Expand All @@ -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' }
)
Expand All @@ -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' }
)
Expand Down

0 comments on commit 8c0a8a9

Please sign in to comment.