Skip to content

Commit

Permalink
feat: expose client schema invalidate(err, code) to enable customization
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Nov 14, 2019
1 parent 2ba11c8 commit d672ee8
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 41 deletions.
86 changes: 45 additions & 41 deletions lib/helpers/client_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ const W3CEmailRegExp = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a
const encAlgRequiringJwks = /^(RSA|ECDH)/;
const requestSignAlgRequiringJwks = /^((P|E|R)S\d{3}|EdDSA)$/;

function invalidate(message) {
throw new InvalidClientMetadata(message);
}

function checkClientAuth(schema) {
return !!clientAuthEndpoints.find((endpoint) => ['private_key_jwt', 'self_signed_tls_client_auth'].includes(schema[`${endpoint}_endpoint_auth_method`]));
}
Expand Down Expand Up @@ -259,7 +255,7 @@ module.exports = function getSchema(provider) {
['default_max_age', 'client_secret_expires_at'].forEach((prop) => {
if (this[prop] !== undefined) {
if (!Number.isSafeInteger(this[prop]) || this[prop] < 0) {
invalidate(`${prop} must be a non-negative integer`);
this.invalidate(`${prop} must be a non-negative integer`);
}
}
});
Expand All @@ -269,20 +265,20 @@ module.exports = function getSchema(provider) {
].reduce((acc, val) => ([...acc, ...val]), []);

if (this.grant_types.some((type) => ['authorization_code', 'implicit'].includes(type)) && !this.response_types.length) {
invalidate('response_types must contain members');
this.invalidate('response_types must contain members');
}

if (responseTypes.length && !this.redirect_uris.length) {
invalidate('redirect_uris must contain members');
this.invalidate('redirect_uris must contain members');
}

if (responseTypes.includes('code') && !this.grant_types.includes('authorization_code')) {
invalidate('grant_types must contain authorization_code when code is amongst response_types');
this.invalidate('grant_types must contain authorization_code when code is amongst response_types');
}

if (responseTypes.includes('token') || responseTypes.includes('id_token')) {
if (!this.grant_types.includes('implicit')) {
invalidate('grant_types must contain implicit when id_token or token are amongst response_types');
this.invalidate('grant_types must contain implicit when id_token or token are amongst response_types');
}
}
{
Expand All @@ -298,11 +294,11 @@ module.exports = function getSchema(provider) {
for (const endpoint of clientAuthEndpoints) { // eslint-disable-line no-restricted-syntax
if (this[`${endpoint}_endpoint_auth_method`] === 'tls_client_auth') {
if (length === 0) {
invalidate('tls_client_auth requires one of the certificate subject value parameters');
this.invalidate('tls_client_auth requires one of the certificate subject value parameters');
}

if (length !== 1) {
invalidate('only one tls_client_auth certificate subject value must be provided');
this.invalidate('only one tls_client_auth certificate subject value must be provided');
}

used = true;
Expand Down Expand Up @@ -332,7 +328,7 @@ module.exports = function getSchema(provider) {
'userinfo_encrypted_response_enc',
].forEach((attr) => {
if (['A192CBC-HS384', 'A256CBC-HS512'].includes(this[attr]) && this[attr.replace(/_enc$/, '_alg')] === 'ECDH-ES') {
invalidate(`${this[attr]} is not possible with ECDH-ES`);
this.invalidate(`${this[attr]} is not possible with ECDH-ES`);
}
});

Expand All @@ -344,26 +340,32 @@ module.exports = function getSchema(provider) {
});

if (this.jwks !== undefined && this.jwks_uri !== undefined) {
invalidate('jwks and jwks_uri must not be used at the same time');
this.invalidate('jwks and jwks_uri must not be used at the same time');
}

if (this.jwks !== undefined) {
if (!Array.isArray(this.jwks.keys)) {
invalidate('jwks must be a JWK Set');
this.invalidate('jwks must be a JWK Set');
}
}

this.processCustomMetadata(ctx);
this.ensureStripUnrecognized();
}

invalidate(message, code) { // eslint-disable-line class-methods-use-this, no-unused-vars
throw new InvalidClientMetadata(message);
}

required() {
let checked = REQUIRED;
if (provider.Client.needsSecret(this)) checked = checked.concat('client_secret');
if (provider.Client.needsSecret(this)) {
checked = checked.concat('client_secret');
}

checked.forEach((prop) => {
if (!this[prop]) {
invalidate(`${prop} is mandatory property`);
this.invalidate(`${prop} is mandatory property`);
}
});

Expand All @@ -375,7 +377,7 @@ module.exports = function getSchema(provider) {
|| (encAlgRequiringJwks.test(this.authorization_encrypted_response_alg));

if (requireJwks && !this.jwks && !this.jwks_uri) {
invalidate('jwks or jwks_uri is mandatory for this client');
this.invalidate('jwks or jwks_uri is mandatory for this client');
}
}

Expand All @@ -385,7 +387,7 @@ module.exports = function getSchema(provider) {
const isAry = ARYS.includes(prop);
(isAry ? this[prop] : [this[prop]]).forEach((val) => {
if (typeof val !== 'string' || !val.length) {
invalidate(isAry
this.invalidate(isAry
? `${prop} must only contain strings`
: `${prop} must be a non-empty string if provided`);
}
Expand All @@ -402,7 +404,7 @@ module.exports = function getSchema(provider) {
const method = HTTPS_URI.includes(prop) ? 'isHttpsUri' : 'isWebUri';
const type = method === 'isWebUri' ? 'web' : 'https';
if (!validUrl[method](val)) {
invalidate(isAry
this.invalidate(isAry
? `${prop} must only contain ${type} uris`
: `${prop} must be a ${type} uri`);
}
Expand All @@ -415,7 +417,7 @@ module.exports = function getSchema(provider) {
ARYS.forEach((prop) => {
if (this[prop] !== undefined) {
if (!Array.isArray(this[prop])) {
invalidate(`${prop} must be an array`);
this.invalidate(`${prop} must be an array`);
}
this[prop] = [...new Set(this[prop])];
}
Expand All @@ -426,7 +428,7 @@ module.exports = function getSchema(provider) {
BOOL.forEach((prop) => {
if (this[prop] !== undefined) {
if (typeof this[prop] !== 'boolean') {
invalidate(`${prop} must be a boolean`);
this.invalidate(`${prop} must be a boolean`);
}
}
});
Expand All @@ -435,7 +437,7 @@ module.exports = function getSchema(provider) {
whens() {
Object.entries(WHEN).forEach(([when, [property, value]]) => {
if (this[when] !== undefined && this[property] === undefined) {
invalidate(`${property} is mandatory property when ${when} is provided`);
this.invalidate(`${property} is mandatory property when ${when} is provided`);
} else if (this[when] === undefined && this[property] !== undefined) {
this[when] = value;
}
Expand All @@ -454,13 +456,13 @@ module.exports = function getSchema(provider) {
}
return !only.includes(val);
})) {
invalidate(`${prop} can only contain members [${[...only]}]`);
this.invalidate(`${prop} can only contain members [${[...only]}]`);
} else if (!isAry) {
if (
(Array.isArray(only) && !only.includes(this[prop]))
|| (only instanceof Set && !only.has(this[prop]))
) {
invalidate(`${prop} must be one of [${[...only]}]`);
this.invalidate(`${prop} must be one of [${[...only]}]`);
}
}
}
Expand Down Expand Up @@ -491,7 +493,7 @@ module.exports = function getSchema(provider) {
try {
new url.URL(uri); // eslint-disable-line no-new
} catch (err) {
invalidate('post_logout_redirect_uris must only contain uris');
this.invalidate('post_logout_redirect_uris must only contain uris');
}
});
}
Expand All @@ -505,13 +507,13 @@ module.exports = function getSchema(provider) {
try {
({ origin, protocol } = new url.URL(uri));
} catch (err) {
invalidate('web_message_uris must only contain valid uris');
this.invalidate('web_message_uris must only contain valid uris');
}
if (!['https:', 'http:'].includes(protocol)) {
invalidate('web_message_uris must only contain web uris');
this.invalidate('web_message_uris must only contain web uris');
}
if (origin !== uri) {
invalidate('web_message_uris must only contain origins');
this.invalidate('web_message_uris must only contain origins');
}
});
}
Expand All @@ -523,45 +525,47 @@ module.exports = function getSchema(provider) {
try {
({ hostname, protocol } = new url.URL(redirectUri));
} catch (err) {
invalidate('redirect_uris must only contain valid uris');
this.invalidate('redirect_uris must only contain valid uris');
}

const { hash } = url.parse(redirectUri);

if (hash) {
invalidate('redirect_uris must not contain fragments');
this.invalidate('redirect_uris must not contain fragments');
}

switch (this.application_type) { // eslint-disable-line default-case
case 'web': {
if (!['https:', 'http:'].includes(protocol)) {
invalidate('redirect_uris must only contain web uris');
this.invalidate('redirect_uris must only contain web uris');
}

if (this.grant_types.includes('implicit') && protocol === 'http:') {
invalidate('redirect_uris for web clients using implicit flow MUST only register URLs using the https scheme');
}
if (this.grant_types.includes('implicit')) {
if (protocol === 'http:') {
this.invalidate('redirect_uris for web clients using implicit flow MUST only register URLs using the https scheme', 'implicit-force-https');
}

if (this.grant_types.includes('implicit') && hostname === 'localhost') {
invalidate('redirect_uris for web clients using implicit flow must not be using localhost');
if (hostname === 'localhost') {
this.invalidate('redirect_uris for web clients using implicit flow must not be using localhost', 'implicit-forbid-localhost');
}
}
break;
}
case 'native': {
switch (protocol) {
case 'http:': // Loopback Interface Redirection
if (!LOOPBACKS.includes(hostname)) {
invalidate('redirect_uris for native clients using http as a protocol can only use loopback addresses as hostnames');
this.invalidate('redirect_uris for native clients using http as a protocol can only use loopback addresses as hostnames');
}
break;
case 'https:': // Claimed HTTPS URI Redirection
if (LOOPBACKS.includes(hostname)) {
invalidate(`redirect_uris for native clients using claimed HTTPS URIs must not be using ${hostname} as hostname`);
this.invalidate(`redirect_uris for native clients using claimed HTTPS URIs must not be using ${hostname} as hostname`);
}
break;
default: // Private-use URI Scheme Redirection
if (!protocol.includes('.')) {
invalidate('redirect_uris for native clients using Custom URI scheme should use reverse domain name based scheme');
this.invalidate('redirect_uris for native clients using Custom URI scheme should use reverse domain name based scheme');
}
}
break;
Expand All @@ -574,7 +578,7 @@ module.exports = function getSchema(provider) {
if (this.contacts) {
this.contacts.forEach((contact) => {
if (!W3CEmailRegExp.test(contact)) {
invalidate('contacts can only contain email addresses');
this.invalidate('contacts can only contain email addresses');
}
});
}
Expand All @@ -600,7 +604,7 @@ module.exports = function getSchema(provider) {
const parsed = new Set(this.scope.split(' '));
parsed.forEach((scope) => {
if (!scopes.has(scope) && !dynamicScopes.has(scope)) {
invalidate('scope must only contain supported scopes');
this.invalidate('scope must only contain supported scopes');
}
});
this.scope = [...parsed].join(' ');
Expand Down
1 change: 1 addition & 0 deletions recipes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ If you or your business use oidc-provider, please consider becoming a [sponsor][
- [Client-based CORS origins](client_based_origins.md)
- [Decentralized claims](decentralized_claims.md)
- [Redirect URI wildcards](redirect_uri_wildcards.md)
- [Allowing HTTP and/or localhost for implicit response types](implicit_http_localhost.md)
- ... got something worthy of being here? Submit a PR with a new recipe to help others.

[support-sponsor]: https://github.com/sponsors/panva
Expand Down
38 changes: 38 additions & 0 deletions recipes/implicit_http_localhost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Allowing HTTP and/or localhost for implicit response type web clients

- built for version: ^6.15.0

> ⚠️ This violates the OIDC Core 1.0 specification. **Its only practical use-case is for development purposes and as such is not recommended
> for any production deployment.**
```js
const { Provider } = require('oidc-provider');

const provider = new Provider('http://localhost:3000', {
clients: [
{
client_id: 'development-implicit',
application_type: 'web',
token_endpoint_auth_method: 'none',
response_types: ['id_token'],
grant_types: ['implicit'],
redirect_uris: ['http://localhost:3001'], // this fails two regular validations http: and localhost
},
],
});

const { invalidate: orig } = provider.Client.Schema.prototype;

provider.Client.Schema.prototype.invalidate = function invalidate(message, code) {
if (code === 'implicit-force-https' || code === 'implicit-forbid-localhost') {
return;
}

orig.call(this, message);
};
```

In addition to this you may also utilize
[extra client metadata](https://github.com/panva/node-oidc-provider/blob/master/docs/README.md#extraclientmetadata)
and only skip these checks for clients in something like a development mode or similar. Again, no
production client should be allowed to skip these validations.

0 comments on commit d672ee8

Please sign in to comment.