mTLS with client certificates
PingGateway can validate the thumbprint of certificate-bound access tokens by reading the client certificate from the TLS connection.
For this example, the client must be connected directly to PingGateway through a TLS connection, for which PingGateway is the TLS termination point, as shown in the following image. If TLS is terminated at a reverse proxy or load balancer before PingGateway, use the example in mTLS with trusted headers.
Follow the steps in this example to try mTLS using standard TLS client certificate authentication.
Prepare the keys
-
To make it easy to identify and refer to secrets used in mTLS examples, create directories and environment variables:
$ export ig_keystore_directory=/path/to/ig/secrets $ export am_keystore_directory=/path/to/am/secrets $ export oauth2_client_keystore_directory=/path/to/client/secrets
-
Create keys and certificates for the example:
-
Create self-signed RSA key pairs for AM and the client:
$ keytool -genkeypair \ -alias openam-server \ -keyalg RSA \ -keysize 2048 \ -keystore $am_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -keypass changeit \ -validity 360 \ -dname CN=am.example.com,O=Example,C=FR
$ keytool -genkeypair \ -alias oauth2-client \ -keyalg RSA \ -keysize 2048 \ -keystore $oauth2_client_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -keypass changeit \ -validity 360 \ -dname CN=test
-
Export the certificates to .pem so that the
curl
client can verify the identity of the AM and PingGateway servers:$ keytool -export \ -rfc \ -alias openam-server \ -keystore $am_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -file $am_keystore_directory/openam-server.cert.pem Certificate stored in file .../openam-server.cert.pem
-
Extract the certificate and client private key to .pem so that the
curl
command can identity itself as the client for the HTTPS connection:$ keytool -export \ -rfc \ -alias oauth2-client \ -keystore $oauth2_client_keystore_directory/keystore.p12 \ -storepass changeit \ -storetype PKCS12 \ -file $oauth2_client_keystore_directory/client.cert.pem Certificate stored in file .../client.cert.pem
$ openssl pkcs12 \ -in $oauth2_client_keystore_directory/keystore.p12 \ -nocerts \ -nodes \ -passin pass:changeit \ -out $oauth2_client_keystore_directory/client.key.pem ...verified OK
-
Create the CACerts truststore so that AM can validate the client identity:
$ keytool -import \ -noprompt \ -trustcacerts \ -file $oauth2_client_keystore_directory/client.cert.pem \ -keystore $oauth2_client_keystore_directory/cacerts.p12 \ -storepass changeit \ -storetype PKCS12 \ -alias client-cert Certificate was added to keystore
-
In the ig_keystore_directory, add a file called
keystore.pass
containing the keystore password:$ cd $ig_keystore_directory $ echo -n 'changeit' > keystore.pass
-
Prepare AM
-
Configure AM for HTTPS connections using information in the AM documentation about Secure HTTP and LDAP connections.
Learn more
-
Add a connector configuration for port
8445
to AM’s Tomcatserver.xml
, replacing the values for the keystore directories with your path. If the file already contains a connector for the port, edit that connector or replace it:<Connector port="8445" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"> <SSLHostConfig protocols="+TLSv1.2,-TLSv1.1,-TLSv1,-SSLv2Hello,-SSLv3" certificateVerification="optionalNoCA" truststoreFile="oauth2_client_keystore_directory/cacerts.p12" truststorePassword="changeit" truststoreType="PKCS12"> <Certificate certificateKeystoreFile="am_keystore_directory/keystore.p12" certificateKeystorePassword="changeit" certificateKeystoreType="PKCS12"/> </SSLHostConfig> </Connector>
-
In AM, export an environment variable for the base64-encoded value of the password (
changeit
) for thecacerts.p12
truststore:$ export PASSWORDSECRETID='Y2hhbmdlaXQ='
-
Restart AM, and make sure you can access it on the secure port
https://am.example.com:8445/openam
.
-
-
Configure AM for mutual TLS using information in the AM documentation about Mutual TLS.
Learn more
-
In the AM admin UI, select Applications > Agents > Identity Gateway, and register a PingGateway agent that can introspect access tokens:
-
Agent ID:
ig_agent
-
Password:
password
-
Token Introspection:
Realm Only
Use secure passwords in a production environment. Consider using a password manager to generate secure passwords.
-
-
Configure an OAuth 2.0 Authorization Server with settings for mTLS:
-
Select Services > Add a Service > OAuth2 Provider, and add a service with the default values.
-
On the Advanced tab, select the following value:
-
Support TLS Certificate-Bound Access Tokens: enabled
-
-
-
Configure an OAuth 2.0 client to request access tokens:
-
Select Applications > OAuth 2.0 > Clients, and add a client with the following values:
-
Client ID:
client-application
-
Client secret:
password
-
Scope(s):
test
-
-
(Optional) On the Core tab, switch to using a client secret associated with a secret label by setting a Secret Label Identifier and mapping the label to a secret.
To learn more, read Create a client profile and Map and rotate secrets in the AM documentation.
-
On the Advanced tab, select the following values:
-
Grant Types:
Client Credentials
The
password
is the only grant type used by the client in the example. -
Token Endpoint Authentication Method:
self_signed_tls_client_auth
-
-
On the Signing and Encryption tab, set the following values:
-
mTLS Self-Signed Certificate: Enter the content of the X.509 certificate, client.cert.pem.
-
mTLS Subject DN:
CN=test
When this option is set, AM requires the subject DN in the client certificate to have the same value. This ensures that the certificate is from the client and not just a valid certificate trusted by the trust manager.
-
Public key selector:
x509
-
Use Certificate-Bound Access Tokens: Enabled
-
-
-
Prepare PingGateway
-
Configure PingGateway for HTTPS using information from Configure PingGateway for TLS (server-side).
This example uses a self-signed certificate stored in a PEM file.
-
Configure PingGateway for mutual TLS using information from Configure PingGateway for mTLS (server-side).
This example uses the following
admin.json
with a SecretsTrustManager:-
Linux
-
Windows
$HOME/.openig/config/admin.json
%appdata%\OpenIG\config\admin.json
{ "mode": "DEVELOPMENT", "properties": { "ig_keystore_directory": "/path/to/ig/secrets", "oauth2_client_keystore_directory": "/path/to/client/secrets" }, "connectors": [ { "port": 8080 }, { "port": 8443, "tls": { "type": "ServerTlsOptions", "config": { "alpn": { "enabled": true }, "clientAuth": "REQUEST", "keyManager": "SecretsKeyManager-1", "trustManager": "SecretsTrustManager-1" } } } ], "heap": [ { "name": "SecretsPasswords", "type": "FileSystemSecretStore", "config": { "directory": "&{ig_keystore_directory}", "format": "PLAIN" } }, { "name": "SecretsKeyManager-1", "type": "SecretsKeyManager", "config": { "signingSecretId": "key.manager.secret.id", "secretsProvider": "ServerIdentityStore" } }, { "name": "SecretsTrustManager-1", "type": "SecretsTrustManager", "config": { "verificationSecretId": "trust.manager.secret.id", "secretsProvider": { "type": "KeyStoreSecretStore", "config": { "file": "&{oauth2_client_keystore_directory}/cacerts.p12", "storePasswordSecretId": "keystore.pass", "secretsProvider": "SecretsPasswords", "mappings": [ { "secretId": "trust.manager.secret.id", "aliases": ["client-cert"] } ] } } } }, { "name": "ServerIdentityStore", "type": "FileSystemSecretStore", "config": { "format": "PLAIN", "directory": "&{ig_keystore_directory}", "suffix": ".pem", "mappings": [{ "secretId": "key.manager.secret.id", "format": { "type": "PemPropertyFormat" } }] } } ] }
Notice the
ServerTlsOptions
:-
The trust manager settings let PingGateway trust the self-signed client certificate.
-
The
"clientAuth": "REQUEST"
setting permits optional HTTPS mutual authentication.Clients using mTLS authenticate with their certificates when setting up HTTPS. Other clients can connect over HTTPS without presenting a client certificate.
-
-
Replace the values of the secret directories with your directories, and then start PingGateway.
Make PingGateway an RS
-
Configure PingGateway as a resource server for mTLS:
-
Set an environment variable for the PingGateway agent password, and then restart PingGateway:
$ export AGENT_SECRET_ID='cGFzc3dvcmQ='
The password is retrieved by a
SystemAndEnvSecretStore
, and must be base64-encoded. -
Add the following route to PingGateway:
-
Linux
-
Windows
$HOME/.openig/config/routes/mtls-certificate.json
%appdata%\OpenIG\config\routes\mtls-certificate.json
{ "name": "mtls-certificate", "condition": "${find(request.uri.path, '/mtls-certificate')}", "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": "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:
-
The route matches requests to
/mtls-certificate
. -
The
OAuth2ResourceServerFilter
uses theConfirmationKeyVerifierAccessTokenResolver
to validate the certificate thumbprint against the thumbprint from the resolved access token provided by AM.The
ConfirmationKeyVerifierAccessTokenResolver
then delegates token resolution to theTokenIntrospectionAccessTokenResolver
. -
The
providerHandler
adds an authorization header to the request, containing the username and password of the OAuth 2.0 client with the scope to examine (introspect) access tokens. -
The
OAuth2ResourceServerFilter
checks the resolved token has the required scopes and injects the token info into the context. -
The
StaticResponseHandler
returns the content of the access token from the context.
-
-
Try certificate-based mTLS
-
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": 1724250516, "sub": "(age!client-application)", "iss": "http://am.example.com:8088/openam/oauth2", "subname": "client-application", "cnf": { "x5t#S256": "TTXH27YoFFCgOAQ0189KMBKeqxU1ZfZ_2nYGxrsjHlM" }, "authGrantId": "JqckJ_KhDLDeb4cKRkeBJcXZZUE", "auditTrackingId": "962fd5f6-fc2f-43c1-b044-ed1eb33d7aef-429" }
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 with the ConfirmationKeyVerifierAccessTokenResolver:
$ curl \ --request POST \ --cacert $ig_keystore_directory/ig.example.com-certificate.pem \ --cert $oauth2_client_keystore_directory/client.cert.pem \ --key $oauth2_client_keystore_directory/client.key.pem \ --header "Authorization: Bearer ${ACCESS_TOKEN}" \ https://ig.example.com:8443/mtls-certificate mTLS Valid token: <ACCESS_TOKEN> Confirmation keys: { ... }
The command displays the validated token and confirmation keys.