mTLS with trusted headers
PingGateway can validate the thumbprint of certificate-bound access tokens by reading the client certificate from a configured, trusted HTTP header.
Use this method when TLS is terminated at a reverse proxy or load balancer before PingGateway. PingGateway cannot authenticate the client through the TLS connection’s client certificate because:
-
If the connection is over TLS, the connection presents the certificate of the TLS termination point before PingGateway.
-
If the connection is not over TLS, the connection presents no client certificate.
If the client is connected directly to PingGateway through a TLS connection, for which PingGateway is the TLS termination point, use the example in mTLS with client certificates.
Configure the proxy or load balancer to:
-
Forward the encoded certificate to PingGateway in the trusted header. Encode the certificate in an HTTP-header compatible format that can convey a full certificate, so that PingGateway can rebuild the certificate.
-
Strip the trusted header from incoming requests, and change the default header name to something an attacker can’t guess.
Because there is a trust relationship between PingGateway and the TLS termination point, PingGateway doesn’t authenticate the contents of the trusted header. PingGateway accepts any value in a header from a trusted TLS termination point.
The following image illustrates the connections and certificates required by the example:
Follow the steps in this example to try mTLS using trusted headers.
Before you start
-
Set up the keystores, truststores, AM, and PingGateway as described in mTLS with client certificates.
-
URL-encode the value of
$oauth2_client_keystore_directory/client.cert.pem
.PingGateway needs the certificate to validate the confirmation key.
Make PingGateway an RS
-
Add the following route to PingGateway:
-
Linux
-
Windows
$HOME/.openig/config/routes/mtls-header.json
%appdata%\OpenIG\config\routes\mtls-header.json
{ "name": "mtls-header", "condition": "${find(request.uri.path, '/mtls-header')}", "heap": [ { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "agent": { "username": "ig_agent", "passwordSecretId": "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1", "url": "http://am.example.com:8088/openam" } } ], "handler": { "type": "Chain", "capture": "all", "config": { "filters": [ { "name": "CertificateThumbprintFilter-1", "type": "CertificateThumbprintFilter", "config": { "certificate": "${pemCertificate(urlDecode(request.headers['x-ssl-cert'][0]))}", "failureHandler": { "type": "ScriptableHandler", "config": { "type": "application/x-groovy", "source": [ "def response = new Response(Status.TEAPOT);", "response.entity = 'Failure in CertificateThumbprintFilter'", "return response" ] } } } }, { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "test" ], "requireHttps": false, "accessTokenResolver": { "type": "ConfirmationKeyVerifierAccessTokenResolver", "config": { "delegate": { "name": "token-resolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } } } } } } } ], "handler": { "name": "StaticResponseHandler-1", "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/plain; charset=UTF-8" ] }, "entity": "mTLS\n Valid token: ${contexts.oauth2.accessToken.token}\n Confirmation keys: ${contexts.oauth2}" } } } } }
Notice the following features of the route compared to
mtls-certificate.json
:-
The route matches requests to
/mtls-header
. -
The
CertificateThumbprintFilter
extracts the client certificate from the trusted header.In this example, the filter is configured as if NGINX were sending the trusted header. For additional examples, refer to the CertificateThumbprintFilter examples.
The filter computes the certificate thumbprint and makes the thumbprint available to the
ConfirmationKeyVerifierAccessTokenResolver
.
-
Try mTLS with trusted headers
-
Get a certificate-bound access token from AM as the client application:
$ export ACCESS_TOKEN=$(curl \ --request POST \ --cacert $am_keystore_directory/openam-server.cert.pem \ --cert $oauth2_client_keystore_directory/client.cert.pem \ --key $oauth2_client_keystore_directory/client.key.pem \ --header 'cache-control: no-cache' \ --header 'content-type: application/x-www-form-urlencoded' \ --data 'client_id=client-application' \ --data 'grant_type=client_credentials' \ --data 'scope=test' \ https://am.example.com:8445/openam/oauth2/access_token | jq -r .access_token)
Notice the client gets an access token without using a client secret. It authenticates with its self-signed certificate.
-
Introspect the access token on AM using the PingGateway agent credentials:
$ curl \ --request POST \ --user ig_agent:password \ --header 'content-type: application/x-www-form-urlencoded' \ --data "token=$ACCESS_TOKEN" \ http://am.example.com:8088/openam/oauth2/realms/root/introspect | jq { "active": true, "scope": "test", "realm": "/", "client_id": "client-application", "user_id": "client-application", "username": "client-application", "token_type": "Bearer", "exp": 1724249775, "sub": "(age!client-application)", "iss": "http://am.example.com:8088/openam/oauth2", "subname": "client-application", "cnf": { "x5t#S256": "TTXH27YoFFCgOAQ0189KMBKeqxU1ZfZ_2nYGxrsjHlM" }, "authGrantId": "LMhPEqYaxMbrd2zXMAQjHcc8JYE", "auditTrackingId": "962fd5f6-fc2f-43c1-b044-ed1eb33d7aef-403" }
The
cnf
property indicates the value of the confirmation code:-
x5
: X509 certificate -
t
: thumbprint -
#
: separator -
S256
: algorithm used to hash the raw certificate bytes
-
-
Access the PingGateway route to validate the confirmation key.
The <url-encoded-cert> is the URL-encoded value of
$oauth2_client_keystore_directory/client.cert.pem
:$ curl \ --request POST \ --cacert $ig_keystore_directory/ig.example.com-certificate.pem \ --header "Authorization: Bearer $ACCESS_TOKEN" \ --header 'x-ssl-cert: <url-encoded-cert>' https://ig.example.com:8443/mtls-header mTLS Valid token: UnUxGRuwXx_ugUCvNKFM3GJo3Cc Confirmation keys: { ... }
The command displays the validated token and confirmation keys.