---
title: OAuth 2.0 token exchange with PingAM
description: Configure OAuth 2.0 token exchange with PingAM as the authorization server, using PingGateway to exchange a subject token for an issued token
component: pinggateway
version: 2026
page_id: pinggateway:gateway-guide:token-exchange
canonical_url: https://docs.pingidentity.com/pinggateway/2026/gateway-guide/token-exchange.html
revdate: 2026-02-12T00:00:00Z
---

# OAuth 2.0 token exchange with PingAM

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*. Learn more in [Token exchange](https://docs.pingidentity.com/pingam/8.1/am-oauth2/token-exchange.html) in the AM documentation.

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

![Flow of information for token exchange.](_images/token-exchange.svg)

|   |                                                                                                                                                                                                                                         |
| - | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|   | This procedure uses the *Resource Owner Password Credentials* grant type. As suggested in [The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749#section-10.7), use other grant types whenever possible. |

Before you begin, prepare AM, PingGateway, and the sample application. Learn more in the [example installation for this guide](preface.html#preface-examples).

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](preface.html#register-agent-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. 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`

   4. Select [icon: code, set=fa]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.

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

   6. 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)](../installation-guide/securing-connections.html#server-side-tls).

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

      ```console
      $ export CLIENT_SECRET_ID='cGFzc3dvcmQ='
      ```

   3. Set an environment variable for the PingGateway agent password, and then restart PingGateway:

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

        `$HOME/.openig/config/routes/token-exchange.json`

      * Windows

        `%appdata%\OpenIG\config\routes\token-exchange.json`

      ```json
      {
        "name": "token-exchange",
        "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}\"}"
              }
            }
          }
        }
      }
      ```

      Source: [token-exchange.json](../_attachments/config/routes/token-exchange.json)

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

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

      Output

      ```
      hc-...c6A
      ```

   2. Introspect the subject token at the AM introspection endpoint:

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

      Output

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

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

      Output

      ```
      F8e...Q3E
      ```

   4. Introspect the issued token at the AM introspection endpoint:

      ```none
      $ 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'
      ```

      Output

      ```
      {
        "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 scope is `mail`.
