Skip to content

Commit

Permalink
feat: enable Certificate Bound Access Tokens
Browse files Browse the repository at this point in the history
Defined in https://tools.ietf.org/html/draft-ietf-oauth-mtls-11 with
this feature the provider is able to bind issued access tokens to a
client certificate. Such a binding is accomplished by associating the
certificate with the token in a way that can be accessed by the
protected resource, such as embedding the certificate hash in an issued
JWT access token directly or through additional token introspection
response claims.
It relies on your TLS-offloading proxy to process and send metadata
about the client X.509 certificate via a header to the upstream node.js
application.

See the configuration doc section for more details.
  • Loading branch information
panva committed Sep 26, 2018
1 parent ce2bf66 commit f43d820
Show file tree
Hide file tree
Showing 25 changed files with 601 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The following drafts/experimental specifications are implemented by oidc-provide
- [RFC7592 - OAuth 2.0 Dynamic Client Registration Management Protocol (Update and Delete)][registration-management]
- [OAuth 2.0 Web Message Response Mode - draft 00][wmrm]
- [OAuth 2.0 Device Flow for Browserless and Input Constrained Devices - draft 12][device-flow]
- [OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens - draft 11][mtls]
- [JWT Response for OAuth Token Introspection - draft 01][jwt-introspection]

Updates to draft and experimental specification versions are released as MINOR library versions,
Expand Down
65 changes: 61 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ Enable/disable features.
pkce: true,
alwaysIssueRefresh: false,
backchannelLogout: false,
certificateBoundAccessTokens: false,
claimsParameter: false,
clientCredentials: false,
conformIdTokenClaims: true,
Expand Down Expand Up @@ -763,6 +764,49 @@ _**default value**_:
false
```

### features.certificateBoundAccessTokens

[draft-ietf-oauth-mtls-11](https://tools.ietf.org/html/draft-ietf-oauth-mtls-11) - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens

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.



_**default value**_:
```js
false
```
<details>
<summary>(Click to expand) Setting up the environment for Certificate Bound Access Tokens</summary>
<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


```nginx
# NGINX
proxy_set_header x-ssl-client-cert $ssl_client_cert;
```
```apache
# Apache
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.


```js
provider.use(async (ctx, next) => {
await next();
if (ctx.oidc.route === 'discovery' && ctx.method === 'GET' && ctx.status === 200) {
ctx.body.userinfo_endpoint = '...';
ctx.body.token_endpoint = '...';
}
});
```
</details>

### features.claimsParameter

[Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.5) - Requesting Claims using the "claims" Request Parameter
Expand Down Expand Up @@ -1608,7 +1652,7 @@ async interactionUrl(ctx, interaction) {

### introspectionEndpointAuthMethods

Array of Client Authentication methods supported by this OP's Introspection Endpoint. If no configuration value is provided the same values as for tokenEndpointAuthMethods will be used. Supported values list is the same as for tokenEndpointAuthMethods
Array of Client Authentication methods supported by this OP's Introspection Endpoint. If no configuration value is provided the same values as for tokenEndpointAuthMethods will be used. Supported values list is the same as for tokenEndpointAuthMethods.

_**affects**_: discovery, client authentication for introspection, registration and registration management

Expand Down Expand Up @@ -1784,7 +1828,7 @@ _**affects**_: authorization, discovery, registration, registration management

### revocationEndpointAuthMethods

Array of Client Authentication methods supported by this OP's Revocation Endpoint. If no configuration value is provided the same values as for tokenEndpointAuthMethods will be used. Supported values list is the same as for tokenEndpointAuthMethods
Array of Client Authentication methods supported by this OP's Revocation Endpoint. If no configuration value is provided the same values as for tokenEndpointAuthMethods will be used. Supported values list is the same as for tokenEndpointAuthMethods.

_**affects**_: discovery, client authentication for revocation, registration and registration management

Expand Down Expand Up @@ -1876,11 +1920,11 @@ _**default value**_:
```
</details>
<details>
<summary>(Click to expand) Setting up tls_client_auth</summary>
<summary>(Click to expand) Setting up the environment for 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)__ `require` - if you only support tls_client_auth, `optional` if you also support additional non-MTLS based authentication methods, `optional_no_ca` - if you also support additional non-MTLS based authentication methods AND self_signed_tls_client_auth (not implemented yet) <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 `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


```nginx
Expand All @@ -1895,6 +1939,19 @@ 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.


```js
provider.use(async (ctx, next) => {
await next();
if (ctx.oidc.route === 'discovery' && ctx.method === 'GET' && ctx.status === 200) {
ctx.body.token_endpoint = '...';
ctx.body.introspection_endpoint = '...';
ctx.body.revocation_endpoint = '...';
}
});
```
</details>

### ttl
Expand Down
4 changes: 4 additions & 0 deletions lib/actions/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ module.exports = function discoveryAction(provider) {
ctx.body.device_authorization_endpoint = ctx.oidc.urlFor('device_authorization');
}

if (config.features.certificateBoundAccessTokens) {
ctx.body.tls_client_certificate_bound_access_tokens = true;
}

defaults(ctx.body, config.discovery);

await next();
Expand Down
9 changes: 9 additions & 0 deletions lib/actions/grants/authorization_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ module.exports.handler = function getAuthorizationCodeHandler(provider) {
sid: code.sid,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidGrant('MTLS client certificate missing');
}
at.setS256Thumbprint(cert);
}

at.setAudiences(await audiences(ctx, account.accountId, at, 'access_token'));

const accessToken = await at.save();
Expand Down
10 changes: 10 additions & 0 deletions lib/actions/grants/client_credentials.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const instance = require('../../helpers/weak_cache');
const { InvalidGrant } = require('../../helpers/errors');

module.exports.handler = function getClientCredentialsHandler(provider) {
return async function clientCredentialsResponse(ctx, next) {
Expand Down Expand Up @@ -26,6 +27,15 @@ module.exports.handler = function getClientCredentialsHandler(provider) {
scope: scopes.join(' ') || undefined,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidGrant('MTLS client certificate missing');
}
token.setS256Thumbprint(cert);
}

token.setAudiences(await audiences(ctx, undefined, token, 'client_credentials'));

const value = await token.save();
Expand Down
9 changes: 9 additions & 0 deletions lib/actions/grants/device_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ module.exports.handler = function getDeviceCodeHandler(provider) {
sid: code.sid,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidGrant('MTLS client certificate missing');
}
at.setS256Thumbprint(cert);
}

at.setAudiences(await audiences(ctx, account.accountId, at, 'access_token'));

const accessToken = await at.save();
Expand Down
9 changes: 9 additions & 0 deletions lib/actions/grants/refresh_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ module.exports.handler = function getRefreshTokenHandler(provider) {
gty: refreshToken.gty,
});

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
const cert = ctx.get('x-ssl-client-cert');

if (!cert) {
throw new InvalidGrant('MTLS client certificate missing');
}
at.setS256Thumbprint(cert);
}

if (!at.gty.endsWith(gty)) {
at.gty = `${at.gty} ${gty}`;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/actions/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ module.exports = function introspectionAction(provider) {
scope: token.scope,
});

if (token['x5t#S256']) {
ctx.body.cnf = { 'x5t#S256': token['x5t#S256'] };
}

await next();
},
];
Expand Down
8 changes: 8 additions & 0 deletions lib/actions/userinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const bodyParser = require('../shared/conditional_body');
const rejectDupes = require('../shared/reject_dupes');
const params = require('../shared/assemble_params');
const noCache = require('../shared/no_cache');
const getS256Thumbprint = require('../helpers/calculate_thumbprint');

const PARAM_LIST = [
'scope',
Expand Down Expand Up @@ -52,6 +53,13 @@ module.exports = function userinfoAction(provider) {
const accessToken = await provider.AccessToken.find(ctx.oidc.bearer);
ctx.assert(accessToken, new InvalidToken());

if (accessToken['x5t#S256']) {
const cert = ctx.get('x-ssl-client-cert');
if (!cert || accessToken['x5t#S256'] !== getS256Thumbprint(cert)) {
throw new InvalidToken();
}
}

uuidToGrantId('switched from uuid=%s to value of grantId=%s', ctx.oidc.uuid, accessToken.grantId);
ctx.oidc.uuid = accessToken.grantId;

Expand Down
1 change: 1 addition & 0 deletions lib/consts/client_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const BOOL = [
'backchannel_logout_session_required',
'frontchannel_logout_session_required',
'require_auth_time',
'tls_client_certificate_bound_access_tokens',
];

const ARYS = [
Expand Down
8 changes: 8 additions & 0 deletions lib/helpers/calculate_thumbprint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { createHash } = require('crypto');

const base64url = require('base64url');

module.exports = function getS256Thumbprint(cert) {
const normalized = cert.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s|=)/g, '');
return base64url(createHash('sha256').update(Buffer.from(normalized, 'base64')).digest());
};
5 changes: 5 additions & 0 deletions lib/helpers/client_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ module.exports = function getSchema(provider) {
DEFAULT.web_message_uris = [];
}

if (features.certificateBoundAccessTokens) {
RECOGNIZED_METADATA.push('tls_client_certificate_bound_access_tokens');
DEFAULT.tls_client_certificate_bound_access_tokens = false;
}

instance(provider).RECOGNIZED_METADATA = RECOGNIZED_METADATA;

const ENUM = {
Expand Down
85 changes: 79 additions & 6 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,64 @@ const DEFAULTS = {
*/
backchannelLogout: false,

/*
* features.certificateBoundAccessTokens
*
* title: [draft-ietf-oauth-mtls-11](https://tools.ietf.org/html/draft-ietf-oauth-mtls-11) - OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens
*
* 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.
*
* <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
* # NGINX
* proxy_set_header x-ssl-client-cert $ssl_client_cert;
* ```
*
* ```apache
* # Apache
* 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.
*
* ```js
* provider.use(async (ctx, next) => {
* await next();
* if (ctx.oidc.route === 'discovery' && ctx.method === 'GET' && ctx.status === 200) {
* ctx.body.userinfo_endpoint = '...';
* ctx.body.token_endpoint = '...';
* }
* });
* ```
*
*/
certificateBoundAccessTokens: false,

/*
* features.claimsParameter
*
Expand Down Expand Up @@ -676,7 +734,7 @@ const DEFAULTS = {
* 'tls_client_auth',
* ]
* ```
* example: Setting up 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.
Expand All @@ -687,14 +745,13 @@ const DEFAULTS = {
*
* <br/><br/>
*
* __`SSLVerifyClient` (Apache) / `ssl_verify_client` (NGINX)__
* `require` - if you only support tls_client_auth,
* `optional` if you also support additional non-MTLS based authentication methods,
* `optional_no_ca` - if you also support additional non-MTLS based authentication methods AND self_signed_tls_client_auth (not implemented yet)
* __`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
* __`SSLCACertificateFile` or `SSLCACertificatePath` (Apache) / `ssl_client_certificate` (NGINX)__
* with the values pointing to your accepted CA Certificates.
*
* <br/><br/>
*
Expand All @@ -714,6 +771,22 @@ const DEFAULTS = {
* 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.
*
* ```js
* provider.use(async (ctx, next) => {
* await next();
* if (ctx.oidc.route === 'discovery' && ctx.method === 'GET' && ctx.status === 200) {
* ctx.body.token_endpoint = '...';
* ctx.body.introspection_endpoint = '...';
* ctx.body.revocation_endpoint = '...';
* }
* });
* ```
*
*/
tokenEndpointAuthMethods: [
'none',
Expand Down
2 changes: 2 additions & 0 deletions lib/models/access_token.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const setAudiences = require('./mixins/set_audiences');
const hasFormat = require('./mixins/has_format');
const hasGrantType = require('./mixins/has_grant_type');
const isCertBound = require('./mixins/is_cert_bound');
const apply = require('./mixins/apply');

module.exports = provider => class AccessToken extends apply([
setAudiences,
isCertBound,
hasGrantType,
hasFormat(provider, 'AccessToken', provider.BaseToken),
]) {
Expand Down
Loading

0 comments on commit f43d820

Please sign in to comment.