Validating Certificate-Bound Access Tokens

Clients can authenticate to AM through mutual TLS (mTLS) and X.509 certificates. Certificates must be self-signed or use public key infrastructure (PKI), as described in version 12 of the draft OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens.

When a client requests an access_token from AM through mTLS, AM can use a confirmation key to bind the access_token to the presented client certificate. The confirmation key is the certificate thumbprint, computed as base64url-encode(sha256(der(certificate))). The access_token is then certificate-bound. For more information, see Authenticating Clients Using Mutual TLS in AM's OAuth 2.0 Guide.

When the client connects to IG by using that certificate, IG can verify that the confirmation key corresponds to the presented certificate. This proof-of-possession interaction ensures that only the client in possession of the key corresponding to the certificate can use the access_token to access protected resources.

The following sections provide examples of how to validate certificate-bound access_tokens:

mTLS Using Standard TLS Client Certificate Authentication

IG can validate the thumbprint of certificate-bound access_tokens by reading the client certificate from the TLS connection. When the web container that is running IG performs a successful TLS connection handshake, the connected client is trusted.

For this example, the client must be connected directly to IG through a TLS connection, for which IG is the TLS termination point. If TLS is terminated at a reverse proxy or load balancer before IG, use the example in "mTLS Using Trusted Headers".

The following images illustrate the example:

Connections for mTLS Using Standard TLS Client Certificate Authentication
Connections for mTLS Using Standard TLS Client Certificate Authentication

Data Flow for mTLS Using Standard TLS Client Certificate Authentication
Data Flow for mTLS Using Standard TLS Client Certificate Authentication

Perform the procedures in this section to set up and test mTLS using standard TLS client certificate authentication:

Set Up Keystores and Truststores
  1. Locate the following keystore directories, and in a terminal create variables for them:

    • oauth2_client_keystore_directory

    • am_keystore_directory

    • ig_keystore_directory

  2. Create self-signed RSA key pairs for AM, IG, 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=openam.example.com,O=Example,C=FR
    $ keytool -genkeypair \
    -alias openig-server \
    -keyalg RSA \
    -keysize 2048 \
    -keystore $ig_keystore_directory/keystore.p12 \
    -storepass changeit \
    -storetype PKCS12 \
    -keypass changeit \
    -validity 360 \
    -dname CN=openig.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
  3. Export the certificates to .pem so that the curl client can verify the identity of the AM and IG 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
    $ keytool -export \
    -rfc \
    -alias openig-server \
    -keystore $ig_keystore_directory/keystore.p12 \
    -storepass changeit \
    -storetype PKCS12 \
    -file $ig_keystore_directory/openig-server.cert.pem
    
    Certificate stored in file openig-server.cert.pem
  4. 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

    You can now delete the client keystore.

  5. 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
Set Up AM for HTTPS (Server-Side) in Tomcat

This procedure sets up AM for HTTPS in Tomcat. For more information, see Configuring AM's Container for HTTPS in AM's Installation Guide.

  1. Add the following connector configuration to AM's Tomcat server.xml, replacing the values for the keystore directories with your paths:

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

    The optionalNoCA property allows the presentation of client certificates to be optional. Tomcat does not check them against the list of trusted CAs.

  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 that you can access it on the secure port https://openam.example.com:8445/openam.

Set Up IG for HTTPS (Server-Side) in Tomcat

This procedure sets up IG for HTTPS in Tomcat. For other container types, see "Configuring IG for HTTPS (Server-Side) in Jetty" and "Configuring IG for HTTPS (Server-Side) in JBoss EAP".

If IG is installed in standalone mode, follow "Set Up IG for HTTPS (Server-Side) in Standalone Mode" instead.

  1. Add the following connector configuration to IG's Tomcat server.xml, replacing the values for the keystore directories with your paths:

    <Connector port="8443" 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="ig_keystore_directory/keystore.p12"
                     certificateKeystorePassword="changeit"
                     certificateKeystoreType="PKCS12" />
      </SSLHostConfig>
    </Connector>

    The optionalNoCA property allows the presentation of client certificates to be optional. Tomcat does not check them against the list of trusted CAs.

  2. Restart IG, and make sure that you can access the welcome page on the secure port https://openig.example.com:8443.

Set Up IG for HTTPS (Server-Side) in Standalone Mode

This procedure sets up IG for HTTPS in standalone mode. Before you start, install IG in standalone mode, as described in "Downloading and Starting IG in Standalone Mode".

When IG is installed in web container mode, follow "Set Up IG for HTTPS (Server-Side) in Tomcat" instead.

  1. In ig_keystore_directory, add a file called keystore.pass containing the keystore password:

    $ cd $ig_keystore_directory
    $ echo -n changeit > keystore.pass
  2. Add the following route to IG, replacing instances of ig_keystore_directory and oauth2_client_keystore_directory with your path:

    $HOME/.openig/config/admin.json
    %appdata%\OpenIG\config\admin.json
    {
      "mode": "DEVELOPMENT",
      "connectors": [
        {
          "port": 8080
        },
        {
          "port": 8443,
          "tls": {
            "type": "ServerTlsOptions",
            "config": {
              "alpn": {
                "enabled": true
              },
              "clientAuth": "REQUEST",
              "keyManager": {
                "type": "SecretsKeyManager",
                "config": {
                  "signingSecretId": "key.manager.secret.id",
                  "secretsProvider": {
                    "type": "KeyStoreSecretStore",
                    "config": {
                      "file": "<ig_keystore_directory>/keystore.p12",
                      "storePassword": "keystore.pass",
                      "secretsProvider": "SecretsPasswords",
                      "mappings": [
                        {
                          "secretId": "key.manager.secret.id",
                          "aliases": [
                            "openig-server"
                          ]
                        }
                      ]
                    }
                  }
                }
              },
              "trustManager": {
                "type": "SecretsTrustManager",
                "config": {
                  "verificationSecretId": "trust.manager.secret.id",
                  "secretsProvider": {
                    "type": "KeyStoreSecretStore",
                    "config": {
                      "file": "<oauth2_client_keystore_directory>/cacerts.p12",
                      "storePassword": "keystore.pass",
                      "secretsProvider": "SecretsPasswords",
                      "mappings": [
                        {
                          "secretId": "trust.manager.secret.id",
                          "aliases": [
                            "client-cert"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      ],
      "heap": [
        {
          "name": "SecretsPasswords",
          "type": "FileSystemSecretStore",
          "config": {
            "directory": "<ig_keystore_directory>",
            "format": "PLAIN"
          }
        }
      ]
    }

    Notice the following features of the route:

    • IG starts on port 8080, and on 8443 over TLS.

    • IG's private keys for TLS are managed by the SecretsKeyManager, which references the KeyStoreSecretStore that holds the keys.

    • The password of the KeyStoreSecretStore is provided by the FileSystemSecretStore.

    • The KeyStoreSecretStore maps the keystore alias to the secret ID for retrieving the private signing keys.

  3. Start IG:

    $ /path/to/identity-gateway/bin/start.sh
    ...
    ... started in 1234ms on ports : [8080 8443]
Set Up AM As an Authorization Server With mTLS

Before you start, install and configure AM on http://openam.example.com:8088/openam, with the default configuration. If you use a different configuration, substitute in the tutorial accordingly.

  1. Select Applications > Agents > Identity Gateway, add an agent with the following values:

    • Agent ID: ig_agent

    • Password: password

    • Token Introspection: Realm Only

  2. Configure an OAuth 2.0 Authorization Server:

    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. 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: tls_client_auth

    3. On the signing and Encryption tab, select the following values:

      • 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 any valid certificate trusted by the trust manager.

      • Use Certificate-Bound Access Tokens: Enabled

  4. Set up AM secret stores to trust the client certificate:

    1. Select  Secret Stores, and add a store with the following values:

      • Secret Store ID: trusted-ca-certs

      • Store Type: Keystore

      • File: $oauth2_client_keystore_directory/cacerts.p12

      • Keystore type: PKCS12

      • Store password secret ID: passwordSecretId

    2. Select Mappings and add the following mapping:

      • Secret ID: am.services.oauth2.tls.client.cert.authentication

      • Aliases: client-cert

      When the token endpoint authentication method is tls_client_auth, this secret is used to validate the client certificate. Add an alias in this list for each client that uses tls_client_auth. For certificates signed by a CA, add the CA certificate to the list.

Set Up IG As a Resource Server With mTLS
  1. Set an environment variable for the IG agent password, and then restart IG:

    $ export AGENT_SECRET_ID='cGFzc3dvcmQ='

    The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.

  2. Add the following route to IG:

    $HOME/.openig/config/routes/mtls-certificate.json
    %appdata%\OpenIG\config\routes\mtls-certificate.json
    {
      "name": "mtls-certificate",
      "condition": "${matches(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://openam.example.com:8088/openam/",
            "version": "7"
          }
        }
      ],
      "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" ]
              },
              "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 that 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.

Test the Setup
  1. Get an access_token from AM, over TLS:

    $ mytoken=$(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&grant_type=client_credentials&scope=test' \
    https://openam.example.com:8445/openam/oauth2/access_token | jq -r .access_token)
  2. Introspect the access_token on AM:

    $ curl --request POST \
    -u ig_agent:password \
    --header 'content-type: application/x-www-form-urlencoded' \
    --data token=${mytoken} \
    http://openam.example.com:8088/openam/oauth2/realms/root/introspect | jq
    
    {
      "active": true,
      "scope": "test",
      "client_id": "client-application",
      "user_id": "client-application",
      "token_type": "Bearer",
      "exp": 1550590833,
      "sub": "client-application",
      "iss": "http://openam.example.com:8088/openam/oauth2",
      "cnf": {
        "x5t#S256": "T4u...R9Q"
      }
    }

    The cnf property indicates the value of the confirmation code, as follows:

    • x5: X509 certificate

    • t: thumbprint

    • #: separator

    • S256: algorithm used to hash the raw certificate bytes

  3. Access the IG route to validate the token's confirmation thumbprint with the ConfirmationKeyVerifierAccessTokenResolver:

    $ curl --request POST \
    --cacert $ig_keystore_directory/openig-server.cert.pem \
    --cert $oauth2_client_keystore_directory/client.cert.pem \
    --key $oauth2_client_keystore_directory/client.key.pem \
    --header "authorization: Bearer ${mytoken}" \
    https://openig.example.com:8443/mtls-certificate
    
    mTLS
      Valid token: 2Bp...s_k
      Confirmation keys: {
      ...
      }

    The validated token and confirmation keys are displayed.

mTLS Using Trusted Headers

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

  • If the connection is not over TLS, the connection presents no client certificate.

If the client is connected directly to IG through a TLS connection, for which IG is the TLS termination point, use the example in "mTLS Using Standard TLS Client Certificate Authentication".

Configure the proxy or load balancer to:

  • Forward the encoded certificate to IG in the trusted header.

    Encode the certificate in an HTTP-header compatible format that can convey a full certificate, so that IG 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 IG and the TLS termination point, IG doesn't authenticate the contents of the trusted header. IG accepts any value in a header from a trusted TLS termination point.

Use this example when the IG instance is running behind a load balancer or other ingress point. If the IG instance is running behind the TLS termination point, consider the example in "mTLS Using Standard TLS Client Certificate Authentication".

The following image illustrates the connections and certificates required by the example:

Connections for mTLS Using Trusted Headers
Connections for mTLS Using Trusted Headers

Data Flow for mTLS Using Trusted Headers
Data Flow for mTLS Using Trusted Headers

Set Up mTLS Using Trusted Headers
  1. Set up the keystores, truststores, AM, and IG as described in "mTLS Using Standard TLS Client Certificate Authentication".

  2. Base64-encode the value of $oauth2_client_keystore_directory/client.cert.pem. The value is used in the final POST.

  3. Add the following route to IG:

    $HOME/.openig/config/routes/mtls-header.json
    %appdata%\OpenIG\config\routes\mtls-header.json
    {
      "name": "mtls-header",
      "condition": "${matches(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://openam.example.com:8088/openam/",
            "version": "7"
          }
        }
      ],
      "handler": {
        "type": "Chain",
        "capture": "all",
        "config": {
          "filters": [
            {
              "name": "CertificateThumbprintFilter-1",
              "type": "CertificateThumbprintFilter",
              "config": {
                "certificate": "${pemCertificate(decodeBase64(request.headers['ssl_client_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" ]
              },
              "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 a Java certificate from the trusted header, computes the SHA-256 thumbprint of that certificate, and makes the thumbprint available for the ConfirmationKeyVerifierAccessTokenResolver.

  4. Test the setup:

    1. Get an access_token from AM, over TLS:

      $ mytoken=$(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&grant_type=client_credentials&scope=test' \
      https://openam.example.com:8445/openam/oauth2/access_token | jq -r .access_token)
    2. Introspect the access_token on AM:

      $ curl --request POST \
      -u ig_agent:password \
      --header 'content-type: application/x-www-form-urlencoded' \
      --data token=${mytoken} \
      http://openam.example.com:8088/openam/oauth2/realms/root/introspect | jq
      
      {
        "active": true,
        "scope": "test",
        "client_id": "client-application",
        "user_id": "client-application",
        "token_type": "Bearer",
        "exp": 157...994,
        "sub": "client-application",
        "iss": "http://openam.example.com:8088/openam/oauth2",
        "cnf": {
          "x5t#S256": "1QG...Wgc"
        },
        "authGrantId": "lto...8vw",
        "auditTrackingId": "119...480"
      }

      The cnf property indicates the value of the confirmation code, as follows:

      • x5: X509 certificate

      • t: thumbprint

      • #: separator

      • S256: algorithm used to hash the raw certificate bytes

    3. Access the IG route to validate the confirmation key, using the base64-encoded value of $oauth2_client_keystore_directory/client.cert.pem:

      $ curl --request POST \
      --header "authorization:Bearer $mytoken" \
      --header 'ssl_client_cert:base64-encoded-cert'
      http://openig.example.com:8080/mtls-header
      
      Valid token: zw5...Sj1
        Confirmation keys: {
        ...
        }

      The validated token and confirmation keys are displayed.

Read a different version of :