PingGateway 2024.6

OAuth 2.0

OAuth 2.0 includes the following entities:

  • Resource owner : A user who owns protected resources on a resource server. For example, a resource owner can store photos in a web service.

  • Resource server : A service that gives authorized client applications access to the resource owner’s protected resources. In OAuth 2.0, an Authorization Server grants authorization to a client application, based on the resource owner’s consent. For example, a resource server can be a web service that holds a user’s photos.

  • Client : An application that requests access to the resource owner’s protected resources, on behalf of the resource owner. For example, a client can be a photo printing service requesting access to a resource owner’s photos stored on a web service, after the resource owner gives the client consent to download the photos.

  • Authorization server : A service responsible for authenticating resource owners, and obtaining their consent to allow client applications to access their resources. For example, AM can act as the OAuth 2.0 Authorization Server to authenticate resource owners and obtain their consent. Other services, such as Google and Facebook can provide OAuth 2.0 authorization services.

PingGateway as an OAuth 2.0 client

PingGateway as an OAuth 2.0 client supports the OAuth 2.0 filters and flows in the following table:

Filter OAuth 2.0 flow Description

(previously named OAuth2ClientFilter)

This filter requires the user agent to authorize the request interactively to obtain an access token and optional ID token.

The access token is maintained only for the OAuth 2.0 session, and is valid only for the configured scopes.

This filter can act as an OpenID Connect relying party or as an OAuth 2.0 client. Use for Web applications running on a server.

According to information in the The OAuth 2.0 Authorization Framework, minimize use of this grant type and use other grant types when possible.

This filter supports the transformation of client credentials and user credentials to obtain an access token from the Authorization Server. It injects the access token into the inbound request as a Bearer Authorization header. The access token is valid only for the configured scopes.

Use for clients trusted with the resource owner credentials.

This filter is similar to the Resource Owner Password Credentials grant type, but the resource owner is not part of the flow and the client accesses only information relevant to itself.

Use when the client is the resource owner, or the client does not act on behalf of the resource owner.

PingGateway as an OAuth 2.0 resource server

The following image illustrates the steps for a client application to access a user’s protected resources, with AM as the Authorization Server and PingGateway as the resource server:

{projectName} as an OAuth 2.0 resource server handling OAuth 2.0 requests
Figure 1. PingGateway as an OAuth 2.0 resource server handling OAuth 2.0 requests
  • The application obtains an authorization grant, representing the resource owner’s consent. For information about the different OAuth 2.0 grant mechanisms supported by AM, refer to OAuth 2.0 grant flows in AM’s OAuth 2.0 guide.

  • The application authenticates to the Authorization Server and requests an access token. The Authorization Server returns an access token to the application.

    An OAuth 2.0 access token is an opaque string issued by the authorization server. When the client interacts with the resource server, the client presents the access token in the Authorization header. For example:

    Authorization: Bearer 7af...da9

    Access tokens are the credentials to access protected resources. The advantage of access tokens over passwords or other credentials is that access tokens can be granted and revoked without exposing the user’s credentials.

    The access token represents the authorization to access protected resources. Because an access token is a bearer token, anyone who has the access token can use it to get the resources. Access tokens must therefore be protected, so that requests involving them go over HTTPS.

    In OAuth 2.0, the token scopes are strings that identify the scope of access authorized to the client, but can also be used for other purposes.

  • The application supplies the access token to the resource server, which then resolves and validates the access token by using an access token resolver, as described in Access token resolvers.

    If the access token is valid, the resource server permits the client access to the requested resource.

The OAuth2ResourceServerFilter grants access to a resource by using an OAuth 2.0 access token from the HTTP Authorization header of a request.

When auditing is enabled, OAuth 2.0 token tracking IDs can be logged in access audit events for routes that contain an OAuth2ResourceServerFilter. For information, refer to Audit the deployment and Audit framework.

Validate stateful or stateless access tokens through the introspection endpoint

This section sets up PingGateway as an OAuth 2.0 resource server, using the introspection endpoint.

For more information about configuring AM as an OAuth 2.0 authorization service, refer to AM’s OAuth 2.0 guide.

This procedure uses the Resource Owner Password Credentials grant type. According to information in the The OAuth 2.0 Authorization Framework, minimize use of this grant type and utilize other grant types whenever possible.

Before you start, prepare AM, PingGateway, and the sample application as described in Example installation for this guide.

  1. Set up AM:

    1. Select Applications > Agents > Identity Gateway, and register a PingGateway agent with the following values:

      • 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. (Optional) Authenticate the agent to AM as described in Authenticate a PingGateway agent to AM.

      PingGateway agents are automatically authenticated to AM by a deprecated authentication module in AM. This step is currently optional, but will be required when authentication chains and modules are removed in a future release of AM.
    3. Create an OAuth 2.0 Authorization Server:

      1. Select Services > Add a Service > OAuth2 Provider.

      2. Add a service with the default values.

    4. Create an OAuth 2.0 Client to request OAuth 2.0 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): mail, employeenumber

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

        • Grant Types: Resource Owner Password Credentials

  2. Set up PingGateway

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

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

    3. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/rs-introspect.json
      %appdata%\OpenIG\config\routes\rs-introspect.json
      {
        "name": "rs-introspect",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/rs-introspect$')}",
        "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",
          "config": {
            "filters": [
              {
                "name": "OAuth2ResourceServerFilter-1",
                "type": "OAuth2ResourceServerFilter",
                "config": {
                  "scopes": [
                    "mail",
                    "employeenumber"
                  ],
                  "requireHttps": false,
                  "realm": "OpenIG",
                  "accessTokenResolver": {
                    "name": "TokenIntrospectionAccessTokenResolver-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": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
              }
            }
          }
        }
      }

      For information about how to set up the PingGateway route in Studio, see Token validation using the introspection endpoint in Structured Editor.

      Notice the following features of the route:

      • The route matches requests to /rs-introspect.

      • The OAuth2ResourceServerFilter expects an OAuth 2.0 access token in the authorization header of the incoming authorization request, with the scopes mail and employeenumber.

        The accessTokenResolver uses the AM server declared in the heap. The introspection endpoint to validate the access token is extrapolated from the URL of the AM server.

        For convenience in this test, requireHttps is false. In production environments, set it to true.

      • After the filter validates the access token, it creates a new context from the Authorization Server response. The context is named oauth2, and can be reached at contexts.oauth2 or contexts['oauth2'].

        The context contains information about the access token, which can be reached at contexts.oauth2.accessToken.info. Filters and handlers further down the chain can access the token info through the context.

        If there is no access token in the request, or token validation does not complete successfully, the filter returns an HTTP error status to the user agent, and PingGateway does not continue processing the request. This is done as specified in the RFC, The OAuth 2.0 Authorization Framework: Bearer Token Usage.

      • The HttpBasicAuthenticationClientFilter adds the credentials to the outgoing token introspection request.

      • The StaticResponseHandler returns the content of the access token from the context ${contexts.oauth2.accessToken.info}.

  3. Test the setup:

    1. In a terminal window, use a curl command similar to the following to retrieve an access token:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Validate the access token returned in the previous step:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-introspect
      
      {
        active = true,
        scope = employeenumber mail,
        realm=/,
        client_id = client-application,
        user_id = demo,
        token_type = Bearer,
        exp = 158...907,
        ...
      }

Define required scopes with a script

This example builds on the example in Validate access tokens through the introspection endpoint to use a script to define the scopes that a request requires in an access token.

  • If the request path is /rs-tokeninfo, the request requires only the scope mail.

  • If the request path is /rs-tokeninfo/employee, the request requires the scopes mail and employeenumber.

Before you start, set up and test the example in Validate access tokens through the introspection endpoint.

  1. Add the following route to PingGateway:

    • Linux

    • Windows

    $HOME/.openig/config/routes/rs-dynamicscope.json
    %appdata%\OpenIG\rs-dynamicscope.json
    {
      "name": "rs-dynamicscope",
      "baseURI": "http://app.example.com:8081",
      "condition": "${find(request.uri.path, '^/rs-dynamicscope')}",
      "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",
        "config": {
          "filters": [
            {
              "name": "OAuth2ResourceServerFilter-1",
              "type": "OAuth2ResourceServerFilter",
              "config": {
                "scopes": {
                  "name": "myscript",
                  "type": "ScriptableResourceAccess",
                  "config": {
                    "type": "application/x-groovy",
                    "source": [
                      "// Minimal set of required scopes",
                      "def scopes = [ 'mail' ] as Set",
                      "if (request.uri.path =~ /employee$/) {",
                      "  // Require another scope to access this resource",
                      "  scopes += 'employeenumber'",
                      "}",
                      "return scopes"
                    ]
                  }
                },
                "requireHttps": false,
                "realm": "OpenIG",
                "accessTokenResolver": {
                  "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": {
            "type": "StaticResponseHandler",
            "config": {
              "status": 200,
              "headers": {
                "Content-Type": [ "text/html; charset=UTF-8" ]
              },
              "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
            }
          }
        }
      }
    }
  2. Test the setup with the mail scope only:

    1. In a terminal, use a curl command to retrieve an access token with the scope mail:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Confirm that the access token is returned for the /rs-dynamicscope path:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-dynamicscope
      
      {
        active = true,
        scope = mail,
        client_id = client-application,
        user_id = demo,
        token_type = Bearer,
        exp = 158...907,
        sub = demo,
        iss = http://am.example.com:8088/openam/oauth2, ...
        ...
      }
    3. Confirm that the access token is not returned for the /rs-dynamicscope/employee path:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-dynamicscope/employee
  3. Test the setup with the scopes mail and employeenumber:

    1. In a terminal window, use a curl command similar to the following to retrieve an access token with the scopes mail and employeenumber:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Confirm that the access token is returned for the /rs-dynamicscope/employee path:

      $ curl -v
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}"
      https://ig.example.com:8443/rs-dynamicscope/employee

Validate stateless access tokens with the StatelessAccessTokenResolver

The StatelessAccessTokenResolver confirms that stateless access tokens provided by AM are well-formed, have a valid issuer, have the expected access token name, and have a valid signature.

After the StatelessAccessTokenResolver resolves an access token, the OAuth2ResourceServerFilter checks that the token is within the expiry time, and that it provides the required scopes. For more information, refer to StatelessAccessTokenResolver.

The following sections provide examples of how to validate signed and encrypted access tokens:

Validate signed access tokens with the StatelessAccessTokenResolver and JwkSetSecretStore

This section provides examples of how to validate signed access tokens with the StatelessAccessTokenResolver, using a JwkSetSecretStore. For more information about JwkSetSecretStore, refer to JwkSetSecretStore.

This procedure uses the Resource Owner Password Credentials grant type. According to information in the The OAuth 2.0 Authorization Framework, minimize use of this grant type and utilize other grant types whenever possible.
  1. Set up AM:

    1. Configure an OAuth 2.0 Authorization Provider:

      1. Select Services, and add an OAuth 2.0 Provider.

      2. Accept the default values and select Create. The service is added to the Services list.

      3. On the Core tab, select the following option:

        • Use Client-Based Access & Refresh Tokens : on

      4. On the Advanced tab, select the following options:

        • Client Registration Scope Allowlist : myscope

        • OAuth2 Token Signing Algorithm : RS256

        • Encrypt Client-Based Tokens : Deselected

    2. Create an OAuth2 Client to request OAuth 2.0 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) : myscope

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

        • Grant Types : Resource Owner Password Credentials

        • Response Types : code token

      3. On the Signing and Encryption tab, include the following setting:

        • ID Token Signing Algorithm : RS256

  2. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/rs-stateless-signed.json
      %appdata%\OpenIG\config\routes\rs-stateless-signed.json
      {
        "name": "rs-stateless-signed",
        "condition": "${find(request.uri.path, '/rs-stateless-signed')}",
        "heap": [
          {
            "name": "SecretsProvider-1",
            "type": "SecretsProvider",
            "config": {
              "stores": [
                {
                  "type": "JwkSetSecretStore",
                  "config": {
                    "jwkUrl": "http://am.example.com:8088/openam/oauth2/connect/jwk_uri"
                  }
                }
              ]
            }
          }
        ],
        "handler": {
          "type": "Chain",
          "capture": "all",
          "config": {
            "filters": [
              {
                "name": "OAuth2ResourceServerFilter-1",
                "type": "OAuth2ResourceServerFilter",
                "config": {
                  "scopes": ["myscope"],
                  "requireHttps": false,
                  "accessTokenResolver": {
                    "type": "StatelessAccessTokenResolver",
                    "config": {
                      "secretsProvider": "SecretsProvider-1",
                      "issuer": "http://am.example.com:8088/openam/oauth2",
                      "verificationSecretId": "any.value.in.regex.format"
                    }
                  }
                }
              }
            ],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /rs-stateless-signed.

      • A SecretsProvider in the heap declares a JwkSetSecretStore to manage secrets for signed access tokens.

      • The JwkSetSecretStore specifies the URL to a JWK set on AM, that contains the signing keys.

      • The OAuth2ResourceServerFilter expects an OAuth 2.0 access token in the header of the incoming authorization request, with the scope myscope.

      • The StatelessAccessTokenResolver uses the SecretsProvider to verify the signature of the provided access token.

      • After the OAuth2ResourceServerFilter validates the access token, it creates the OAuth2Context context. For more information, refer to OAuth2Context.

      • If there is no access token in a request, or token validation does not complete successfully, the filter returns an HTTP error status to the user agent, and PingGateway does not continue processing the request. This is done as specified in the RFC The OAuth 2.0 Authorization Framework: Bearer Token Usage.

      • The StaticResponseHandler returns the content of the access token from the context.

  3. Test the setup for a signed access token:

    1. Get an access token for the demo user, using the scope myscope:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=myscope" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Display the token:

      $ echo ${mytoken}

      Note that the token is structured as a signed token.

    3. Access the route by providing the token returned in the previous step:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-stateless-signed
      
      ...
          Decoded access_token: {
          sub=(usr!demo),
          cts=OAUTH2_STATELESS_GRANT,
          ...

Validate signed access tokens with the StatelessAccessTokenResolver and KeyStoreSecretStore

This section provides examples of how to validate signed access tokens with the StatelessAccessTokenResolver, using a KeyStoreSecretStore. For more information about KeyStoreSecretStore, refer to KeyStoreSecretStore.

Set up keys to sign access tokens
  1. Locate the following directories for keys, keystores, and certificates, and in a terminal create variables for them:

    • Directory where the keystore is created: keystore_directory

    • AM keystore directory: am_keystore_directory

    • PingGateway keystore directory: ig_keystore_directory

  2. Set up the keystore for signing keys:

    1. Generate a private key called signature-key, and a corresponding public certificate called x509certificate.pem:

      $ openssl req -x509 \
      -newkey rsa:2048 \
      -nodes \
      -subj "/CN=ig.example.com/OU=example/O=com/L=fr/ST=fr/C=fr" \
      -keyout $keystore_directory/signature-key.key \
      -out $keystore_directory/x509certificate.pem \
      -days 365
      
      ...
      writing new private key to '$keystore_directory/signature-key.key'
    2. Convert the private key and certificate files into a PKCS#12 file, called signature-key, and store them in a keystore named keystore.p12:

      $ openssl pkcs12 \
      -export \
      -in $keystore_directory/x509certificate.pem \
      -inkey $keystore_directory/signature-key.key \
      -out $keystore_directory/keystore.p12 \
      -passout pass:password \
      -name signature-key
    3. List the keys in keystore.p12:

      $ keytool -list \
      -v \
      -keystore "$keystore_directory/keystore.p12" \
      -storepass "password" \
      -storetype PKCS12
      
      ...
      Your keystore contains 1 entry
      Alias name: signature-key
  3. Set up keys for AM:

    1. Copy the signing key keystore.p12 to AM:

      $ cp $keystore_directory/keystore.p12 $am_keystore_directory/AM_keystore.p12
    2. List the keys in the AM keystore:

      $ keytool -list \
      -v \
      -keystore "$am_keystore_directory/AM_keystore.p12" \
      -storepass "password" \
      -storetype PKCS12
      
      ...
      Your keystore contains 1 entry
      Alias name: signature-key
    3. Add a file called keystore.pass, containing the store password password:

      $ cd $am_keystore_directory
      $ echo -n 'password' > keystore.pass
      Make sure the password file contains only the password, with no trailing spaces or carriage returns.

      The filename corresponds to the secret ID of the store password and entry password for the KeyStoreSecretStore.

    4. Restart AM.

  4. Set up keys for PingGateway:

    1. Import the public certificate to the IG keystore, with the alias verification-key:

      $ keytool -import \
      -trustcacerts \
      -rfc \
      -alias verification-key \
      -file "$keystore_directory/x509certificate.pem" \
      -keystore "$ig_keystore_directory/IG_keystore.p12" \
      -storetype PKCS12 \
      -storepass "password"
      
      ...
      Trust this certificate? [no]:  yes
      Certificate was added to keystore
    2. List the keys in the PingGateway keystore:

      $ keytool -list \
      -v \
      -keystore "$ig_keystore_directory/IG_keystore.p12" \
      -storepass "password" \
      -storetype PKCS12
      
      ...
      Your keystore contains 1 entry
      Alias name: verification-key
    3. In the PingGateway configuration, set an environment variable for the keystore password:

      $ export KEYSTORE_SECRET_ID='cGFzc3dvcmQ='
    4. Restart PingGateway.

Validate signed access tokens with the StatelessAccessTokenResolver and KeyStoreSecretStore
This procedure uses the Resource Owner Password Credentials grant type. According to information in the The OAuth 2.0 Authorization Framework, minimize use of this grant type and utilize other grant types whenever possible.
  1. Set up AM:

    1. Create a KeyStoreSecretStore to manage the new AM keystore:

      1. In AM, select Secret Stores, and then add a secret store with the following values:

        • Secret Store ID : keystoresecretstore

        • Store Type : Keystore

        • File : am_keystore_directory/AM_keystore.p12

        • Keystore type : PKCS12

        • Store password secret label : keystore.pass

        • Entry password secret label : keystore.pass

      2. Select the Mappings tab, and add a mapping with the following values:

        • Secret Label : am.services.oauth2.stateless.signing.RSA

        • Aliases : signature-key

          The mapping sets signature-key as the active alias to use for signature generation.

    2. Create a FileSystemSecretStore to manage secrets for the KeyStoreSecretStore:

      1. Select Secret Stores, and then create a secret store with the following configuration:

        • Secret Store ID : filesystemsecretstore

        • Store Type : File System Secret Volumes

        • Directory : am_keystore_directory

        • File format : Plain text

    3. Configure an OAuth 2.0 Authorization Provider:

      1. Select Services, and add an OAuth 2.0 Provider.

      2. Accept all of the default values, and select Create. The service is added to the Services list.

      3. On the Core tab, select the following option:

        • Use Client-Based Access & Refresh Tokens : on

      4. On the Advanced tab, select the following options:

        • Client Registration Scope Allowlist : myscope

        • OAuth2 Token Signing Algorithm : RS256

        • Encrypt Client-Based Tokens : Deselected

    4. Create an OAuth2 Client to request OAuth 2.0 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) : myscope

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

        • Grant Types : Resource Owner Password Credentials

        • Response Types : code token

      3. On the Signing and Encryption tab, include the following setting:

        • ID Token Signing Algorithm : RS256

  2. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway, replacing ig_keystore_directory:

      • Linux

      • Windows

      $HOME/.openig/config/routes/rs-stateless-signed-ksss.json
      %appdata%\OpenIG\config\routes\rs-stateless-signed-ksss.json
      {
        "name": "rs-stateless-signed-ksss",
        "condition" : "${find(request.uri.path, '/rs-stateless-signed-ksss')}",
        "heap": [
          {
            "name": "SystemAndEnvSecretStore-1",
            "type": "SystemAndEnvSecretStore"
          },
          {
            "name": "KeyStoreSecretStore-1",
            "type": "KeyStoreSecretStore",
            "config": {
              "file": "<ig_keystore_directory>/IG_keystore.p12",
              "storeType": "PKCS12",
              "storePasswordSecretId": "keystore.secret.id",
              "entryPasswordSecretId": "keystore.secret.id",
              "secretsProvider": "SystemAndEnvSecretStore-1",
              "mappings": [
                {
                  "secretId": "stateless.access.token.verification.key",
                  "aliases": [ "verification-key" ]
                }
              ]
            }
          }
        ],
        "handler" : {
          "type" : "Chain",
          "capture" : "all",
          "config" : {
            "filters" : [ {
              "name" : "OAuth2ResourceServerFilter-1",
              "type" : "OAuth2ResourceServerFilter",
              "config" : {
                "scopes" : [ "myscope" ],
                "requireHttps" : false,
                "accessTokenResolver": {
                  "type": "StatelessAccessTokenResolver",
                  "config": {
                    "secretsProvider": "KeyStoreSecretStore-1",
                    "issuer": "http://am.example.com:8088/openam/oauth2",
                    "verificationSecretId": "stateless.access.token.verification.key"
                  }
                }
              }
            } ],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /rs-stateless-signed-ksss.

      • The keystore password is provided by the SystemAndEnvSecretStore in the heap.

      • The OAuth2ResourceServerFilter expects an OAuth 2.0 access token in the header of the incoming authorization request, with the scope myscope.

      • The accessTokenResolver uses a StatelessAccessTokenResolver to resolve and verify the authenticity of the access token. The secret is provided by the KeyStoreSecretStore in the heap.

      • After the OAuth2ResourceServerFilter validates the access token, it creates the OAuth2Context context. For more information, refer to OAuth2Context.

      • If there is no access token in a request, or if the token validation does not complete successfully, the filter returns an HTTP error status to the user agent, and PingGateway stops processing the request, as specified in the RFC, The OAuth 2.0 Authorization Framework: Bearer Token Usage.

      • The StaticResponseHandler returns the content of the access token from the context.

  3. Test the setup for a signed access token:

    1. Get an access token for the demo user, using the scope myscope:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=myscope" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Display the token:

      $ echo ${mytoken}
    3. Access the route by providing the token returned in the previous step:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-stateless-signed-ksss
      
      ...
      Decoded access_token: {
      sub=(usr!demo),
      cts=OAUTH2_STATELESS_GRANT,
      ...

Validating encrypted access tokens with the StatelessAccessTokenResolver and KeyStoreSecretStore

Set up keys to encrypt access tokens
  1. Locate the following directories for keys, keystores, and certificates, and in a terminal create variables for them:

    • Directory where the keystore is created: keystore_directory

    • AM keystore directory: am_keystore_directory

    • PingGateway keystore directory: ig_keystore_directory

  2. Set up keys for AM:

    1. Generate the encryption key:

      $ keytool -genseckey \
      -alias encryption-key \
      -dname "CN=ig.example.com, OU=example, O=com, L=fr, ST=fr, C=fr" \
      -keystore "$am_keystore_directory/AM_keystore.p12" \
      -storetype PKCS12 \
      -storepass "password" \
      -keyalg AES \
      -keysize 256
    2. List the keys in the AM keystore:

      $ keytool -list \
      -v \
      -keystore "$am_keystore_directory/AM_keystore.p12" \
      -storepass "password" \
      -storetype PKCS12
      
      ...
      Your keystore contains 1 entry
      Alias name: encryption-key
    3. Add a file called keystore.pass, with the content password:

      $ cd $am_keystore_directory
      $ echo -n 'password' > keystore.pass
      Make sure the password file contains only the password, with no trailing spaces or carriage returns.

      The filename corresponds to the secret ID of the store password and entry password for the KeyStoreSecretStore.

    4. Restart AM.

  3. Set up keys for PingGateway:

    1. Import encryption-key into the PingGateway keystore, with the alias decryption-key:

      $ keytool -importkeystore \
      -srcalias encryption-key \
      -srckeystore "$am_keystore_directory/AM_keystore.p12" \
      -srcstoretype PKCS12 \
      -srcstorepass "password" \
      -destkeystore "$ig_keystore_directory/IG_keystore.p12" \
      -deststoretype PKCS12 \
      -destalias decryption-key \
      -deststorepass "password" \
      -destkeypass "password"
    2. List the keys in the PingGateway keystore:

      $ keytool -list \
      -v \
      -keystore "$ig_keystore_directory/IG_keystore.p12" \
      -storepass "password" \
      -storetype PKCS12
      
      ...
      Your keystore contains 1 entry
      Alias name: decryption-key
    3. In the PingGateway configuration, set an environment variable for the keystore password:

      $ export KEYSTORE_SECRET_ID='cGFzc3dvcmQ='
    4. Restart PingGateway.

Validate encrypted access tokens with the StatelessAccessTokenResolver and KeyStoreSecretStore
  1. Set up AM:

    1. Set up AM as described in Validate signed access tokens with the StatelessAccessTokenResolver and KeyStoreSecretStore.

    2. Add a mapping for the encryption keystore:

      1. Select Secret Stores > keystoresecretstore.

      2. Select the Mappings tab, and add a mapping with the following values:

        • Secret Label : am.services.oauth2.stateless.token.encryption

        • Alias : encryption-key

    3. Enable token encryption on the OAuth 2.0 Authorization Provider:

      1. Select Services > OAuth2 Provider.

      2. On the Advanced tab, select Encrypt Client-Side Tokens.

  2. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway, replacing ig_keystore_directory:

      • Linux

      • Windows

      $HOME/.openig/config/routes/rs-stateless-encrypted.json
      %appdata%\OpenIG\config\routes\rs-stateless-encrypted.json
      {
        "name": "rs-stateless-encrypted",
        "condition": "${find(request.uri.path, '/rs-stateless-encrypted')}",
        "heap": [
          {
            "name": "SystemAndEnvSecretStore-1",
            "type": "SystemAndEnvSecretStore"
          },
          {
            "name": "KeyStoreSecretStore-1",
            "type": "KeyStoreSecretStore",
            "config": {
              "file": "<ig_keystore_directory>/IG_keystore.p12",
              "storeType": "PKCS12",
              "storePasswordSecretId": "keystore.secret.id",
              "entryPasswordSecretId": "keystore.secret.id",
              "secretsProvider": "SystemAndEnvSecretStore-1",
              "mappings": [
                {
                  "secretId": "stateless.access.token.decryption.key",
                  "aliases": [ "decryption-key" ]
                }
              ]
            }
          }
        ],
        "handler": {
          "type": "Chain",
          "capture": "all",
          "config": {
            "filters": [ {
              "name": "OAuth2ResourceServerFilter-1",
              "type": "OAuth2ResourceServerFilter",
              "config": {
                "scopes": [ "myscope" ],
                "requireHttps": false,
                "accessTokenResolver": {
                  "type": "StatelessAccessTokenResolver",
                  "config": {
                    "secretsProvider": "KeyStoreSecretStore-1",
                    "issuer": "http://am.example.com:8088/openam/oauth2",
                    "decryptionSecretId": "stateless.access.token.decryption.key"
                  }
                }
              }
            } ],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
              }
            }
          }
        }
      }

      Notice the following features of the route compared to rs-stateless-signed.json, used in: Validate signed access tokens with the StatelessAccessTokenResolver and KeyStoreSecretStore:

      • The route matches requests to /rs-stateless-encrypted.

      • The OAuth2ResourceServerFilter and KeyStoreSecretStore refer to the configuration for a decryption key instead of a verification key.

  3. Test the setup

    1. Get an access token for the demo user, using the scope myscope:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=myscope" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Display the token:

      $ echo ${mytoken}

      Note that the token is structured as an encrypted token.

    3. Access the route by providing the token returned in the previous step:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-stateless-encrypted
      
      ...
      Decoded access_token: {
      sub=demo,
      cts=OAUTH2_STATELESS_GRANT,
      ...

Validate 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, refer to Mutual TLS in AM’s OAuth 2.0 guide.

When the client connects to PingGateway by using that certificate, PingGateway 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.

mTLS using standard TLS client certificate authentication

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. If TLS is terminated at a reverse proxy or load balancer before PingGateway, use the example in mTLS Using Trusted Headers.

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

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

  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 ig_keystore_directory, add a file called keystore.pass containing the keystore password:

      $ cd $ig_keystore_directory
      $ echo -n 'changeit' > keystore.pass
  3. Configure AM:

    1. Configure AM for TLS connections using information from Secure HTTP and LDAP connections in AM’s Security guide.

      Learn more about the setup for this example.
      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 mTLS using information from Mutual TLS in AM’s OAuth 2.0 guide.

      Learn more about the setup for this example.
      1. In a the AM admin UI, select Applications > Agents > Identity Gateway, and register a PingGateway agent with the following values:

        • 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. (Optional) Authenticate the agent to AM as described in Authenticate a PingGateway agent to AM.

        PingGateway agents are automatically authenticated to AM by a deprecated authentication module in AM. This step is currently optional, but will be required when authentication chains and modules are removed in a future release of AM.
      3. 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

      4. 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, set the following values:

          • mTLS Self-Signed Certificate: Enter the content of the X.509 certificate, client.cert.pem.

          • mTLS Subject DN: CN=test

          • Public key selector: x509

            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. Configure PingGateway for mTLS connections:

    1. Configure PingGateway using information from Configure PingGateway for mTLS (server-side).

      This example uses a self-signed certificate stored in a PEM file.

    2. Add a SecretsTrustManager to admin.json. This example uses the following file:

      • 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"
                }
              }]
            }
          }
        ]
      }
    3. Replace the values of the secret directories with your directories, and then start PingGateway.

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

  6. Test the setup

    1. Get an access token from AM, over mTLS:

      $ 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://am.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://am.example.com:8088/openam/oauth2/realms/root/introspect | jq
      
      {
        "active": true,
        "scope": "test",
        "realm": "/",
        "client_id": "client-application",
        "user_id": "client-application",
        "token_type": "Bearer",
        "exp": 155...833,
        "sub": "(age!client-application)",
        "subname": "client-application",
        "iss": "http://am.example.com:8088/openam/oauth2",
        "cnf": {
          "x51...156": "T4u...R9Q"
        },
        "authGrantId": "dfE...2vk",
        "auditTrackingId": "e36...524"
      }

      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 PingGateway route to validate the token’s confirmation thumbprint 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 ${mytoken}" \
      https://ig.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

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 Using Standard TLS Client Certificate Authentication.

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.

Use this example when the PingGateway instance is running behind a load balancer or other ingress point. If the PingGateway 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:

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

The client can provide its certificate to AM using either standard TLS client certificate authentication or trusted HTTP headers. This example passes the certificate in a trusted HTTP header. Learn more from Providing client certificates to AM in AM’s OAuth2.0 guide.

  1. Set up the keystores, truststores, AM, and PingGateway 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 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(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; 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 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 mTLS:

      $ 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://am.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://am.example.com:8088/openam/oauth2/realms/root/introspect | jq
      
      {
        "active": true,
        "scope": "test",
        "realm": "/",
        "client_id": "client-application",
        "user_id": "client-application",
        "token_type": "Bearer",
        "exp": 157...994,
        "sub": "(age!client-application)",
        "subname": "client-application",
        "iss": "http://am.example.com:8088/openam/oauth2",
        "cnf": {
          "x51...156": "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 PingGateway route to validate the confirmation key, replacing 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://ig.example.com:8080/mtls-header
      
      Valid token: zw5...Sj1
        Confirmation keys: {
        ...
        }

      The validated token and confirmation keys are displayed.

Use the OAuth 2.0 context to log in to the sample application

This section contains an example route that retrieves scopes from a token introspection, assigns them as the PingGateway session username and password, and uses them to log the user directly in to the sample application.

For information about the context, refer to OAuth2Context.

Before you start, set up and test the example in Validate access tokens through the introspection endpoint.

  1. Set up AM:

    1. Select Identities, and change the email address of the demo user to demo.

    2. Select Scripts > OAuth2 Access Token Modification Script, and replace the default script as follows:

      import org.forgerock.http.protocol.Request
      import org.forgerock.http.protocol.Response
      import com.iplanet.sso.SSOException
      import groovy.json.JsonSlurper
      
      def attributes = identity.getAttributes(["mail"].toSet())
      accessToken.setField("mail", attributes["mail"][0])
      accessToken.setField("password", "Ch4ng31t")

      The AM script adds user profile information to the access token, and adds a password field with the value Ch4ng31t.

      Don’t use this example in production. If the token is stateless and unencrypted, the password value is easily accessible when you have the token.
  2. Set up PingGateway:

    1. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/rs-pwreplay.json
      %appdata%\OpenIG\config\routes\rs-pwreplay.json
      {
        "name" : "rs-pwreplay",
        "baseURI" : "http://app.example.com:8081",
        "condition" : "${find(request.uri.path, '^/rs-pwreplay')}",
        "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",
          "config" : {
            "filters" : [
              {
                "name" : "OAuth2ResourceServerFilter-1",
                "type" : "OAuth2ResourceServerFilter",
                "config" : {
                  "scopes" : [ "mail", "employeenumber" ],
                  "requireHttps" : false,
                  "realm" : "OpenIG",
                  "accessTokenResolver": {
                    "name": "TokenIntrospectionAccessTokenResolver-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"
                        }
                      }
                    }
                  }
                }
              },
              {
                "type": "AssignmentFilter",
                "config": {
                  "onRequest": [{
                    "target": "${session.username}",
                    "value": "${contexts.oauth2.accessToken.info.mail}"
                  },
                    {
                      "target": "${session.password}",
                      "value": "${contexts.oauth2.accessToken.info.password}"
                    }
                  ]
                }
              },
              {
                "type": "StaticRequestFilter",
                "config": {
                  "method": "POST",
                  "uri": "http://app.example.com:8081/login",
                  "form": {
                    "username": [
                      "${session.username}"
                    ],
                    "password": [
                      "${session.password}"
                    ]
                  }
                }
              }
            ],
            "handler": "ReverseProxyHandler"
          }
        }
      }

      Notice the following features of the route compared to rs-introspect.json:

      • The route matches requests to /rs-pwreplay.

      • The AssignmentFilter accesses the context, and injects the username and password into the SessionContext, $[.labelSession].

      • The StaticRequestFilter retrieves the username and password from session, and replaces the original HTTP GET request with an HTTP POST login request that contains the credentials to authenticate.

  3. Test the setup:

    1. In a terminal window, use a curl command similar to the following to retrieve an access token:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Validate the access token returned in the previous step:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-pwreplay

      HTML for the sample application is displayed.

Cache access tokens

This section builds on the example in Validate access tokens through the introspection endpoint to cache and then revoke access tokens.

When the access token is not cached, PingGateway calls AM to validate the access token. When the access token is cached, PingGateway doesn’t validate the access token with AM.

When an access token is revoked on AM, the CacheAccessTokenResolver can delete the token from the cache when both of the following conditions are true:

  • The notification property of AmService is enabled.

  • The delegate AccessTokenResolver provides the token metadata required to update the cache.

When a refresh_token is revoked on AM, all associated access tokens are automatically and immediately revoked.

Before you start, set up and test the example in Validate access tokens through the introspection endpoint.

  1. Add the following route to PingGateway:

    • Linux

    • Windows

    $HOME/.openig/config/routes/rs-introspect-cache.json
    %appdata%\OpenIG\config\routes\rs-introspect-cache.json
    {
      "name": "rs-introspect-cache",
      "baseURI": "http://app.example.com:8081",
      "condition": "${find(request.uri.path, '^/rs-introspect-cache$')}",
      "heap": [
        {
          "name": "SystemAndEnvSecretStore-1",
          "type": "SystemAndEnvSecretStore"
        },
        {
          "name": "AmService-1",
          "type": "AmService",
          "config": {
            "url": "http://am.example.com:8088/openam",
            "realm": "/",
            "agent" : {
              "username" : "ig_agent",
              "passwordSecretId" : "agent.secret.id"
            },
            "secretsProvider": "SystemAndEnvSecretStore-1"
          }
        }
      ],
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [
            {
              "name": "OAuth2ResourceServerFilter-1",
              "type": "OAuth2ResourceServerFilter",
              "config": {
                "scopes": [
                  "mail",
                  "employeenumber"
                ],
                "requireHttps": false,
                "realm": "OpenIG",
                "accessTokenResolver": {
                  "name": "CacheAccessTokenResolver-1",
                  "type": "CacheAccessTokenResolver",
                  "config": {
                    "enabled": true,
                    "defaultTimeout ": "1 hour",
                    "maximumTimeToCache": "1 day",
                    "amService":"AmService-1",
                    "delegate": {
                      "name": "TokenIntrospectionAccessTokenResolver-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": {
                              "type": "Delegate",
                              "capture": "all",
                              "config": {
                                "delegate": "ForgeRockClientHandler"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          ],
          "handler": {
            "type": "StaticResponseHandler",
            "config": {
              "status": 200,
              "headers": {
                "Content-Type": [ "text/html; charset=UTF-8" ]
              },
              "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>"
            }
          }
        }
      }
    }

    Notice the following features of the route compared to rs-introspect.json, in Validate access tokens through the introspection endpoint:

    • The OAuth2ResourceServerFilter uses a CacheAccessTokenResolver to cache the access token, and then delegate token resolution to the TokenIntrospectionAccessTokenResolver.

    • The amService property in CacheAccessTokenResolver enables WebSocket notifications from AM, for events such as token revocation.

    • The TokenIntrospectionAccessTokenResolver uses a ForgeRockClientHandler and a capture decorator to capture PingGateway’s interactions with AM.

  2. Test token caching:

    1. In a terminal window, use a curl command similar to the following to retrieve an access token:

      $ mytoken=$(curl -s \
      --user "client-application:password" \
      --data "grant_type=password&username=demo&password=Ch4ng31t&scope=mail%20employeenumber" \
      http://am.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
    2. Access the route, using the access token returned in the previous step:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-introspect-cache
      
      {
       active = true,
       scope = employeenumber mail,
       client_id = client-application,
       user_id = demo,
       token_type = Bearer,
       exp = 158...907,
       ...
      }
    3. In the route log, note that PingGateway calls AM to introspect the access token:

      POST http://am.example.com:8088/openam/oauth2/realms/root/introspect HTTP/1.1
    4. Access the route again. In the route log note that this time PingGateway doesn’t call AM, because the token is cached.

    5. Disable the cache and repeat the previous steps to cause PingGateway to call AM to validate the access token for each request.

  3. Test token revocation:

    1. In a terminal window, use a curl command similar to the following to revoke the access token obtained in the previous step:

      $ curl --request POST \
      --data "token=${mytoken}" \
      --data "client_id=client-application" \
      --data "client_secret=password" \
      "http://am.example.com:8088/openam/oauth2/realms/root/token/revoke"
    2. Access the route using the access token and and note that the request isn’t authorized because the token is revoked:

      $ curl -v \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --header "Authorization: Bearer ${mytoken}" \
      https://ig.example.com:8443/rs-introspect-cache
      
      ...
      HTTP/1.1 401 Unauthorized

Use OAuth 2.0 client credentials

This example shows how a client service accesses an OAuth 2.0-protected resource by using its OAuth 2.0 client credentials.

ClientCredentialsOAuth2ClientFilter
  1. Set up the AM as an Authorization Server:

    1. Register a PingGateway agent with the following values, as described in Register a PingGateway agent in AM:

      • 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. (Optional) Authenticate the agent to AM as described in Authenticate a PingGateway agent to AM.

      PingGateway agents are automatically authenticated to AM by a deprecated authentication module in AM. This step is currently optional, but will be required when authentication chains and modules are removed in a future release of AM.
    3. Create an OAuth 2.0 Authorization Server:

      1. Select Services > Add a Service > OAuth2 Provider.

      2. Add a service with the default values.

    4. Create an OAuth 2.0 client to request access tokens, using client credentials for authentication:

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

        • Client ID : client-service

        • Client secret : password

        • Scope(s) : client-scope

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

        • Grant Types : Client Credentials

  2. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

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

    3. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/oauth2-protected-resource.json
      %appdata%\OpenIG\config\routes\oauth2-protected-resource.json
      {
        "name": "oauth2-protected-resource",
        "condition": "${find(request.uri.path, '^/oauth2-protected-resource')}",
        "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",
          "config": {
            "filters": [
              {
                "name": "OAuth2ResourceServerFilter-1",
                "type": "OAuth2ResourceServerFilter",
                "config": {
                  "scopes": [ "client-scope" ],
                  "requireHttps": false,
                  "realm": "OpenIG",
                  "accessTokenResolver": {
                    "name": "TokenIntrospectionAccessTokenResolver-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": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body><h2>Access Granted</h2></body></html>"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /oauth2-protected-resource.

      • The OAuth2ResourceServerFilter expects an OAuth 2.0 access token in the header of the incoming request, with the scope client-scope.

      • The filter uses a TokenIntrospectionAccessTokenResolver to resolve the access token. The introspect endpoint is protected with HTTP Basic Authentication, and the providerHandler uses an HttpBasicAuthenticationClientFilter to provide the resource server credentials.

      • For convenience in this test, "requireHttps" is false. In production environments, set it to true.

      • After the filter successfully validates the access token, it creates a new context from the Authorization Server response, containing information about the access token.

      • The StaticResponseHandler returns a message that access is granted.

    4. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/client-credentials.json
      %appdata%\OpenIG\config\routes\client-credentials.json
      {
        "name": "client-credentials",
        "baseURI": "http://ig.example.com:8080",
        "condition" : "${find(request.uri.path, '^/client-credentials')}",
        "heap" : [ {
          "name" : "clientSecretAccessTokenExchangeHandler",
          "type" : "Chain",
          "capture" : "all",
          "config" : {
            "filters" : [ {
              "type" : "ClientSecretBasicAuthenticationFilter",
              "config" : {
                "clientId" : "client-service",
                "clientSecretId" : "client.secret.id",
                "secretsProvider" : {
                  "type" : "Base64EncodedSecretStore",
                  "config" : {
                    "secrets" : {
                      "client.secret.id" : "cGFzc3dvcmQ="
                    }
                  }
                }
              }
            } ],
            "handler" : "ForgeRockClientHandler"
          }
        }, {
          "name" : "oauth2EnabledClientHandler",
          "type" : "Chain",
          "capture" : "all",
          "config" : {
            "filters" : [ {
              "type" : "ClientCredentialsOAuth2ClientFilter",
              "config" : {
                "tokenEndpoint" : "http://am.example.com:8088/openam/oauth2/access_token",
                "endpointHandler": "clientSecretAccessTokenExchangeHandler",
                "scopes" : [ "client-scope" ]
              }
            } ],
            "handler" : "ForgeRockClientHandler"
          }
        } ],
        "handler" : {
          "type" : "ScriptableHandler",
          "config" : {
            "type" : "application/x-groovy",
            "clientHandler" : "oauth2EnabledClientHandler",
            "source" : [ "request.uri.path = '/oauth2-protected-resource'", "return http.send(context, request);" ]
          }
        }
      }

      Note the following features of the route:

      • The route matches requests to /client-credentials.

      • The ScriptableHandler rewrites the request to target it to /oauth2-protected-resource, and then calls the HTTP client, that has been redefined to use the oauth2EnabledClientHandler.

      • The oauth2EnabledClientHandler calls the ClientCredentialsOAuth2ClientFilter to obtain an access token from AM.

      • The ClientCredentialsOAuth2ClientFilter calls the clientSecretAccessTokenExchangeHandler to exchange tokens on the authorization endpoint.

      • The clientSecretAccessTokenExchangeHandler calls a ClientSecretBasicAuthenticationFilter to authenticate the client through the HTTP basic access authentication scheme, and a ForgeRockClientHandler to propagate the request.

      • The route oauth2-protected-resource.json uses the AM introspection endpoint to resolve the access token and display its contents.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to to https://ig.example.com:8443/client-credentials.

    2. If you see warnings that the site isn’t secure, respond to the warnings to access the site.

      A message shows that access is granted.

Use OAuth 2.0 resource owner password credentials

This example shows how a client service accesses an OAuth 2.0-protected resource by using resource owner password credentials.

ResourceOwnerOAuth2ClientFilter
This procedure uses the Resource Owner Password Credentials grant type. According to information in the The OAuth 2.0 Authorization Framework, minimize use of this grant type and utilize other grant types whenever possible.
  1. Set up the AM as an Authorization Server:

    1. Register a PingGateway agent with the following values, as described in Register a PingGateway agent in AM:

      • 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. (Optional) Authenticate the agent to AM as described in Authenticate a PingGateway agent to AM.

      PingGateway agents are automatically authenticated to AM by a deprecated authentication module in AM. This step is currently optional, but will be required when authentication chains and modules are removed in a future release of AM.
    3. Create an OAuth 2.0 Authorization Server:

      1. Select Services > Add a Service > OAuth2 Provider.

      2. Add a service with the default values.

    4. Create an OAuth 2.0 client to request access tokens, using the resource owner’s password for authentication:

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

        • Client ID : resource-owner-client

        • Client secret : password

        • Scope(s) : client-scope

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

        • Grant Types : Resource Owner Password Credentials

  2. Set up PingGateway:

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

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

    3. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/oauth2-protected-resource.json
      %appdata%\OpenIG\config\routes\oauth2-protected-resource.json
      {
        "name": "oauth2-protected-resource",
        "condition": "${find(request.uri.path, '^/oauth2-protected-resource')}",
        "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",
          "config": {
            "filters": [
              {
                "name": "OAuth2ResourceServerFilter-1",
                "type": "OAuth2ResourceServerFilter",
                "config": {
                  "scopes": [ "client-scope" ],
                  "requireHttps": false,
                  "realm": "OpenIG",
                  "accessTokenResolver": {
                    "name": "TokenIntrospectionAccessTokenResolver-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": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/html; charset=UTF-8" ]
                },
                "entity": "<html><body><h2>Access Granted</h2></body></html>"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /oauth2-protected-resource.

      • The OAuth2ResourceServerFilter expects an OAuth 2.0 access token in the header of the incoming request, with the scope client-scope.

      • The filter uses a TokenIntrospectionAccessTokenResolver to resolve the access token. The introspect endpoint is protected with HTTP Basic Authentication, and the providerHandler uses an HttpBasicAuthenticationClientFilter to provide the resource server credentials.

      • For convenience in this test, "requireHttps" is false. In production environments, set it to true.

      • After the filter successfully validates the access token, it creates a new context from the Authorization Server response, containing information about the access token.

      • The StaticResponseHandler returns a message that access is granted.

    4. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/resource-owner.json
      %appdata%\OpenIG\config\routes\resource-owner.json
      {
        "name": "resource-owner",
        "baseURI": "http://ig.example.com:8080",
        "condition" : "${find(request.uri.path, '^/resource-owner')}",
        "heap" : [ {
          "name" : "clientSecretAccessTokenExchangeHandler",
          "type" : "Chain",
          "capture" : "all",
          "config" : {
            "filters" : [ {
              "type" : "ClientSecretBasicAuthenticationFilter",
              "config" : {
                "clientId" : "resource-owner-client",
                "clientSecretId" : "client.secret.id",
                "secretsProvider" : {
                  "type" : "Base64EncodedSecretStore",
                  "config" : {
                    "secrets" : {
                      "client.secret.id" : "cGFzc3dvcmQ="
                    }
                  }
                }
              }
            } ],
            "handler" : "ForgeRockClientHandler"
          }
        }, {
          "name" : "oauth2EnabledClientHandler",
          "type" : "Chain",
          "capture" : "all",
          "config" : {
            "filters" : [ {
              "type" : "ResourceOwnerOAuth2ClientFilter",
              "config" : {
                "tokenEndpoint" : "http://am.example.com:8088/openam/oauth2/access_token",
                "endpointHandler": "clientSecretAccessTokenExchangeHandler",
                "scopes" : [ "client-scope" ],
                "username" : "demo",
                "passwordSecretId" : "user.password.secret.id",
                "secretsProvider" : {
                  "type" : "Base64EncodedSecretStore",
                  "config" : {
                    "secrets" : {
                      "user.password.secret.id" : "Q2g0bmczMXQ="
                    }
                  }
                }
              }
            } ],
            "handler" : "ForgeRockClientHandler"
          }
        } ],
        "handler" : {
          "type" : "ScriptableHandler",
          "config" : {
            "type" : "application/x-groovy",
            "clientHandler" : "oauth2EnabledClientHandler",
            "source" : [ "request.uri.path = '/oauth2-protected-resource'", "return http.send(context, request);" ]
          }
        }
      }

      Note the following features of the route:

      • The route matches requests to /resource-owner.

      • The ScriptableHandler rewrites the request to target it to /oauth2-protected-resource, and then calls the HTTP client, that has been redefined to use the oauth2EnabledClientHandler.

      • The oauth2EnabledClientHandler calls the ResourceOwnerOAuth2ClientFilter to obtain an access token from AM.

      • The ResourceOwnerOAuth2ClientFilter calls the clientSecretAccessTokenExchangeHandler to exchange tokens on the authorization endpoint. The demo user authenticates with their username and password.

      • The clientSecretAccessTokenExchangeHandler calls a ClientSecretBasicAuthenticationFilter to authenticate the client through the HTTP basic access authentication scheme, and a ForgeRockClientHandler to propagate the request.

      • The route oauth2-protected-resource.json uses the AM introspection endpoint to resolve the access token and display its contents.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to to https://ig.example.com:8443/resource-owner.

    2. If you see warnings that the site isn’t secure, respond to the warnings to access the site.

      A message shows that access is granted.