PingGateway 2024.9

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.

This image illustrates the connections when PingGateway reads certificates from the TLS connection to validate certificate-bound access tokens.
mtls-certificate-flow

Follow the steps in this example to try mTLS using standard TLS client certificate authentication.

Prepare the keys

  1. 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
  2. Create keys and certificates for the example:

    1. 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
    2. 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
    3. 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
    4. 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
    5. 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

  1. Configure AM for HTTPS connections using information in the AM documentation about Secure HTTP and LDAP connections.

    Learn more
    1. Add a connector configuration for port 8445 to AM’s Tomcat server.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>
    2. In AM, export an environment variable for the base64-encoded value of the password (changeit) for the cacerts.p12 truststore:

      $ export PASSWORDSECRETID='Y2hhbmdlaXQ='
    3. Restart AM, and make sure you can access it on the secure port https://am.example.com:8445/openam.

  2. Configure AM for mutual TLS using information in the AM documentation about Mutual TLS.

    Learn more
    1. 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.
    2. Configure an OAuth 2.0 Authorization Server with settings for mTLS:

      1. Select Services > Add a Service > OAuth2 Provider, and add a service with the default values.

      2. On the Advanced tab, select the following value:

        • Support TLS Certificate-Bound Access Tokens: enabled

    3. Configure an OAuth 2.0 client to request access tokens:

      1. Select Applications > OAuth 2.0 > Clients, and add a client with the following values:

        • Client ID: client-application

        • Client secret: password

        • Scope(s): test

      2. (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.

      3. 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

      4. 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

  1. 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.

  2. 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.

  3. Replace the values of the secret directories with your directories, and then start PingGateway.

Make PingGateway an RS

  1. Configure PingGateway as a resource server for mTLS:

    1. 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.

    2. 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 the ConfirmationKeyVerifierAccessTokenResolver to validate the certificate thumbprint against the thumbprint from the resolved access token provided by AM.

        The ConfirmationKeyVerifierAccessTokenResolver then delegates token resolution to the TokenIntrospectionAccessTokenResolver.

      • 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

  1. 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.

  2. 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

  3. 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.