PingGateway 2024.9

OAuth 2.0 token exchange

This page describes how to exchange an OAuth 2.0 access token for another access token with AM as an Authorization Server. Other authorization providers can be used instead of AM.

Token exchange requires a subject token and provides an issued token. The subject token is the original access token, obtained using the OAuth 2.0/OpenID Connect flow. The issued token is provided in exchange for the subject token.

The token exchange can be conducted only by an OAuth 2.0 client that "may act" on the subject token, as configured in the authorization service.

This example is a typical scenario for token impersonation. For more information, refer to Token exchange in the AM documentation.

The following sequence diagram shows the flow of information during token exchange between PingGateway and AM:

sso
This procedure uses the Resource Owner Password Credentials grant type. As suggested in The OAuth 2.0 Authorization Framework, use 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 Services > Add a Service and add a Validation Service with the following Valid goto URL Resources:

      • https://ig.example.com:8443/*

      • https://ig.example.com:8443/*?*

    2. 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.
    3. (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.
    4. Select Services > Add a Service, and add an OAuth2 Provider service with the following values:

      • OAuth2 Access Token May Act Script : OAuth2 May Act Script

      • OAuth2 ID Token May Act Script : OAuth2 May Act Script

    5. Select Scripts# > OAuth2 May Act Script, and replace the example script with the following script:

      import org.forgerock.json.JsonValue
      token.setMayAct(
          JsonValue.json(JsonValue.object(
              JsonValue.field("client_id", "serviceConfidentialClient"))))

      This script adds a may_act claim to the token, indicating that the OAuth 2.0 client, serviceConfidentialClient, may act to exchange the subject token in the impersonation use case.

    6. Add 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 Grant Types : Resource Owner Password Credentials.

    7. Add an OAuth 2.0 client to perform the token exchange:

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

        • Client ID : serviceConfidentialClient

        • Client secret : password

        • Scope(s) : mail, employeenumber

      2. On the Advanced tab, select:

        • Grant Types : Token Exchange

        • Token Endpoint Authentication Methods : client_secret_post

  2. Set up PingGateway:

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

    2. Set the following environment variables for the serviceConfidentialClient password:

      $ export CLIENT_SECRET_ID='cGFzc3dvcmQ='
    3. 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.

    4. Add the following route to PingGateway to exchange the access token:

      • Linux

      • Windows

      $HOME/.openig/config/routes/token-exchange.json
      %appdata%\OpenIG\config\routes\token-exchange.json
      {
        "name": "token-exchange",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/token-exchange')}",
        "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/"
            }
          },
          {
            "name": "ExchangeHandler",
            "type": "Chain",
            "capture": "all",
            "config": {
              "handler": "ForgeRockClientHandler",
              "filters": [
                {
                  "type": "ClientSecretBasicAuthenticationFilter",
                  "config": {
                    "clientId": "serviceConfidentialClient",
                    "clientSecretId": "client.secret.id",
                    "secretsProvider" : "SystemAndEnvSecretStore-1"
                  }
                }
              ]
            }
          },
          {
            "name": "ExchangeFailureHandler",
            "type": "StaticResponseHandler",
            "capture": "all",
            "config": {
              "status": 400,
              "entity": "${contexts.oauth2Failure.error}: ${contexts.oauth2Failure.description}",
              "headers": {
                "Content-Type": [
                  "application/json"
                ]
              }
            }
          }
        ],
        "handler": {
          "type": "Chain",
          "config": {
            "filters": [
              {
                "name": "oauth2TokenExchangeFilter",
                "type": "OAuth2TokenExchangeFilter",
                "config": {
                  "amService": "AmService-1",
                  "endpointHandler": "ExchangeHandler",
                  "subjectToken": "#{request.entity.form['subject_token'][0]}",
                  "scopes": ["mail"],
                  "failureHandler": "ExchangeFailureHandler"
                }
              }
            ],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "content-type": [
                    "application/json"
                  ]
                },
                "entity": "{\"access_token\": \"${contexts.oauth2TokenExchange.issuedToken}\", \"issued_token_type\": \"${contexts.oauth2TokenExchange.issuedTokenType}\"}"
              }
            }
          }
        }
      }

      Notice the following features of the route:

      • The route matches requests to /token-exchange

      • PingGateway reads the subjectToken from the request entity.

      • The StaticResponseHandler returns an issued token.

  3. Test the setup:

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

      $ subjecttoken=$(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") \
      && echo $subjecttoken
      
      hc-...c6A
    2. Introspect the subject token at the AM introspection endpoint:

      $ curl --location \
      --request POST 'http://am.example.com:8088/openam/oauth2/realms/root/introspect' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode "token=${subjecttoken}" \
      --data-urlencode 'client_id=client-application' \
      --data-urlencode 'client_secret=password'
      
      Decoded access_token: {
        "active": true,
        "scope": "employeenumber mail",
        "realm": "/",
        "client_id": "client-application",
        "user_id": "demo",
        "username": "demo",
        "token_type": "Bearer",
        "exp": 1626796888,
        "sub": "(usr!demo)",
        "subname": "demo",
        "iss": "http://am.example.com:8088/openam/oauth2",
        "auth_level": 0,
        "authGrantId": "W-j...E1E",
        "may_act": {
          "client_id": "serviceConfidentialClient"
        },
        "auditTrackingId": "4be...169"
      }

      Note that in the subject token, the client_id is client-application, and the scopes are employeenumber and mail. The may_act claim indicates that serviceConfidentialClient is authorized to exchange this token.

    3. Exchange the subject token for an issued token:

      $ issuedtoken=$(curl \
      --cacert /path/to/secrets/ig.example.com-certificate.pem \
      --location \
      --request POST 'https://ig.example.com:8443/token-exchange' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data "subject_token=${subjecttoken}" | jq -r ".access_token") \
      && echo $issuedtoken
      
      F8e...Q3E
    4. Introspect the issued token at the AM introspection endpoint:

      $ curl --location \
      --request POST 'http://am.example.com:8088/openam/oauth2/realms/root/introspect' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode "token=${issuedtoken}" \
      --data-urlencode 'client_id=serviceConfidentialClient' \
      --data-urlencode 'client_secret=password'
      
      {
        "active": true,
        "scope": "mail",
        "realm": "/",
        "client_id": "serviceConfidentialClient",
        "user_id": "demo",
        "username": "demo",
        "token_type": "Bearer",
        "exp": 1629200490,
        "sub": "(usr!demo)",
        "subname": "demo",
        "iss": "http://am.example.com:8088/openam/oauth2",
        "auth_level": 0,
        "authGrantId": "aYK...EPA",
        "may_act": {
          "client_id": "serviceConfidentialClient"
        },
        "auditTrackingId": "814...367"
      }

      Note that in the issued token, the client_id is serviceConfidentialClient, and the only the scope is mail.