oidc-provider
Advanced tools
Comparing version 0.9.0 to 0.10.0
Following semver, 1.0.0 will mark the first API stable release and commence of this file, | ||
until then please use the compare views of github for reference. | ||
- https://github.com/panva/node-oidc-provider/compare/v0.9.0...v0.10.0 | ||
- added: custom discovery property config | ||
- added: returning distributed and aggregated claims | ||
- added: Back-Channel Logout draft implementation | ||
- added: registration.success event | ||
- added: allow clients for introspections/revocations only (Resource Servers) with no authorization flow access | ||
- added: draft / experimental features now warn upon provider init | ||
- fix: introspection follows normal/pairwise subject claim of the token's client | ||
- fix: added client_id_issued_at client property upon registration | ||
- https://github.com/panva/node-oidc-provider/compare/v0.8.1...v0.9.0 | ||
@@ -5,0 +14,0 @@ - added: (no)cache headers according to specs |
'use strict'; | ||
const _ = require('lodash'); | ||
module.exports = function discoveryAction(provider) { | ||
@@ -63,6 +65,12 @@ const config = provider.configuration(); | ||
this.body.end_session_endpoint = this.oidc.urlFor('end_session'); | ||
if (config.features.backchannelLogout) { | ||
this.body.backchannel_logout_supported = true; | ||
} | ||
} | ||
_.defaults(this.body, config.discovery); | ||
yield next; | ||
}; | ||
}; |
@@ -79,2 +79,24 @@ 'use strict'; | ||
if (provider.configuration('features.backchannelLogout')) { | ||
try { | ||
const Client = provider.get('Client'); | ||
const cookieValue = this.cookies.get('_session_states', { | ||
signed: provider.configuration('cookies.long.signed'), | ||
}); | ||
const clientIds = Object.keys(JSON.parse(cookieValue)); | ||
const logouts = clientIds.map(visitedClientId => Client.find(visitedClientId) | ||
.then(visitedClient => { | ||
if (visitedClient && visitedClient.backchannelLogoutUri) { | ||
return visitedClient.backchannelLogout(); | ||
} | ||
return undefined; | ||
}) | ||
); | ||
yield logouts; | ||
} catch (err) {} | ||
} | ||
yield this.oidc.session.destroy(); | ||
@@ -81,0 +103,0 @@ this.cookies.set('_session_states', null); |
@@ -75,2 +75,9 @@ 'use strict'; | ||
if (token.clientId !== this.oidc.client.clientId) { | ||
this.body.sub = Claims.sub(token.accountId, | ||
(yield provider.get('Client').find(token.clientId)).sectorIdentifier); | ||
} else { | ||
this.body.sub = Claims.sub(token.accountId, this.oidc.client.sectorIdentifier); | ||
} | ||
Object.assign(this.body, { | ||
@@ -84,3 +91,2 @@ active: token.isValid, | ||
scope: token.scope, | ||
sub: Claims.sub(token.accountId, this.oidc.client.sectorIdentifier), | ||
}); | ||
@@ -87,0 +93,0 @@ |
@@ -26,7 +26,28 @@ 'use strict'; | ||
client_id: uuid.v4(), | ||
client_secret: crypto.randomBytes(48).toString('base64'), | ||
client_secret_expires_at: 0, | ||
client_id_issued_at: Date.now() / 1000 | 0, | ||
registration_access_token: crypto.randomBytes(48).toString('base64'), | ||
}); | ||
let clientSecretRequired = properties.token_endpoint_auth_method === undefined || | ||
['private_key_jwt', 'none'].indexOf(properties.token_endpoint_auth_method) === -1; | ||
clientSecretRequired = clientSecretRequired || [ | ||
'id_token_signed_response_alg', | ||
'request_object_signing_alg', | ||
'token_endpoint_auth_signing_alg', | ||
'userinfo_signed_response_alg', | ||
].some(prop => { | ||
if (properties[prop] !== undefined && String(properties[prop]).startsWith('HS')) { | ||
return true; | ||
} | ||
return false; | ||
}); | ||
if (clientSecretRequired) { | ||
Object.assign(properties, { | ||
client_secret: crypto.randomBytes(48).toString('base64'), | ||
client_secret_expires_at: 0, | ||
}); | ||
} | ||
const client = yield provider.addClient(properties); | ||
@@ -44,2 +65,4 @@ const dumpable = _.mapKeys(client, (value, key) => _.snakeCase(key)); | ||
this.status = 201; | ||
provider.emit('registration.success', client, this); | ||
}, | ||
@@ -46,0 +69,0 @@ ]), |
@@ -46,2 +46,3 @@ 'use strict'; | ||
result() { | ||
const available = this.available; | ||
const include = _.chain(this.filter) | ||
@@ -75,4 +76,16 @@ .pickBy((value) => { | ||
const claims = _.chain(this.available).pick(include).value(); | ||
/* eslint-disable no-underscore-dangle */ | ||
const claims = _.pick(available, include); | ||
if (available._claim_names && available._claim_sources) { | ||
claims._claim_names = _.pick(available._claim_names, include); | ||
claims._claim_sources = _.pick(available._claim_sources, _.values(claims._claim_names)); | ||
if (_.isEmpty(claims._claim_names)) { | ||
delete claims._claim_names; | ||
delete claims._claim_sources; | ||
} | ||
} | ||
/* eslint-enable no-underscore-dangle */ | ||
if (this.sector && claims.sub) { | ||
@@ -79,0 +92,0 @@ claims.sub = this.constructor.sub(claims.sub, this.sector); |
@@ -10,3 +10,5 @@ 'use strict'; | ||
'application_type', | ||
'backchannel_logout_uri', | ||
'client_id', | ||
'client_id_issued_at', | ||
'client_name', | ||
@@ -56,3 +58,3 @@ 'client_secret', | ||
'client_id', | ||
'client_secret', | ||
// 'client_secret', => validated elsewhere and only needed somewhen | ||
'redirect_uris', | ||
@@ -77,2 +79,3 @@ ]; | ||
'application_type', | ||
'backchannel_logout_uri', | ||
'client_id', | ||
@@ -124,2 +127,3 @@ 'client_name', | ||
'sector_identifier_uri', | ||
'backchannel_logout_uri', | ||
@@ -205,11 +209,16 @@ // in arrays | ||
if (_.includes(rts, 'code')) { | ||
if (this.grant_types.indexOf('authorization_code') === -1) { | ||
if (this.token_endpoint_auth_method === 'none') { | ||
if (_.includes(this.grant_types, 'authorization_code')) { | ||
throw new errors.InvalidClientMetadata( | ||
'grant_types must contain authorization_code when code is amongst response_types'); | ||
'grant_types must not use token endpoint when token_endpoint_auth_method is none'); | ||
} | ||
} | ||
if (_.includes(rts, 'code') && !_.includes(this.grant_types, 'authorization_code')) { | ||
throw new errors.InvalidClientMetadata( | ||
'grant_types must contain authorization_code when code is amongst response_types'); | ||
} | ||
if (_.includes(rts, 'token') || _.includes(rts, 'id_token')) { | ||
if (this.grant_types.indexOf('implicit') === -1) { | ||
if (!_.includes(this.grant_types, 'implicit')) { | ||
throw new errors.InvalidClientMetadata( | ||
@@ -231,2 +240,9 @@ 'grant_types must contain implicit when id_token or token are amongst response_types'); | ||
const validateSecretPresence = validateSecretLength || | ||
['private_key_jwt', 'none'].indexOf(this.token_endpoint_auth_method) === -1; | ||
if (validateSecretPresence && !this.client_secret) { | ||
throw new errors.InvalidClientMetadata('client_secret is mandatory property'); | ||
} | ||
if (validateSecretLength) { | ||
@@ -297,3 +313,4 @@ if (this.client_secret.length < validateSecretLength) { | ||
throw new errors.InvalidClientMetadata( | ||
isAry ? `${prop} must only contain strings` : `${prop} must be a string`); | ||
isAry ? `${prop} must only contain strings` : | ||
`${prop} must be a non-empty string if provided`); | ||
} | ||
@@ -331,2 +348,5 @@ }); | ||
lengths() { | ||
if (LENGTH.every(prop => this[prop] && this[prop].length === 0)) { | ||
return true; | ||
} | ||
LENGTH.forEach((prop) => { | ||
@@ -333,0 +353,0 @@ if (this[prop] !== undefined && !this[prop].length) { |
@@ -6,2 +6,15 @@ 'use strict'; | ||
const STABLE_FLAGS = [ | ||
'claimsParameter', | ||
'clientCredentials', | ||
'discovery', | ||
'encryption', | ||
'introspection', | ||
'refreshToken', | ||
'registration', | ||
'request', | ||
'requestUri', | ||
'revocation', | ||
]; | ||
class Configuration { | ||
@@ -24,2 +37,17 @@ constructor(config) { | ||
if (this.features.backchannelLogout && !this.features.sessionManagement) { | ||
throw new Error('backchannelLogout is only available in conjuction with sessionManagement'); | ||
} | ||
/* eslint-disable no-restricted-syntax, no-console */ | ||
if (process.env.NODE_ENV !== 'test') { | ||
for (const flag in this.features) { | ||
if (this.features[flag] && STABLE_FLAGS.indexOf(flag) === -1) { | ||
console.warn(`WARNING: a draft/experimental feature (${flag}) enabled, future updates to \ | ||
this feature will be released as MINOR releases`); | ||
} | ||
} | ||
} | ||
/* eslint-enable */ | ||
if (!this.adapter) this.adapter = MemoryAdapter; | ||
@@ -26,0 +54,0 @@ if (!this.findById) { |
@@ -25,3 +25,13 @@ 'use strict'; | ||
}, | ||
discovery: { | ||
claim_types_supported: ['normal'], | ||
claims_locales_supported: undefined, | ||
display_values_supported: undefined, | ||
op_policy_uri: undefined, | ||
op_tos_uri: undefined, | ||
service_documentation: undefined, | ||
ui_locales_supported: undefined, | ||
}, | ||
features: { | ||
backchannelLogout: false, | ||
claimsParameter: false, | ||
@@ -43,2 +53,3 @@ clientCredentials: false, | ||
jwks_uri: 1500, | ||
backchannel_logout_uri: 1500, | ||
}, | ||
@@ -45,0 +56,0 @@ interactionUrl: ctx => `/interaction/${ctx.oidc.uuid}`, |
@@ -10,2 +10,3 @@ /* eslint-disable newline-per-chained-call */ | ||
const got = require('got'); | ||
const uuid = require('uuid').v4; | ||
@@ -15,2 +16,3 @@ const errors = require('../helpers/errors'); | ||
const NOOP = () => {}; | ||
const KEY_ATTRIBUTES = ['crv', 'e', 'kid', 'kty', 'n', 'use', 'x', 'y']; | ||
@@ -26,2 +28,3 @@ const KEY_TYPES = ['RSA', 'EC']; | ||
const cache = new Map(); | ||
const IdToken = provider.get('IdToken'); | ||
@@ -159,7 +162,9 @@ function schemaValidate(client, metadata) { | ||
return Promise.all(promises).then(() => { | ||
client.keystore.add({ | ||
k: base64url(new Buffer(client.clientSecret)), | ||
kid: 'clientSecret', | ||
kty: 'oct', | ||
}); | ||
if (client.clientSecret !== undefined) { | ||
client.keystore.add({ | ||
k: base64url(new Buffer(client.clientSecret)), | ||
kid: 'clientSecret', | ||
kty: 'oct', | ||
}); | ||
} | ||
}) | ||
@@ -184,2 +189,18 @@ .then(() => client); | ||
backchannelLogout(sub) { | ||
const logoutToken = new IdToken({ sub }, this.sectorIdentifier); | ||
logoutToken.mask = { sub: null }; | ||
logoutToken.set('logout_only', true); | ||
logoutToken.set('jti', uuid()); | ||
return logoutToken.sign(this, { expiresIn: 120 }) | ||
.then(token => got.post(this.backchannelLogoutUri, { | ||
headers: { 'User-Agent': provider.userAgent() }, | ||
timeout: provider.configuration('timeouts.backchannel_logout_uri'), | ||
retries: 0, | ||
followRedirect: false, | ||
body: { logout_token: token }, | ||
}).then(NOOP).catch(NOOP)); | ||
} | ||
responseTypeAllowed(type) { | ||
@@ -186,0 +207,0 @@ return this.responseTypes.indexOf(type) !== -1; |
@@ -43,2 +43,3 @@ 'use strict'; | ||
opts.expiresAt = 'expiresAt' in opts ? opts.expiresAt : null; | ||
opts.expiresIn = 'expiresIn' in opts ? opts.expiresIn : null; | ||
@@ -49,2 +50,4 @@ let expiresIn; | ||
expiresIn = opts.expiresAt - (Date.now() / 1000 | 0); | ||
} else if (opts.expiresIn) { | ||
expiresIn = opts.expiresIn; | ||
} | ||
@@ -51,0 +54,0 @@ |
@@ -60,3 +60,3 @@ { | ||
}, | ||
"version": "0.9.0", | ||
"version": "0.10.0", | ||
"files": [ | ||
@@ -63,0 +63,0 @@ "lib" |
@@ -28,2 +28,3 @@ # oidc-provider | ||
* [Custom Grant Types](#custom-grant-types) | ||
* [Custom Discovery Properties](#custom-discovery-properties) | ||
* [Events](#events) | ||
@@ -50,4 +51,5 @@ * [Certification](#certification) | ||
- Claims | ||
- Standard-defined Claims | ||
- Custom Claims | ||
- Normal Claims | ||
- Aggregated Claims | ||
- Distributed Claims | ||
- UserInfo Endpoint including | ||
@@ -71,3 +73,2 @@ - Signing (Asymmetric and Symmetric Signatures) | ||
- [OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 1][feature-registration] | ||
- [OpenID Connect Session Management 1.0 - draft26][feature-session-management] | ||
- [OAuth 2.0 Form Post Response mode][feature-form-post] | ||
@@ -77,2 +78,8 @@ - [RFC7009 - OAuth 2.0 Token revocation][feature-revocation] | ||
The following drafts/experimental specifications are implemented by oidc-provider. | ||
- [OpenID Connect Session Management 1.0 - draft 26][feature-session-management] | ||
- [OpenID Connect Back-Channel Logout 1.0 - draft 02][feature-backchannel-logout] | ||
Updates to drafts and experimental specifications are released as MINOR versions. | ||
## Get started | ||
@@ -183,8 +190,13 @@ To run and experiment with an example server, clone the oidc-provider repo and install the dependencies: | ||
Enables the use of Introspection endpoint as described in [RFC7662][feature-introspection] for | ||
tokens of type AccessToken, ClientCredentials and RefreshToken. When enabled | ||
token_introspection_endpoint property of the discovery endpoint is `true`, otherwise the property | ||
tokens of type AccessToken, ClientCredentials and RefreshToken. When enabled the | ||
token_introspection_endpoint property of the discovery endpoint is published, otherwise the property | ||
is not sent. The use of this endpoint is covered by the same authz mechanism as the regular token | ||
endpoint. | ||
This feature is a recommended way for Resource Servers to validate presented Bearer tokens, since | ||
the token endpoint access must be authorized it is recommended to setup a client for the RS to | ||
use. This client should be unusable for standard authorization flow, to set up such a client provide | ||
grant_types, response_types and redirect_uris as empty arrays. | ||
**Revocation endpoint** | ||
@@ -195,4 +207,4 @@ ```js | ||
Enables the use of Revocation endpoint as described in [RFC7009][feature-revocation] for tokens of | ||
type AccessToken, ClientCredentials and RefreshToken. When enabled | ||
token_revocation_endpoint property of the discovery endpoint is `true`, otherwise the property | ||
type AccessToken, ClientCredentials and RefreshToken. When enabled the | ||
token_revocation_endpoint property of the discovery endpoint is published, otherwise the property | ||
is not sent. The use of this endpoint is covered by the same authz mechanism as the regular token | ||
@@ -206,5 +218,12 @@ endpoint. | ||
``` | ||
Enables features described in [Session Management 1.0 - draft26][feature-session-management]. | ||
Enables features described in [Session Management 1.0 - draft 26][feature-session-management]. | ||
**Back-Channel Logout features** | ||
```js | ||
const configuration = { features: { sessionManagement: true, backchannelLogout: Boolean[false] } }; | ||
``` | ||
Enables features described in [Back-Channel Logout 1.0 - draft 02][feature-backchannel-logout]. | ||
**Dynamic registration features** | ||
@@ -281,2 +300,20 @@ ```js | ||
**Aggregated and Distributed claims** | ||
Returning aggregated and distributed claims is as easy as having your Account#claims method return | ||
the two necessary members `_claim_sources` and `_claim_names` with the | ||
[expected][feature-aggregated-distributed-claims] properties. oidc-provider will include only the | ||
sources for claims that are part of the request scope, omitting the ones that the RP did not request | ||
and leaving out the entire `_claim_sources` and `_claim_sources` if they bear no requested claims. | ||
Note: to make sure the RPs can expect these claims you should configure your discovery to return | ||
the respective claim types via the `claim_types_supported` property. | ||
```js | ||
const oidc = new Provider('http://localhost:3000', { | ||
discovery: { | ||
claim_types_supported: ['normal', 'aggregated', 'distributed'] | ||
} | ||
}); | ||
``` | ||
### Interaction | ||
@@ -369,2 +406,13 @@ Since oidc-provider comes with no views and interaction handlers what so ever it's up to you to fill | ||
### Custom Discovery Properties | ||
You can extend the returned discovery properties beyond the defaults | ||
```js | ||
const oidc = new Provider('http://localhost:3000', { | ||
discovery: { | ||
service_documentation: 'http://server.example.com/connect/service_documentation.html', | ||
ui_locales_supported: ['en-US', 'en-GB', 'en-CA', 'fr-FR', 'fr-CA'] | ||
} | ||
}); | ||
``` | ||
## Events | ||
@@ -414,2 +462,6 @@ The oidc-provider instance is an event emitter, `this` is always the instance. In events where `ctx`(koa | ||
**registration.success** | ||
oidc.on(`'registration.success', function (client, ctx) { }`) | ||
Emitted with every successful client registration request. | ||
**registration.error** | ||
@@ -478,3 +530,3 @@ oidc.on(`'registration.error', function (error, ctx) { }`) | ||
[feature-registration]: http://openid.net/specs/openid-connect-registration-1_0.html | ||
[feature-session-management]: http://openid.net/specs/openid-connect-session-1_0.html | ||
[feature-session-management]: http://openid.net/specs/openid-connect-session-1_0-26.html | ||
[feature-form-post]: http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html | ||
@@ -492,1 +544,3 @@ [feature-revocation]: https://tools.ietf.org/html/rfc7009 | ||
[password-grant]: https://tools.ietf.org/html/rfc6749#section-4.3 | ||
[feature-aggregated-distributed-claims]: http://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims | ||
[feature-backchannel-logout]: http://openid.net/specs/openid-connect-backchannel-1_0-02.html |
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
160435
3725
535
4