PingGateway 2024.11

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:

This image illustrates the connections when PingGateway validates certificate-bound access tokens by reading certificates from HTTP headers.
mtls-header-flow

Follow the steps in this example to try mTLS using trusted headers.

Before you start

  1. Set up the keystores, truststores, AM, and PingGateway as described in mTLS with client certificates.

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

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

  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": 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

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