---
title: Token transformation
description: This chapter builds on the example in OpenID Connect to transform OpenID Connect ID tokens into SAML 2.0 assertions.
component: pinggateway
version: 2023.11
page_id: pinggateway:gateway-guide:token-transformation
canonical_url: https://docs.pingidentity.com/pinggateway/2026/gateway-guide/token-transformation.html
llms_txt: https://docs.pingidentity.com/pinggateway/llms.txt
docs_for_agents: https://developer.pingidentity.com/build-with-ai/docs-for-agents.md
revdate: 2023-06-07T15:12:25Z
keywords: ["Configuration", "OpenID Connect (OIDC)", "SAML 2.0", "Security Token Service (STS)", "OAuth 2.0"]
superseded_by: https://docs.pingidentity.com/pinggateway/2026/gateway-guide/token-transformation.html
section_ids:
  ttf: Transform OpenID Connect ID tokens into SAML assertions
  token-exchange: OAuth 2.0 token exchange
---

# Token transformation

## Transform OpenID Connect ID tokens into SAML assertions

This chapter builds on the example in [OpenID Connect](oidc.html) to transform OpenID Connect ID tokens into SAML 2.0 assertions.

Many enterprises use existing or legacy, SAML 2.0-based SSO, but many mobile and social applications are managed by OpenID Connect. Use the IG TokenTransformationFilter to bridge the gap between OpenID Connect and SAML 2.0 frameworks.

The following figure illustrates the data flow:

![Flow of information between a request](_images/ttf-schematic.svg)

1. A user tries to access to a protected resource.

2. If the user is not authenticated, the AuthorizationCodeOAuth2ClientFilter redirects the request to AM. After authentication, AM asks for the user's consent to give IG access to private information.

3. If the user consents, AM returns an id_token to the AuthorizationCodeOAuth2ClientFilter. The filter opens the id_token JWT and makes it available in `attributes.openid .id_token` and `attributes.openid.id_token_claims` for downstream filters.

4. The TokenTransformationFilter calls the AM STS to transform the id_token into a SAML 2.0 assertion.

5. The STS validates the signature, decodes the payload, and verifies that the user issued the transaction. The STS then issues a SAML assertion to IG on behalf of the user.

6. The TokenTransformationFilter makes the result of the token transformation available to downstream handlers in the `issuedToken` property of the `${contexts.sts}` context.

The following sequence diagram shows a more detailed view of the flow:

![The steps when an id_token is generated.](_images/ttf-idtoken.svg)

1. Set up an AM Security Token Service (STS), where the subject confirmation method is Bearer. For more information about setting up a REST STS instance, see AM's [Security Token Service (STS) guide](https://docs.pingidentity.com/pingam/7.4/sts/preface.html).

   1. Set up AM as described in [Use AM as a single OpenID Connect provider](oidc.html#oidc-am).

   2. Select Applications > Agents > Identity Gateway and register an IG agent with the following values:

      * Agent ID: `ig_agent`

      * Password: `password`

        For AM 6.5.x and earlier versions, register an agent as described in [Register an IG agent in AM 6.5 and earlier](preface.html#register-agent-65).

        |   |                                                                                                                   |
        | - | ----------------------------------------------------------------------------------------------------------------- |
        |   | 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 an IG agent to AM](preface.html#authenticate-agent-am).

      |   |                                                                                                                                                                                                                                  |
      | - | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
      |   | IG 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. Create a Bearer Module:

      1. In the top level realm, select [icon: user, set=fa]Authentication > Modules, and add a module with the following values:

         * Module name : `oidc`

         * Type : `OpenID Connect id_token bearer`

      2. In the configuration page, enter the following values:

         * OpenID Connect validation configuration type : `Client Secret`

         * OpenID Connect validation configuration value : `password`

           This is the password of the OAuth 2.0/OpenID Connect client.

         * Client secret : `password`

         * Name of OpenID Connect ID Token Issuer : `http://am.example.com:8088/openam/oauth2`

         * Audience name : `oidc_client`

           This is the name of the OAuth 2.0/OpenID Connect client.

         * List of accepted authorized parties : `oidc_client`

           Leave all other values as default, and save your settings.

   5. Create an instance of STS REST.

      1. In the top level realm, select STS, and add a Rest STS instance with the following values:

         * Deployment URL Element : `openig`

           This value identifies the STS instance and is used by the `instance` parameter in the TokenTransformationFilter.

         * SAML2 Token

           |   |                                                                      |
           | - | -------------------------------------------------------------------- |
           |   | For STS, it isn't necessary to create a SAML SP configuration in AM. |

           * SAML2 issuer Id : `OpenAM`

           * Service Provider Entity Id : `openig_sp`

           * NameIdFormat : Select `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`

         * OpenID Connect Token

           * OpenID Connect Token Provider Issuer Id : `oidc`

           * Token signature algorithm : Enter a value that is consistent with [Use AM as a single OpenID Connect provider](oidc.html#oidc-am), for example, `HMAC SHA 256`

           * Client Secret : `password`

           * Issued Tokens Audience : `oidc_client`

      2. On the SAML 2 Token tab, add the following Attribute Mappings:

         * Key : `userName`, Value : `uid`

         * Key : `password`, Value : `mail`

   6. Log out of AM.

2. Set up IG:

   1. Set an environment variable for `oidc_client` and `ig_agent`, and then restart IG:

      ```bash
      $ export OIDC_SECRET_ID='cGFzc3dvcmQ='
      $ export AGENT_SECRET_ID='cGFzc3dvcmQ='
      ```

   2. Add the following route to IG:

      * Linux

      * Windows

      ```
      $HOME/.openig/config/routes/50-idtoken.json
      ```

      ```
      %appdata%\OpenIG\config\routes\50-idtoken.json
      ```

      ```json
      {
        "name": "50-idtoken",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/home/id_token')}",
        "heap": [
          {
            "name": "SystemAndEnvSecretStore-1",
            "type": "SystemAndEnvSecretStore"
          },
          {
            "name": "AuthenticatedRegistrationHandler-1",
            "type": "Chain",
            "config": {
              "filters": [
                {
                  "name": "ClientSecretBasicAuthenticationFilter-1",
                  "type": "ClientSecretBasicAuthenticationFilter",
                  "config": {
                    "clientId": "oidc_client",
                    "clientSecretId": "oidc.secret.id",
                    "secretsProvider": "SystemAndEnvSecretStore-1"
                  }
                }
              ],
              "handler": "ForgeRockClientHandler"
            }
          },
          {
            "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": "AuthorizationCodeOAuth2ClientFilter-1",
                "type": "AuthorizationCodeOAuth2ClientFilter",
                "config": {
                  "clientEndpoint": "/home/id_token",
                  "failureHandler": {
                    "type": "StaticResponseHandler",
                    "config": {
                      "status": 500,
                      "headers": {
                        "Content-Type": [
                          "text/plain"
                        ]
                      },
                      "entity": "An error occurred during the OAuth2 setup."
                    }
                  },
                  "registrations": [
                    {
                      "name": "oidc-user-info-client",
                      "type": "ClientRegistration",
                      "config": {
                        "clientId": "oidc_client",
                        "issuer": {
                          "name": "Issuer",
                          "type": "Issuer",
                          "config": {
                            "wellKnownEndpoint": "http://am.example.com:8088/openam/oauth2/.well-known/openid-configuration"
                          }
                        },
                        "scopes": [
                          "openid",
                          "profile",
                          "email"
                        ],
                        "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-1"
                      }
                    }
                  ],
                  "requireHttps": false,
                  "cacheExpiration": "disabled"
                }
              },
              {
                "name": "TokenTransformationFilter-1",
                "type": "TokenTransformationFilter",
                "config": {
                  "idToken": "${attributes.openid.id_token}",
                  "instance": "openig",
                  "amService": "AmService-1"
                }
              }
            ],
            "handler": {
              "type": "StaticResponseHandler",
              "config": {
                "status": 200,
                "headers": {
                  "Content-Type": [ "text/plain; charset=UTF-8" ]
                },
                "entity": "{\"id_token\":\n\"${attributes.openid.id_token}\"} \n\n\n{\"saml_assertions\":\n\"${contexts.sts.issuedToken}\"}"
              }
            }
          }
        }
      }
      ```

      For information about how to set up the IG route in Studio, refer to [Token transformation in Structured Editor](../studio-guide/examples-se.html#example-ttf-se).

      Notice the following features of the route:

      * The route matches requests to `/home/id_token`.

      * The AmService in the heap is used for authentication and REST STS requests.

      * The AuthorizationCodeOAuth2ClientFilter enables IG to act as an OpenID Connect relying party:

        * The client endpoint is set to `/home/id_token`, so the service URIs for this filter on the IG server are `/home/id_token/login`, `/home/id_token/logout`, and `/home/id_token/callback`.

        * For convenience in this test, `requireHttps` is false. In production environments, set it to true. So that you see the delegated authorization process when you make a request, `requireLogin` is true.

        * The target for storing authorization state information is `${attributes.openid}`. Subsequent filters and handlers can find access tokens and user information at this target.

      * The ClientRegistration holds configuration provided in [Use AM as a single OpenID Connect provider](oidc.html#oidc-am), and used by IG to connect with AM.

      * The TokenTransformationFilter transforms an id_token into a SAML assertion:

        * The `id_token` parameter defines where this filter gets the id_token created by the `AuthorizationCodeOAuth2ClientFilter`.

          The TokenTransformationFilter makes the result of the token transformation available to downstream handlers in the `issuedToken` property of the `${contexts.sts}` context.

        * The `instance` parameter must match the `Deployment URL Element` for the REST STS instance.

          Errors that occur during token transformation cause an error response to be returned to the client and an error message to be logged for the IG administrator.

      * When the request succeeds, a StaticResponseHandler retrieves and displays the id_token from the target `{attributes.openid.id_token}`.

3. Test the setup:

   1. Go to <http://ig.example.com:8080/home/id_token>.

      The AM login screen is displayed.

   2. Log in to AM as username `demo`, password `Ch4ng31t`.

      An OpenID Connect request to access private information is displayed.

   3. Select Allow.

      The id_token and SAML assertions are displayed:

      ```json
      {"id_token": "eyA . . ."}

      {"saml_assertions": "<\"saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Version= . . ."}
      ```

## OAuth 2.0 token exchange

The following sections describe 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/7.4/am-oauth2/token-exchange.html) in the AM documentation.

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

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

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

Before you start, prepare AM, IG, and the sample application as described in [Example installation for this guide](preface.html#preface-examples).

1. Set up AM:

   1. (From AM 6.5.3) Select Services > Add a Service, and add a Validation Service with the following Valid goto URL Resources:

      * `http://ig.example.com:8080/*`

      * `http://ig.example.com:8080/*?*`

   2. Select Applications > Agents > Identity Gateway, and register an IG 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. |

   3. (Optional) Authenticate the agent to AM as described in [Authenticate an IG agent to AM](preface.html#authenticate-agent-am).

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

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

   1. Set an environment variable for the serviceConfidentialClient password:

      ```bash
      $ export CLIENT_SECRET_ID='cGFzc3dvcmQ='
      ```

   2. Set an environment variable for the IG agent password, and then restart IG:

      ```bash
      $ export AGENT_SECRET_ID='cGFzc3dvcmQ='
      ```

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

   3. Add the following route to IG to exchange the access token:

      * Linux

      * Windows

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

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

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

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

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

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

      ```bash
      $ issuedtoken=$(curl \
      --location \
      --request POST 'http://ig.example.com:8080/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:

      ```bash
      $ 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`.
