oidc-provider
Advanced tools
Comparing version 1.3.0 to 1.4.0
@@ -8,2 +8,4 @@ # oidc-provider CHANGELOG | ||
<!-- TOC START min:2 max:2 link:true update:true --> | ||
- [Version 1.4.0](#version-140) | ||
- [Version 1.3.0](#version-130) | ||
- [Version 1.2.0](#version-120) | ||
@@ -20,2 +22,15 @@ - [Version 1.1.0](#version-110) | ||
## Version 1.4.0 | ||
- [DIFF](https://github.com/panva/node-oidc-provider/compare/v1.3.0...v1.4.0) | ||
- added optional support for [OAuth 2.0 for Native Apps BCP - draft 06][feature-oauth-native-apps] | ||
- enable with configuration `features.oauthNativeApps = true`; | ||
- offline_access scope is now ignored when consent prompt is missing instead of being rejected as invalid_request | ||
- unrecognized authentication requests scopes are now ignored instead of being rejected as invalid_request | ||
- renamed the refreshToken feature flag to a more appropriate alwaysIssueRefresh | ||
## Version 1.3.0 | ||
- [DIFF](https://github.com/panva/node-oidc-provider/compare/v1.2.0...v1.3.0) | ||
- added optional Registration Access Token rotation strategy for Dynamic Client Registration Management Protocol | ||
- added request ctx bind to findById | ||
## Version 1.2.0 | ||
@@ -171,1 +186,3 @@ - [DIFF](https://github.com/panva/node-oidc-provider/compare/v1.1.0...v1.2.0) | ||
prohibiting any tampering with the payload and header content. | ||
[feature-oauth-native-apps]: https://tools.ietf.org/html/draft-ietf-oauth-native-apps-06 |
@@ -13,10 +13,6 @@ 'use strict'; | ||
module.exports = provider => function* checkScope(next) { | ||
const scopes = this.oidc.params.scope.split(' '); | ||
const scopes = _.intersection(this.oidc.params.scope.split(' '), instance(provider).configuration('scopes')); | ||
const responseType = this.oidc.params.response_type; | ||
const prompts = this.oidc.prompts; | ||
const unsupported = _.difference(scopes, instance(provider).configuration('scopes')); | ||
this.assert(_.isEmpty(unsupported), new errors.InvalidRequestError( | ||
`invalid scope value(s) provided. (${unsupported.join(',')})`)); | ||
this.assert(scopes.indexOf('openid') !== -1, | ||
@@ -34,12 +30,11 @@ new errors.InvalidRequestError('openid is required scope')); | ||
if (scopes.indexOf('offline_access') !== -1) { | ||
if (responseType.includes('code')) { | ||
this.assert(prompts.indexOf('consent') !== -1, | ||
new errors.InvalidRequestError('offline_access scope requires consent prompt')); | ||
} else { | ||
this.oidc.params.scope = _.pull(scopes, 'offline_access').join(' '); | ||
if (!responseType.includes('code') || prompts.indexOf('consent') === -1) { | ||
_.pull(scopes, 'offline_access').join(' '); | ||
} | ||
} | ||
this.oidc.params.scope = scopes.join(' '); | ||
yield next; | ||
}; |
@@ -75,3 +75,3 @@ 'use strict'; | ||
const grantPresent = this.oidc.client.grantTypes.indexOf('refresh_token') !== -1; | ||
const shouldIssue = instance(provider).configuration('features.refreshToken') || | ||
const shouldIssue = instance(provider).configuration('features.alwaysIssueRefresh') || | ||
code.scope.split(' ').indexOf('offline_access') !== -1; | ||
@@ -78,0 +78,0 @@ |
@@ -10,2 +10,6 @@ 'use strict'; | ||
function invalidate(message) { | ||
throw new errors.InvalidClientMetadata(message); | ||
} | ||
const RECOGNIZED_METADATA = [ | ||
@@ -147,2 +151,4 @@ 'application_type', | ||
const LOOPBACKS = ['localhost', '127.0.0.1', '::1']; | ||
module.exports = function getSchema(provider) { | ||
@@ -204,2 +210,3 @@ const ENUM = { | ||
this.redirectUris(); | ||
this.normalizeNativeAppUris(); | ||
@@ -209,3 +216,3 @@ // MAX AGE FORMAT | ||
if (!Number.isInteger(this.default_max_age) || this.default_max_age <= 0) { | ||
throw new errors.InvalidClientMetadata('default_max_age must be a positive integer'); | ||
invalidate('default_max_age must be a positive integer'); | ||
} | ||
@@ -222,4 +229,3 @@ } | ||
if (_.includes(this.grant_types, 'authorization_code')) { | ||
throw new errors.InvalidClientMetadata( | ||
'grant_types must not use token endpoint when token_endpoint_auth_method is none'); | ||
invalidate('grant_types must not use token endpoint when token_endpoint_auth_method is none'); | ||
} | ||
@@ -229,4 +235,3 @@ } | ||
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'); | ||
invalidate('grant_types must contain authorization_code when code is amongst response_types'); | ||
} | ||
@@ -236,4 +241,3 @@ | ||
if (!_.includes(this.grant_types, 'implicit')) { | ||
throw new errors.InvalidClientMetadata( | ||
'grant_types must contain implicit when id_token or token are amongst response_types'); | ||
invalidate('grant_types must contain implicit when id_token or token are amongst response_types'); | ||
} | ||
@@ -257,3 +261,3 @@ } | ||
if (validateSecretPresence && !this.client_secret) { | ||
throw new errors.InvalidClientMetadata('client_secret is mandatory property'); | ||
invalidate('client_secret is mandatory property'); | ||
} | ||
@@ -263,3 +267,3 @@ | ||
if (this.client_secret.length < validateSecretLength) { | ||
throw new errors.InvalidClientMetadata('insufficient client_secret length'); | ||
invalidate('insufficient client_secret length'); | ||
} | ||
@@ -278,4 +282,3 @@ } | ||
} else { | ||
throw new errors.InvalidClientMetadata( | ||
'sector_identifier_uri is required when using multiple hosts in your redirect_uris'); | ||
invalidate('sector_identifier_uri is required when using multiple hosts in your redirect_uris'); | ||
} | ||
@@ -287,4 +290,3 @@ } else if (this.sector_identifier_uri) { | ||
if (this.jwks !== undefined && this.jwks_uri !== undefined) { | ||
throw new errors.InvalidClientMetadata( | ||
'jwks and jwks_uri must not be used at the same time'); | ||
invalidate('jwks and jwks_uri must not be used at the same time'); | ||
} | ||
@@ -294,6 +296,6 @@ | ||
if (!Array.isArray(this.jwks.keys)) { | ||
throw new errors.InvalidClientMetadata('jwks must be a JWK Set'); | ||
invalidate('jwks must be a JWK Set'); | ||
} | ||
if (!this.jwks.keys.length) { | ||
throw new errors.InvalidClientMetadata('jwks.keys must not be empty'); | ||
invalidate('jwks.keys must not be empty'); | ||
} | ||
@@ -306,3 +308,3 @@ } | ||
if (!this[prop]) { | ||
throw new errors.InvalidClientMetadata(`${prop} is mandatory property`); | ||
invalidate(`${prop} is mandatory property`); | ||
} | ||
@@ -317,3 +319,3 @@ }); | ||
if (requireJwks && !this.jwks && !this.jwks_uri) { | ||
throw new errors.InvalidClientMetadata('jwks or jwks_uri is mandatory for this client'); | ||
invalidate('jwks or jwks_uri is mandatory for this client'); | ||
} | ||
@@ -328,5 +330,5 @@ } | ||
if (typeof val !== 'string' || !val.length) { | ||
throw new errors.InvalidClientMetadata( | ||
isAry ? `${prop} must only contain strings` : | ||
`${prop} must be a non-empty string if provided`); | ||
invalidate(isAry ? | ||
`${prop} must only contain strings` : | ||
`${prop} must be a non-empty string if provided`); | ||
} | ||
@@ -346,4 +348,5 @@ }); | ||
if (!validUrl[method](val)) { | ||
throw new errors.InvalidClientMetadata( | ||
isAry ? `${prop} must only contain ${type} uris` : `${prop} must be a ${type} uri`); | ||
invalidate(isAry ? | ||
`${prop} must only contain ${type} uris` : | ||
`${prop} must be a ${type} uri`); | ||
} | ||
@@ -359,3 +362,3 @@ }); | ||
if (!Array.isArray(this[prop])) { | ||
throw new errors.InvalidClientMetadata(`${prop} must be an array`); | ||
invalidate(`${prop} must be an array`); | ||
} | ||
@@ -373,3 +376,3 @@ this[prop] = _.uniq(this[prop]); | ||
if (this[prop] !== undefined && !this[prop].length) { | ||
throw new errors.InvalidClientMetadata(`${prop} must contain members`); | ||
invalidate(`${prop} must contain members`); | ||
} | ||
@@ -383,3 +386,3 @@ }); | ||
if (typeof this[prop] !== 'boolean') { | ||
throw new errors.InvalidClientMetadata(`${prop} must be a boolean`); | ||
invalidate(`${prop} must be a boolean`); | ||
} | ||
@@ -393,3 +396,3 @@ } | ||
if (this[when] !== undefined && this[then[0]] === undefined) { | ||
throw new errors.InvalidClientMetadata(`${then[0]} is mandatory property`); | ||
invalidate(`${then[0]} is mandatory property`); | ||
} else if (this[when] === undefined && this[then[0]] !== undefined) { | ||
@@ -415,5 +418,5 @@ this[when] = then[1]; | ||
})) { | ||
throw new errors.InvalidClientMetadata(`${prop} can only contain members [${only}]`); | ||
invalidate(`${prop} can only contain members [${only}]`); | ||
} else if (!isAry && only.indexOf(this[prop]) === -1) { | ||
throw new errors.InvalidClientMetadata(`${prop} must be one of [${only}]`); | ||
invalidate(`${prop} must be one of [${only}]`); | ||
} | ||
@@ -424,6 +427,22 @@ } | ||
normalizeNativeAppUris() { | ||
if (this.application_type === 'web') return; | ||
if (!instance(provider).configuration('features.oauthNativeApps')) return; | ||
this.redirect_uris = _.map(this.redirect_uris, (redirectUri) => { | ||
if (redirectUri.startsWith('http:')) { // this removes the port component, making dynamic ports allowed | ||
return url.format(Object.assign(url.parse(redirectUri), { | ||
host: null, | ||
port: null, | ||
})); | ||
} | ||
return redirectUri; | ||
}); | ||
} | ||
redirectUris() { | ||
this.redirect_uris.forEach((redirectUri) => { | ||
if (redirectUri.indexOf('#') !== -1) { | ||
throw new errors.InvalidClientMetadata('redirect_uris must not contain fragments'); | ||
invalidate('redirect_uris must not contain fragments'); | ||
} | ||
@@ -434,13 +453,11 @@ | ||
if (!validUrl.isWebUri(redirectUri)) { | ||
throw new errors.InvalidClientMetadata('redirect_uris must only contain valid web uris'); | ||
invalidate('redirect_uris must only contain valid web uris'); | ||
} | ||
if (this.grant_types.indexOf('implicit') !== -1 && redirectUri.startsWith('http:')) { | ||
throw new errors.InvalidClientMetadata( | ||
'redirect_uris for web clients using implicit flow MUST only register URLs using the https scheme'); | ||
invalidate('redirect_uris for web clients using implicit flow MUST only register URLs using the https scheme'); | ||
} | ||
if (url.parse(redirectUri).hostname === 'localhost') { | ||
throw new errors.InvalidClientMetadata( | ||
'redirect_uris for web clients must not be using localhost'); | ||
invalidate('redirect_uris for web clients must not be using localhost'); | ||
} | ||
@@ -450,12 +467,33 @@ break; | ||
if (!validUrl.isUri(redirectUri)) { | ||
throw new errors.InvalidClientMetadata('redirect_uris must only contain valid uris'); | ||
invalidate('redirect_uris must only contain valid uris'); | ||
} | ||
if (redirectUri.startsWith('https:')) { | ||
throw new errors.InvalidClientMetadata( | ||
'redirect_uris for native clients must not be using https URI scheme'); | ||
if (instance(provider).configuration('features.oauthNativeApps')) { | ||
const uri = url.parse(redirectUri); | ||
switch (uri.protocol) { | ||
case 'http:': // Loopback URI Redirection | ||
if (LOOPBACKS.indexOf(uri.hostname) === -1) { | ||
invalidate('redirect_uris for native clients using http as a protocol can only use loopback addresses as hostnames'); | ||
} | ||
break; | ||
case 'https:': // App-claimed HTTPS URI Redirection | ||
if (LOOPBACKS.indexOf(uri.hostname) !== -1) { | ||
invalidate(`redirect_uris for native clients using claimed HTTPS URIs must not be using ${uri.hostname} as hostname`); | ||
} | ||
break; | ||
default: // App-declared Custom URI Scheme Redirection | ||
if (uri.hostname !== 'localhost') { | ||
invalidate('redirect_uris for native clients using custom URI scheme must be using localhost as hostname'); | ||
} | ||
} | ||
} else { | ||
if (redirectUri.startsWith('https:')) { | ||
invalidate('redirect_uris for native clients must not be using https URI scheme'); | ||
} | ||
if (url.parse(redirectUri).hostname !== 'localhost') { | ||
invalidate('redirect_uris for native clients must be using localhost as hostname'); | ||
} | ||
} | ||
if (url.parse(redirectUri).hostname !== 'localhost') { | ||
throw new errors.InvalidClientMetadata( | ||
'redirect_uris for native clients must be using localhost as hostname'); | ||
} | ||
break; | ||
@@ -462,0 +500,0 @@ } |
'use strict'; | ||
const _ = require('lodash'); | ||
const util = require('util'); | ||
const defaults = require('./defaults'); | ||
@@ -59,2 +60,7 @@ | ||
const rtFlagRename = util.deprecate(/* istanbul ignore next */ function refreshTokenRename() { | ||
this.features.alwaysIssueRefresh = this.features.refreshToken; | ||
delete this.features.refreshToken; | ||
}, 'features.refreshToken: Use features.alwaysIssueRefresh instead'); | ||
module.exports = class ConfigurationSchema { | ||
@@ -91,3 +97,9 @@ constructor(config) { | ||
if (this.features.refreshToken || this.scopes.indexOf('offline_access') !== -1) { | ||
// 2.0 DEPRECATED | ||
/* istanbul ignore if */ | ||
if (Object.keys(this.features).indexOf('refreshToken') !== -1) { | ||
rtFlagRename.call(this); | ||
} | ||
if (this.features.alwaysIssueRefresh || this.scopes.indexOf('offline_access') !== -1) { | ||
this.grantTypes.add('refresh_token'); | ||
@@ -94,0 +106,0 @@ } |
@@ -11,3 +11,3 @@ 'use strict'; | ||
'introspection', | ||
'refreshToken', | ||
'alwaysIssueRefresh', | ||
'registration', | ||
@@ -14,0 +14,0 @@ 'request', |
@@ -97,3 +97,3 @@ 'use strict'; | ||
introspection: false, | ||
refreshToken: false, | ||
alwaysIssueRefresh: false, | ||
registration: false, | ||
@@ -104,2 +104,3 @@ registrationManagement: false, | ||
revocation: false, | ||
oauthNativeApps: false, | ||
sessionManagement: false, | ||
@@ -106,0 +107,0 @@ }, |
@@ -225,4 +225,19 @@ 'use strict'; | ||
redirectUriAllowed(uri) { | ||
return this.redirectUris.indexOf(uri) !== -1; | ||
redirectUriAllowed(redirectUri) { | ||
const checkedUri = (() => { | ||
if (this.applicationType === 'native' && | ||
redirectUri.startsWith('http:') && | ||
instance(provider).configuration('features.oauthNativeApps') | ||
) { | ||
return url.format(Object.assign(url.parse(redirectUri), { | ||
host: null, | ||
port: null, | ||
})); | ||
} | ||
return redirectUri; | ||
})(); | ||
return this.redirectUris.indexOf(checkedUri) !== -1; | ||
} | ||
@@ -229,0 +244,0 @@ |
@@ -66,3 +66,3 @@ { | ||
}, | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"files": [ | ||
@@ -69,0 +69,0 @@ "lib" |
@@ -27,3 +27,4 @@ # oidc-provider | ||
The following specifications are implemented by oidc-provider. | ||
The following specifications are implemented by oidc-provider. Note that not all features are | ||
enabled by default, check the configuration section on how to enable them. | ||
@@ -53,2 +54,3 @@ - [OpenID Connect Core 1.0 incorporating errata set 1][feature-core] | ||
- [RFC7592 - OAuth 2.0 Dynamic Client Registration Management Protocol (Update and Delete)][feature-registration-management] | ||
- [OAuth 2.0 for Native Apps BCP - draft 06][feature-oauth-native-apps] | ||
@@ -141,1 +143,2 @@ Updates to drafts and experimental specification versions are released as MINOR library versions. | ||
[feature-registration-management]: https://tools.ietf.org/html/rfc7592 | ||
[feature-oauth-native-apps]: https://tools.ietf.org/html/draft-ietf-oauth-native-apps-06 |
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
202414
4887
142