Skip to content

Commit

Permalink
feat: add self_signed_tls_client_auth client authentication method
Browse files Browse the repository at this point in the history
Defined in https://tools.ietf.org/html/draft-ietf-oauth-mtls-11 this
client authentication method uses mutual Transport Layer Security (TLS)
to authenticate a client for token, introspection and revocation
endpoints. It relies on your TLS-offloading proxy to parse, validate and
send metadata about the X.509 certificate via headers to the upstream
node.js application.

See the configuration doc tokenEndpointAuthMethods section for more
details.
  • Loading branch information
panva committed Sep 26, 2018
1 parent f43d820 commit 9a1f0a3
Show file tree
Hide file tree
Showing 15 changed files with 302 additions and 42 deletions.
15 changes: 9 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ false
<br>


To enable Certificate Bound Access Tokens the provider expects `x-ssl-client-cert` header to be presented by your TLS-offloading proxy with the variable value set by this proxy. An important aspect is to sanitize the inbound request header at the proxy. <br/><br/> The most common openssl based proxies are Apache and NGINX, with those you're looking to use <br/><br/> __`SSLVerifyClient` (Apache) / `ssl_verify_client` (NGINX)__ with the appropriate configuration value that matches your setup requirements. <br/><br/> Set the proxy request header with variable set as a result of enabling MTLS
To enable Certificate Bound Access Tokens the provider expects your TLS-offloading proxy to handle the client certificate validation, parsing, handling, etc. Once set up you are expected to forward `x-ssl-client-cert` header with variable values set by this proxy. An important aspect is to sanitize the inbound request headers at the proxy. <br/><br/> The most common openssl based proxies are Apache and NGINX, with those you're looking to use <br/><br/> __`SSLVerifyClient` (Apache) / `ssl_verify_client` (NGINX)__ with the appropriate configuration value that matches your setup requirements. <br/><br/> Set the proxy request header with variable set as a result of enabling MTLS


```nginx
Expand All @@ -793,7 +793,7 @@ proxy_set_header x-ssl-client-cert $ssl_client_cert;
RequestHeader set x-ssl-client-cert ""
RequestHeader set x-ssl-client-cert "%{SSL_CLIENT_CERT}s"
```
You should also consider hosting the token and userinfo endpoints on a separate host name or port in order to prevent unintended impact on the TLS behaviour of your other endpoints, e.g. Discovery or the authorization endpoint and changing the discovery values for these with a post-middleware since you need MTLS to issue tokens (token_endpoint) and userinfo will now act as a Resource Server supporting Certificate Bound Access Tokens and therefore needs to handle client certificates.
You should also consider hosting the endpoints supporting client authentication, on a separate host name or port in order to prevent unintended impact on the TLS behaviour of your other endpoints, e.g. Discovery or the authorization endpoint and changing the discovery values for them with a post-middleware. When doing that be sure to remove the client provided headers of the same name on the non-MTLS enabled host name / port in your proxy setup.


```js
Expand Down Expand Up @@ -1915,31 +1915,34 @@ _**default value**_:
'none',
'client_secret_basic', 'client_secret_post',
'client_secret_jwt', 'private_key_jwt',
'tls_client_auth',
'tls_client_auth', 'self_signed_tls_client_auth',
]
```
</details>
<details>
<summary>(Click to expand) Setting up the environment for tls_client_auth</summary>
<summary>(Click to expand) Setting up the environment for tls_client_auth and self_signed_tls_client_auth</summary>
<br>


To enable `tls_client_auth` the provider expects `x-ssl-client-verify` and `x-ssl-client-s-dn` headers to be presented by your TLS-offloading proxy with the variable values set by these proxies. An important aspect is to sanitize the inbound request headers at the proxy. <br/><br/> The most common openssl based proxies are Apache and NGINX, with those you're looking to use <br/><br/> __`SSLVerifyClient` (Apache) / `ssl_verify_client` (NGINX)__ with the appropriate configuration value that matches your setup requirements. <br/><br/> __`SSLCACertificateFile` or `SSLCACertificatePath` (Apache) / `ssl_client_certificate` (NGINX)__ with the values pointing to your accepted CA Certificates. <br/><br/> Set the proxy request headers with variables set as a result of enabling MTLS
To enable MTLS based authentication methods the provider expects your TLS-offloading proxy to handle the client certificate validation, parsing, handling, etc. Once set up you are expected to forward `x-ssl-client-verify`, `x-ssl-client-s-dn` and `x-ssl-client-cert` headers with variable values set by this proxy. An important aspect is to sanitize the inbound request headers at the proxy. <br/><br/> The most common openssl based proxies are Apache and NGINX, with those you're looking to use <br/><br/> __`SSLVerifyClient` (Apache) / `ssl_verify_client` (NGINX)__ with the appropriate configuration value that matches your setup requirements. <br/><br/> __`SSLCACertificateFile` or `SSLCACertificatePath` (Apache) / `ssl_client_certificate` (NGINX)__ with the values pointing to your accepted CA Certificates. <br/><br/> Set the proxy request headers with variables set as a result of enabling MTLS


```nginx
# NGINX
proxy_set_header x-ssl-client-cert $ssl_client_cert;
proxy_set_header x-ssl-client-verify $ssl_client_verify;
proxy_set_header x-ssl-client-s-dn $ssl_client_s_dn;
```
```apache
# Apache
RequestHeader set x-ssl-client-cert ""
RequestHeader set x-ssl-client-cert "%{SSL_CLIENT_CERT}s"
RequestHeader set x-ssl-client-verify ""
RequestHeader set x-ssl-client-verify "%{SSL_CLIENT_VERIFY}s"
RequestHeader set x-ssl-client-s-dn ""
RequestHeader set x-ssl-client-s-dn "%{SSL_CLIENT_S_DN}s"
```
You should also consider hosting the endpoints supporting client authentication, on a separate host name or port in order to prevent unintended impact on the TLS behaviour of your other endpoints, e.g. Discovery or the authorization endpoint and changing the discovery values for them with a post-middleware.
You should also consider hosting the endpoints supporting client authentication, on a separate host name or port in order to prevent unintended impact on the TLS behaviour of your other endpoints, e.g. Discovery or the authorization endpoint and changing the discovery values for them with a post-middleware. When doing that be sure to remove the client provided headers of the same name on the non-MTLS enabled host name / port in your proxy setup.


```js
Expand Down
16 changes: 8 additions & 8 deletions lib/helpers/client_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ const { InvalidClientMetadata } = require('./errors');
const sectorIdentifier = require('./sector_identifier');
const instance = require('./weak_cache');

const clientAuthEndpoints = ['token', 'introspection', 'revocation'];
const W3CEmailRegExp = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;

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

const W3CEmailRegExp = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
function checkClientAuth(schema) {
return !!clientAuthEndpoints.find(endpoint => ['private_key_jwt', 'self_signed_tls_client_auth'].includes(schema[`${endpoint}_endpoint_auth_method`]));
}

module.exports = function getSchema(provider) {
const configuration = instance(provider).configuration();
Expand Down Expand Up @@ -240,7 +245,7 @@ module.exports = function getSchema(provider) {
return lengths;
}, new Set());

['token', 'introspection', 'revocation'].forEach((endpoint) => {
clientAuthEndpoints.forEach((endpoint) => {
switch (this[`${endpoint}_endpoint_auth_method`]) {
case 'client_secret_jwt':
if (this[`${endpoint}_endpoint_auth_signing_alg`] === undefined) {
Expand Down Expand Up @@ -304,12 +309,7 @@ module.exports = function getSchema(provider) {
}
});

const requireJwks = this.token_endpoint_auth_method === 'private_key_jwt'
|| this.introspection_endpoint_auth_method === 'private_key_jwt'
|| this.revocation_endpoint_auth_method === 'private_key_jwt'
// || this.token_endpoint_auth_method === 'self_signed_tls_client_auth'
// || this.introspection_endpoint_auth_method === 'self_signed_tls_client_auth'
// || this.revocation_endpoint_auth_method === 'self_signed_tls_client_auth'
const requireJwks = checkClientAuth(this)
|| (requestSignAlgRequiringJwks.exec(this.request_object_signing_alg))
|| (encAlgRequiringJwks.exec(this.id_token_encrypted_response_alg))
|| (encAlgRequiringJwks.exec(this.userinfo_encrypted_response_alg))
Expand Down
36 changes: 21 additions & 15 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,12 @@ const DEFAULTS = {
* description: Enables Certificate Bound Access Tokens. Clients may be registered with
* `tls_client_certificate_bound_access_tokens` to indicate intention to receive mutual TLS client
* certificate bound access tokens.
*
* example: Setting up the environment for Certificate Bound Access Tokens
* To enable Certificate Bound Access Tokens the provider expects `x-ssl-client-cert`
* header to be presented by your TLS-offloading proxy with the variable value set by this
* proxy. An important aspect is to sanitize the inbound request header at the proxy.
* To enable Certificate Bound Access Tokens the provider expects your TLS-offloading proxy to
* handle the client certificate validation, parsing, handling, etc. Once set up you are expected
* to forward `x-ssl-client-cert` header with variable values set by this proxy. An important
* aspect is to sanitize the inbound request headers at the proxy.
*
* <br/><br/>
*
Expand All @@ -305,12 +306,11 @@ const DEFAULTS = {
* RequestHeader set x-ssl-client-cert "%{SSL_CLIENT_CERT}s"
* ```
*
* You should also consider hosting the token and userinfo endpoints on a separate host name
* or port in order to prevent unintended impact on the TLS behaviour of your other
* You should also consider hosting the endpoints supporting client authentication, on a separate
* host name or port in order to prevent unintended impact on the TLS behaviour of your other
* endpoints, e.g. discovery or the authorization endpoint and changing the discovery values
* for these with a post-middleware since you need MTLS to issue tokens (token_endpoint) and
* userinfo will now act as a Resource Server supporting Certificate Bound Access Tokens and
* therefore needs to handle client certificates.
* for them with a post-middleware. When doing that be sure to remove the client
* provided headers of the same name on the non-MTLS enabled host name / port in your proxy setup.
*
* ```js
* provider.use(async (ctx, next) => {
Expand Down Expand Up @@ -731,13 +731,15 @@ const DEFAULTS = {
* 'none',
* 'client_secret_basic', 'client_secret_post',
* 'client_secret_jwt', 'private_key_jwt',
* 'tls_client_auth',
* 'tls_client_auth', 'self_signed_tls_client_auth',
* ]
* ```
* example: Setting up the environment for tls_client_auth
* To enable `tls_client_auth` the provider expects `x-ssl-client-verify` and `x-ssl-client-s-dn`
* headers to be presented by your TLS-offloading proxy with the variable values set by these
* proxies. An important aspect is to sanitize the inbound request headers at the proxy.
* example: Setting up the environment for tls_client_auth and self_signed_tls_client_auth
* To enable MTLS based authentication methods the provider expects your TLS-offloading proxy to
* handle the client certificate validation, parsing, handling, etc. Once set up you are expected
* to forward `x-ssl-client-verify`, `x-ssl-client-s-dn` and `x-ssl-client-cert` headers with
* variable values set by this proxy. An important aspect is to sanitize the inbound request
* headers at the proxy.
*
* <br/><br/>
*
Expand All @@ -759,12 +761,15 @@ const DEFAULTS = {
*
* ```nginx
* # NGINX
* proxy_set_header x-ssl-client-cert $ssl_client_cert;
* proxy_set_header x-ssl-client-verify $ssl_client_verify;
* proxy_set_header x-ssl-client-s-dn $ssl_client_s_dn;
* ```
*
* ```apache
* # Apache
* RequestHeader set x-ssl-client-cert ""
* RequestHeader set x-ssl-client-cert "%{SSL_CLIENT_CERT}s"
* RequestHeader set x-ssl-client-verify ""
* RequestHeader set x-ssl-client-verify "%{SSL_CLIENT_VERIFY}s"
* RequestHeader set x-ssl-client-s-dn ""
Expand All @@ -774,7 +779,8 @@ const DEFAULTS = {
* You should also consider hosting the endpoints supporting client authentication, on a separate
* host name or port in order to prevent unintended impact on the TLS behaviour of your other
* endpoints, e.g. discovery or the authorization endpoint and changing the discovery values
* for them with a post-middleware.
* for them with a post-middleware. When doing that be sure to remove the client
* provided headers of the same name on the non-MTLS enabled host name / port in your proxy setup.
*
* ```js
* provider.use(async (ctx, next) => {
Expand Down
45 changes: 40 additions & 5 deletions lib/models/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const crypto = require('crypto');
const assert = require('assert');

const _ = require('lodash');
const { JWK: { createKeyStore } } = require('node-jose');
const { JWK: { createKeyStore, asKey } } = require('node-jose');
const LRU = require('lru-cache');
const base64url = require('base64url');
const uuid = require('uuid/v4');
Expand All @@ -16,10 +16,10 @@ const getSchema = require('../helpers/client_schema');
const sectorIdentifier = require('../helpers/sector_identifier');
const { LOOPBACKS } = require('../consts/client_attributes');

const KEY_ATTRIBUTES = ['crv', 'e', 'kid', 'kty', 'n', 'use', 'x', 'y'];
const KEY_ATTRIBUTES = ['crv', 'e', 'kid', 'kty', 'n', 'use', 'x', 'y', 'x5c'];
const KEY_TYPES = ['RSA', 'EC'];

const nonSecretAuthMethods = ['private_key_jwt', 'none', 'tls_client_auth'];
const nonSecretAuthMethods = ['private_key_jwt', 'none', 'tls_client_auth', 'self_signed_tls_client_auth'];
const clientEncryptions = [
'id_token_encrypted_response_alg',
'request_object_encryption_alg',
Expand Down Expand Up @@ -52,6 +52,29 @@ function stripFragment(uri) {
return format(new URL(uri), { fragment: false });
}

async function validateCertificateChain(jwk) {
const x5c = jwk.get('x5c');
if (!Array.isArray(x5c) || !x5c.length) {
throw new InvalidClientMetadata('when provided, JWK x5c must be non-empty an array');
}
const cert = x5c[0];
const crt = await asKey(Buffer.from(cert, 'base64'), 'x509').catch(() => {
throw new InvalidClientMetadata('invalid x5c provided');
});
switch (jwk.kty) {
case 'RSA':
assert(crt.get('n').equals(jwk.get('n')), 'cert and key n mismatch');
assert(crt.get('e').equals(jwk.get('e')), 'cert and key e mismatch');
break;
case 'EC':
assert.equal(crt.get('crv'), jwk.get('crv'), 'cert and key crv mismatch');
assert(crt.get('x').equals(jwk.get('x')), 'cert and key x mismatch');
assert(crt.get('y').equals(jwk.get('y')), 'cert and key y mismatch');
break;
default:
}
}

const JWKStore = createKeyStore().constructor;

Object.defineProperties(JWKStore.prototype, {
Expand Down Expand Up @@ -133,7 +156,13 @@ Object.defineProperties(JWKStore.prototype, {
if (handled(key.kty) && !kids.includes(key.kid)) promises.push(this.remove(key));
});

await Promise.all(promises);
const keys = await Promise.all(promises);
await Promise.all(keys.filter((key) => {
if (key && key.get('x5c')) {
return true;
}
return false;
}).map(validateCertificateChain));
} catch (err) {
throw new InvalidClientMetadata(`jwks_uri could not be refreshed (${err.message})`);
}
Expand Down Expand Up @@ -242,7 +271,13 @@ module.exports = function getClient(provider) {
})
.value();

await Promise.all(promises);
const keys = await Promise.all(promises);
await Promise.all(keys.filter((key) => {
if (key && key.get('x5c')) {
return true;
}
return false;
}).map(validateCertificateChain));

return client;
}
Expand Down
29 changes: 28 additions & 1 deletion lib/shared/token_auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ module.exports = function tokenAuth(provider, endpoint) {
if (clientSecret !== undefined) throw new InvalidRequest('client authentication must only be provided using one mechanism');
} else if (clientId !== undefined) {
ctx.oidc.authorization.clientId = clientId;
ctx.oidc.authorization.methods = ['client_secret_post', 'none', 'tls_client_auth'];
ctx.oidc.authorization.methods = ['client_secret_post', 'none', 'tls_client_auth', 'self_signed_tls_client_auth'];
}

if (clientAssertion !== undefined) {
Expand Down Expand Up @@ -178,6 +178,33 @@ module.exports = function tokenAuth(provider, endpoint) {
if (ctx.get('x-ssl-client-s-dn') !== ctx.oidc.client.tlsClientAuthSubjectDn) {
throw new InvalidClientAuth('Subject DN does not match the registered one');
}

break;

case 'self_signed_tls_client_auth': {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidClientAuth('client cert was not provided');
}

if (ctx.oidc.client.keystore.stale()) await ctx.oidc.client.keystore.refresh();

const normalized = cert.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '');
const match = ctx.oidc.client.keystore.all().find((key) => {
const [x5c] = key.get('x5c') || [];
if (x5c && x5c === normalized) {
return true;
}
return false;
});

if (!match) {
throw new InvalidClientAuth('unregistered certificate provided');
}

break;
}
}

await next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { expect } = require('chai');

const bootstrap = require('../test_helper');

const crt = readFileSync('./test/client.crt').toString();
const crt = readFileSync('./test/jwks/client.crt').toString();
const expectedS256 = 'eXvgMeO-8uLw0FGYkJefOXSFHOnbbcfv95rIYCPsbpo';

describe('features.certificateBoundAccessTokens', () => {
Expand Down
22 changes: 21 additions & 1 deletion test/client_auth/client_auth.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ const { cloneDeep } = require('lodash');

const config = cloneDeep(require('../default.config'));
const clientKey = require('../client.sig.key');
const mtlsKeys = require('../jwks/jwks.json');

const rsaKeys = cloneDeep(mtlsKeys);
rsaKeys.keys.splice(0, 1);

config.tokenEndpointAuthMethods = [
'none',
Expand All @@ -10,6 +14,7 @@ config.tokenEndpointAuthMethods = [
'private_key_jwt',
'client_secret_jwt',
'tls_client_auth',
'self_signed_tls_client_auth',
];

module.exports = {
Expand Down Expand Up @@ -54,9 +59,24 @@ module.exports = {
keys: [clientKey],
},
}, {
client_id: 'client-pki-tls',
client_id: 'client-pki-mtls',
redirect_uris: ['https://client.example.com/cb'],
token_endpoint_auth_method: 'tls_client_auth',
tls_client_auth_subject_dn: 'foobar',
}, {
client_id: 'client-self-signed-mtls',
redirect_uris: ['https://client.example.com/cb'],
token_endpoint_auth_method: 'self_signed_tls_client_auth',
jwks: mtlsKeys,
}, {
client_id: 'client-self-signed-mtls-rsa',
redirect_uris: ['https://client.example.com/cb'],
token_endpoint_auth_method: 'self_signed_tls_client_auth',
jwks: rsaKeys,
}, {
client_id: 'client-self-signed-mtls-jwks_uri',
redirect_uris: ['https://client.example.com/cb'],
token_endpoint_auth_method: 'self_signed_tls_client_auth',
jwks_uri: 'https://client.example.com/jwks',
}],
};
Loading

0 comments on commit 9a1f0a3

Please sign in to comment.