---
title: mTLS with PingAM and trusted headers
description: PingGateway can validate the thumbprint of certificate-bound access tokens by reading the client certificate from a configured, trusted HTTP header.
component: pinggateway
version: 2026
page_id: pinggateway:gateway-guide:oauth2-rs-introspect-mtls-header
canonical_url: https://docs.pingidentity.com/pinggateway/2026/gateway-guide/oauth2-rs-introspect-mtls-header.html
revdate: 2025-04-01T17:53:34Z
section_ids:
  before_you_start: Before you start
  make_pinggateway_an_rs: Make PingGateway an RS
  try_mtls_with_trusted_headers: Try mTLS with trusted headers
---

# mTLS with PingAM and 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 PingAM using client certificates](oauth2-rs-introspect-mtls-certificate.html).

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.](_images/ig-mtls-header.png)![mtls-header-flow](https://kroki.io/plantuml/svg/eNqNU8lu2zAQvfMrBrmkBbI1hxYo4AJKgKYBssHyoQdfaGlkEZU46pCM4_5Rf6Nf1qHoTamT9CRDftvMPKnTQwWX1C3ZzGsPf37D-dmHT3Asj_OP8GDsHK5LtN74pcC4I9bekFUKJrVxUFCJIE9PMEMIDkvAp6IJzjxiswRjBWEtFpEDC-PrVyTBUeUXmhGIwSE_mgLdiXqZAWTFg6oK2YELRb1XIWZrcK4b6BUMOgWLmqDWjxhfIUtoYwWlYWZsGd0aIVqHoOeM2Arojewn6vBUKR082dDOkFWn2ZvCdFqoB5eNEcoBaAfp5_DvLPia2PxKM-USG3lqo1d225OyfEi4IV3ChW60LSIyDjtGIUngB6anZU-6u7q--z7kjdFR4AIHHlfa40InzjhXajSC-5nXcrmskO05mNAPtDAaqZT9-EuWf4Z3k5v8vbj-DOg86IT0EamyPEEuZJfga4Si54Fk9aYyhfhNra9lTx3L2uN5IuofjWS3tfKB7RAVs65CatgMt5O038FaIUfJw6vE_TkHYj12Q8k9m67P5Tk4Lw2pUZfIUDG1MT6upY7iAB3L_mWWiniOvHwmNhbqni30WtFjao0tqI3lkqQ7X8wznawsZVCr220c8V7nmIqGjYeLOvv3vtYby3m-EsuXEpu_st7Ms5WZ2t0Vgbb7xlBqnCfJV-dMsb9NJg9Tm8IfJUFquyAg48Vm04o3NWcUYr1oFW22hOz2KB6GKczr-D0zuS6tMRlVsY8vuFySrQzHY2ifrr6gHZyDVvui3oV7YwOC86Ict3gfP2I4PzmDqqFFD1wfrZEX26pJVchLLIxdTI1167MMG_8_xL8tlAMO?id=figure-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 PingAM using client certificates](oauth2-rs-introspect-mtls-certificate.html).

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

     `$HOME/.openig/config/routes/mtls-header.json`

   * Windows

     `%appdata%\OpenIG\config\routes\mtls-header.json`

   ```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}"
           }
         }
       }
     }
   }
   ```

   Source: [mtls-header.json](../_attachments/config/routes/mtls-header.json)

   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. Find additional examples in the [CertificateThumbprintFilter reference](../reference/CertificateThumbprintFilter.html#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:

   ```console
   $ 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:

   ```console
   $ 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
   ```

   Output

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

   ```console
   $ 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
   ```

   Output

   ```
   mTLS
    Valid token: UnUxGRuwXx_ugUCvNKFM3GJo3Cc
    Confirmation keys: { ... }
   ```

   The command displays the validated token and confirmation keys.
