Comparing version 1.17.2 to 1.18.0
@@ -5,2 +5,11 @@ # Change Log | ||
# [1.18.0](https://github.com/panva/jose/compare/v1.17.2...v1.18.0) (2019-12-31) | ||
### Features | ||
* add JWT validation profiles for Access Tokens and Logout Tokens ([7bb5c95](https://github.com/panva/jose/commit/7bb5c953a9c6d9bd915e8ebc0608bc0649427745)) | ||
## [1.17.2](https://github.com/panva/jose/compare/v1.17.1...v1.17.2) (2019-12-17) | ||
@@ -7,0 +16,0 @@ |
@@ -15,2 +15,6 @@ const isObject = require('../help/is_object') | ||
const IDTOKEN = 'id_token' | ||
const LOGOUTTOKEN = 'logout_token' | ||
const ATJWT = 'at+JWT' | ||
const isTimestamp = (value, label, required = false) => { | ||
@@ -87,3 +91,3 @@ if (required && value === undefined) { | ||
switch (options.profile) { | ||
case 'id_token': | ||
case IDTOKEN: | ||
if (!options.issuer) { | ||
@@ -98,2 +102,22 @@ throw new TypeError('"issuer" option is required to validate an ID Token') | ||
break | ||
case ATJWT: | ||
if (!options.issuer) { | ||
throw new TypeError('"issuer" option is required to validate a JWT Access Token') | ||
} | ||
if (!options.audience) { | ||
throw new TypeError('"audience" option is required to validate a JWT Access Token') | ||
} | ||
break | ||
case LOGOUTTOKEN: | ||
if (!options.issuer) { | ||
throw new TypeError('"issuer" option is required to validate a Logout Token') | ||
} | ||
if (!options.audience) { | ||
throw new TypeError('"audience" option is required to validate a Logout Token') | ||
} | ||
break | ||
case undefined: | ||
@@ -106,18 +130,53 @@ break | ||
const validatePayloadTypes = (payload, profile) => { | ||
isTimestamp(payload.iat, 'iat', profile === 'id_token') | ||
isTimestamp(payload.exp, 'exp', profile === 'id_token') | ||
const validateTypes = ({ header, payload }, profile) => { | ||
isPayloadString(header.alg, '"alg" header parameter', true) | ||
isTimestamp(payload.iat, 'iat', profile === IDTOKEN || profile === LOGOUTTOKEN) | ||
isTimestamp(payload.exp, 'exp', profile === IDTOKEN || profile === ATJWT) | ||
isTimestamp(payload.auth_time, 'auth_time') | ||
isTimestamp(payload.nbf, 'nbf') | ||
isPayloadString(payload.jti, '"jti" claim') | ||
isPayloadString(payload.jti, '"jti" claim', profile === LOGOUTTOKEN) | ||
isPayloadString(payload.acr, '"acr" claim') | ||
isPayloadString(payload.nonce, '"nonce" claim') | ||
isPayloadString(payload.iss, '"iss" claim', profile === 'id_token') | ||
isPayloadString(payload.sub, '"sub" claim', profile === 'id_token') | ||
isStringOrArrayOfStrings(payload.aud, 'aud', profile === 'id_token') | ||
isPayloadString(payload.azp, '"azp" claim', profile === 'id_token' && Array.isArray(payload.aud) && payload.aud.length > 1) | ||
isPayloadString(payload.iss, '"iss" claim', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN) | ||
isPayloadString(payload.sub, '"sub" claim', profile === IDTOKEN || profile === ATJWT) | ||
isStringOrArrayOfStrings(payload.aud, 'aud', profile === IDTOKEN || profile === ATJWT || profile === LOGOUTTOKEN) | ||
isPayloadString(payload.azp, '"azp" claim', profile === IDTOKEN && Array.isArray(payload.aud) && payload.aud.length > 1) | ||
isStringOrArrayOfStrings(payload.amr, 'amr') | ||
if (profile === ATJWT) { | ||
isPayloadString(payload.client_id, '"client_id" claim', true) | ||
isPayloadString(header.typ, '"typ" header parameter', true) | ||
} | ||
if (profile === LOGOUTTOKEN) { | ||
isPayloadString(payload.sid, '"sid" claim') | ||
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') | ||
} | ||
if (!('events' in payload)) { | ||
throw new JWTClaimInvalid('"events" claim is missing') | ||
} | ||
if (!isObject(payload.events)) { | ||
throw new JWTClaimInvalid('"events" claim must be an object') | ||
} | ||
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') | ||
} | ||
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') | ||
} | ||
} | ||
} | ||
const checkAudiencePresence = (audPayload, audOption) => { | ||
const checkAudiencePresence = (audPayload, audOption, profile) => { | ||
if (typeof audPayload === 'string') { | ||
@@ -127,4 +186,13 @@ return audOption.includes(audPayload) | ||
audPayload = new Set(audPayload) | ||
return audOption.some(Set.prototype.has.bind(audPayload)) | ||
if (profile === ATJWT) { | ||
// reject if it contains additional audiences that are not known aliases of the resource | ||
// indicator of the current resource server | ||
audOption = new Set(audOption) | ||
return audPayload.every(Set.prototype.has.bind(audOption)) | ||
} else { | ||
// Each principal intended to process the JWT MUST | ||
// identify itself with a value in the audience claim | ||
audPayload = new Set(audPayload) | ||
return audOption.some(Set.prototype.has.bind(audPayload)) | ||
} | ||
} | ||
@@ -165,3 +233,3 @@ | ||
const decoded = decode(token, { complete: true }) | ||
validatePayloadTypes(decoded.payload, profile) | ||
validateTypes(decoded, profile) | ||
@@ -184,3 +252,3 @@ if (issuer && decoded.payload.iss !== issuer) { | ||
if (audience && !checkAudiencePresence(decoded.payload.aud, typeof audience === 'string' ? [audience] : audience)) { | ||
if (audience && !checkAudiencePresence(decoded.payload.aud, typeof audience === 'string' ? [audience] : audience, profile)) { | ||
throw new JWTClaimInvalid('audience mismatch') | ||
@@ -224,6 +292,10 @@ } | ||
if (profile === 'id_token' && Array.isArray(decoded.payload.aud) && decoded.payload.aud.length > 1 && decoded.payload.azp !== audience) { | ||
if (profile === IDTOKEN && Array.isArray(decoded.payload.aud) && decoded.payload.aud.length > 1 && decoded.payload.azp !== audience) { | ||
throw new JWTClaimInvalid('azp mismatch') | ||
} | ||
if (profile === ATJWT && decoded.header.typ !== ATJWT) { | ||
throw new JWTClaimInvalid('invalid JWT typ header value for the used validation profile') | ||
} | ||
key = getKey(key, true) | ||
@@ -230,0 +302,0 @@ |
{ | ||
"name": "jose", | ||
"version": "1.17.2", | ||
"version": "1.18.0", | ||
"description": "JSON Web Almost Everything - JWA, JWS, JWE, JWK, JWT, JWKS for Node.js with minimal dependencies", | ||
@@ -25,2 +25,6 @@ "keywords": [ | ||
"jwt", | ||
"access_token", | ||
"access token", | ||
"logout_token", | ||
"logout token", | ||
"secp256k1", | ||
@@ -33,2 +37,3 @@ "sign", | ||
"repository": "panva/jose", | ||
"funding": "https://github.com/sponsors/panva", | ||
"license": "MIT", | ||
@@ -41,3 +46,2 @@ "author": "Filip Skokan <panva.ip@gmail.com>", | ||
], | ||
"funding": "https://github.com/sponsors/panva", | ||
"main": "lib/index.js", | ||
@@ -62,2 +66,9 @@ "types": "types/index.d.ts", | ||
}, | ||
"ava": { | ||
"babel": false, | ||
"compileEnhancements": false, | ||
"files": [ | ||
"test/**/*.test.js" | ||
] | ||
}, | ||
"dependencies": { | ||
@@ -71,3 +82,3 @@ "asn1.js": "^5.2.0" | ||
"babel-eslint": "^10.0.3", | ||
"c8": "^6.0.1", | ||
"c8": "^7.0.0", | ||
"dtslint": "^2.0.0", | ||
@@ -80,9 +91,2 @@ "husky": "^3.0.9", | ||
}, | ||
"ava": { | ||
"babel": false, | ||
"compileEnhancements": false, | ||
"files": [ | ||
"test/**/*.test.js" | ||
] | ||
}, | ||
"standard": { | ||
@@ -89,0 +93,0 @@ "parser": "babel-eslint" |
@@ -27,3 +27,5 @@ # jose | ||
- Generic JWT | ||
- ID Token (id_token) - [OpenID Connect Core 1.0][spec-oidc-id_token] | ||
- 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] | ||
@@ -81,4 +83,4 @@ <details> | ||
| 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] | ◯ || | ||
| Logout Token - [OpenID Connect Back-Channel Logout 1.0][spec-oidc-logout_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] | ◯ || | ||
@@ -229,2 +231,5 @@ | ||
<details> | ||
<summary><em><strong>Verifying OIDC ID Tokens</strong></em> (Click to expand)</summary><br> | ||
#### ID Token Verifying | ||
@@ -255,2 +260,55 @@ | ||
</details> | ||
<details> | ||
<summary><em><strong>Verifying OAuth 2.0 JWT Access Tokens</strong></em> (Click to expand)</summary><br> | ||
#### JWT Access Token Verifying | ||
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` 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. | ||
```js | ||
jose.JWT.verify( | ||
'eyJhbGciOiJQUzI1NiIsInR5cCI6ImF0K0pXVCIsImtpZCI6InIxTGtiQm8zOTI1UmIyWkZGckt5VTNNVmV4OVQyODE3S3gwdmJpNmlfS2MifQ.eyJzdWIiOiJmb28iLCJjbGllbnRfaWQiOiJ1cm46ZXhhbXBsZTpjbGllbnRfaWQiLCJhdWQiOiJ1cm46ZXhhbXBsZTpyZXNvdXJjZS1zZXJ2ZXIiLCJleHAiOjE1NjM4ODg4MzAsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20iLCJzY29wZSI6ImFwaTpyZWFkIn0.UYy8vEGWS0cS24giCYobMMy9-bqI45p807yV1l-2WXX2J4UO-eohV_R58LE2oM88gl414c6XydO6QSYXul5roNPoOs41jpEvreQIP-HmegjbWGutktWJKfvoOblE5FjYwjrwStjLQGUzkq6KWcnDLPGmpFy7n6gZ4LF8YVz4dLEaO335hMNVNrmSPSXYqr7bAWybnLVpLxjDYwNfCO1g0_TlFx8fHh2OftHoOOmJFltFwb8JypkSB-JXVVSEh43IOEjeeMJIG_ylWIOxfLLi5Q7vPWgub83ZTkuGNe4KmlQJKIsH5k0yZSshsLYUOOH0RiXqQ-SA4Ubh3Fowigdu-g', | ||
keystore, | ||
{ | ||
profile: 'at+JWT', | ||
issuer: 'https://op.example.com', | ||
audience: 'urn:example:resource-server', | ||
algorithms: ['PS256'] | ||
} | ||
) | ||
``` | ||
</details> | ||
<details> | ||
<summary><em><strong>Verifying OIDC Logout Token</strong></em> (Click to expand)</summary><br> | ||
#### Logout Token Verifying | ||
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` 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. | ||
```js | ||
jose.JWT.verify( | ||
'eyJhbGciOiJQUzI1NiJ9.eyJzdWIiOiJmb28iLCJhdWQiOiJ1cm46ZXhhbXBsZTpjbGllbnRfaWQiLCJpYXQiOjE1NjM4ODg4MzAsImp0aSI6ImhqazMyN2RzYSIsImlzcyI6Imh0dHBzOi8vb3AuZXhhbXBsZS5jb20iLCJldmVudHMiOnsiaHR0cDovL3NjaGVtYXMub3BlbmlkLm5ldC9ldmVudC9iYWNrY2hhbm5lbC1sb2dvdXQiOnt9fX0.SBi7uNUvjHL9TFoFzautGgTQ1MjyeGUNYHL7inpgq3XgTv6xc9EAKuPRtpixmhdNhmInGwUvAeqDSJxomwv1KK1cTndrC9zAMZ7h657BGQAwGhu7nTm41fWMpKQdiLa9sqp3yit5_FNBmqUNeOoMPrYT_Vl9ytsoNO89MUQy2aqCd-Z7BrNJZH0QycdW6dmYlrmZL7w3t3TaAXoJDJ4Hgl2Itkkkb6_6gO-VoPIdVD8sDuf1zQzGhIkmcFrk0fXczVYOkeF2hNYBuvsM8LuO-EPA3oyE2In9djai3M7yceTQetRa1vwlqWkg_xmYS59ry-6wT44aN7-Y6p0TdXm-Zg', | ||
keystore, | ||
{ | ||
profile: 'logout_token', | ||
issuer: 'https://op.example.com', | ||
audience: 'urn:example:client_id', | ||
algorithms: ['PS256'] | ||
} | ||
) | ||
``` | ||
</details> | ||
#### JWS Signing | ||
@@ -257,0 +315,0 @@ |
@@ -26,3 +26,3 @@ /// <reference types="node" /> | ||
export type keyObjectTypes = asymmetricKeyObjectTypes | 'secret'; | ||
export type JWTProfiles = 'id_token'; | ||
export type JWTProfiles = 'id_token' | 'at+JWT' | 'logout_token'; | ||
export type KeyInput = PrivateKeyInput | PublicKeyInput | string | Buffer; | ||
@@ -29,0 +29,0 @@ export type ProduceKeyInput = JWK.Key | KeyObject | KeyInput | JWKOctKey | JWKRSAKey | JWKECKey | JWKOKPKey; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
220308
4744
449