---
title: OIDC ID tokens to SAML assertions with PingAM
description: Configure PingGateway to use the TokenTransformationFilter with PingAM STS to transform OIDC ID tokens into SAML 2.0 assertions
component: pinggateway
version: 2026
page_id: pinggateway:gateway-guide:ttf
canonical_url: https://docs.pingidentity.com/pinggateway/2026/gateway-guide/ttf.html
revdate: 2026-01-15
section_ids:
  update_am_settings: Update AM settings
  token_signing: Token signing
  client_secret: Client secret
  id_token_normalization_script: ID token normalization script
  account_mapping_script: Account mapping script
  tree_for_sts: Tree for STS
  add_rest_sts: Add REST STS
  configure_pinggateway: Configure PingGateway
  validation: Validation
---

# OIDC ID tokens to SAML assertions with PingAM

This page builds on the example in [OpenID Connect and PingAM](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 PingGateway TokenTransformationFilter to bridge the gap between OpenID Connect and SAML 2.0 frameworks.

The following figure illustrates the data flow:

![Flow of information for token transformation.](_images/ig-ttf-schematic.png)

1. A user tries to access a protected resource.

2. If the user isn't authenticated, the `AuthorizationCodeOAuth2ClientFilter` redirects the request to AM. After authentication, AM asks for the user's consent to give PingGateway 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 `contexts.oauth2Info.idToken` and `contexts.oauth2Info.idTokenClaims` 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 the user issued the transaction. The STS then issues a SAML assertion to PingGateway on behalf of the user.

6. The `TokenTransformationFilter` puts the SAML assertion in `${contexts.sts.issuedToken}`.

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

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

## Update AM settings

Before you start, set up and test the example in [AM as OIDC provider](oidc-am.html).

The following example uses an authentication tree for the [Security Token Service (STS)](https://docs.pingidentity.com/pingam/8.1/sts/preface.html) to validate the incoming ID token and map its subject to an account.

### Token signing

Change the token signature algorithm in the `oidc_client` profile.

1. In the AM admin UI, select Applications > OAuth 2.0 > Clients > `oidc_client`.

2. Under Signing and Encryption, set ID Token Signing Algorithm to `HS256` and click Save Changes.

### Client secret

Store the client secret in a secret store.

1. Go to <http://am.example.com:8088/openam/encode.jsp> and use the page to encode the client secret, `password`.

2. Write the result to a AM default password secret store file named `clientpass`:

   ```console
   $ echo -n <encoded-client-secret> > /path/to/openam/security/secrets/encrypted/clientpass
   ```

|   |                                           |
| - | ----------------------------------------- |
|   | In production, use your own secret store. |

### ID token normalization script

Create a script to normalize the ID token for the STS.

1. Select [icon: code, set=fa]Scripts > + New Script.

2. Create a script with the following settings:

   * Name: `Normalize id_token`

   * Script Type: `Social Identity Provider Profile Transformation`

   * Language: `JavaScript`

   * Evaluator Version: `Legacy`

3. In the script editor, add the following JavaScript and click Save Changes:

   ```javascript
   // The ID token "subname" claim holds the account username:
   (function () {
       var fr = JavaImporter( org.forgerock.json.JsonValue);
       var identity = fr.JsonValue.json(fr.JsonValue.object());
       identity.put('userName', jwtClaims.get('subname'));
       return identity;
   }());
   ```

### Account mapping script

Create a script to map the normalized ID token to the AM account username.

1. Select [icon: code, set=fa]Scripts > + New Script.

2. Create a script with the following settings:

   * Name: `Add username to shared state`

   * Script Type: `Decision node script for authentication trees`

   * Evaluator Version: `Next Generation`

3. In the script editor, add the following JavaScript and click Save Changes:

   ```javascript
   // Get the username from the identity returned by the previous script:
   var attributes = nodeState.get("lookupAttributes");
   var userName = attributes.get("userName");
   nodeState.putShared("username", userName);
   action.goTo('true');
   ```

### Tree for STS

Add a tree to validate the incoming ID token and map its subject to the AM demo user account.

1. Select [icon: user, set=fa]Authentication > Trees > + Create Tree.

2. Name the new tree `TransformIdToken`.

3. Add nodes to the tree as in the following image:

   ![TransformIdToken tree](_images/TransformIdToken-tree.png)

   * The [OIDC ID Token Validator node](https://docs.pingidentity.com/auth-node-ref/8.1/auth-node-oidc-idtoken-validator.html) lets the STS validate the incoming ID token.

     The node has the following non-default settings:

     * OpenID Connect Validation Type: `Client Secret`

     * OpenID Connect Validation Value: `password` (Although required, the value isn't used.)

     * Client Secret Label: `clientpass`

     * Token Issuer: `http://am.example.com:8088/openam/oauth2`

     * Audience name: `oidc_client`

     * Authorized parties: `oidc_client`

     * Transformation Script: `Normalize id_token`

   * The [Scripted Decision node](https://docs.pingidentity.com/auth-node-ref/8.1/auth-node-scripted-decision.html) maps the ID token subject claim to an AM username.

     The node has the following non-default settings:

     * Script: `Add username to shared state`

     * Outcomes: `true`

4. Click Save.

### Add REST STS

Add a REST STS instance to transform the ID token to a SAML v2.0 assertion.

1. Click STS > + Add Rest STS, add the following non-default settings:

   * Deployment URL Element

     `openig` (must match the TokenTransformationFilter `"instance"`)

   * Deployment

     * Authentication Target Mappings: replace the existing `OPENIDCONNECT|…​` value with:

       `OPENIDCONNECT|service|TransformIdToken|oidc_id_token_auth_target_header_key=oidc_id_token`

   * 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: `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`

   * OpenID Connect Token

     * OpenID Connect Token Provider Issuer Id: `oidc`

     * Token signature algorithm: `HMAC SHA 256`

     * Client Secret: `password`

     * Issued Tokens Audience: `oidc_client`

2. Click Create.

3. Under SAML 2 Token on the new STS instance, add the following Attribute Mappings:

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

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

4. Click Save Changes.

## Configure PingGateway

1. Set environment variables for `oidc_client` and `ig_agent`, then restart PingGateway:

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

2. Add the following route to PingGateway:

   * Linux

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

   * Windows

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

   ```json
   {
     "name": "50-idtoken",
     "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"
                       }
                     },
                     "clientSecretIdUsage": "ID_TOKEN_VALIDATION_ONLY",
                     "secretsProvider": "SystemAndEnvSecretStore-1",
                     "clientSecretId": "oidc.secret.id",
                     "scopes": [
                       "openid",
                       "profile",
                       "email"
                     ],
                     "authenticatedRegistrationHandler": "AuthenticatedRegistrationHandler-1"
                   }
                 }
               ],
               "requireHttps": false,
               "cacheExpiration": "disabled"
             }
           },
           {
             "name": "TokenTransformationFilter-1",
             "type": "TokenTransformationFilter",
             "config": {
               "idToken": "${contexts.oauth2Info.idToken}",
               "instance": "openig",
               "amService": "AmService-1"
             }
           }
         ],
         "handler": {
           "type": "StaticResponseHandler",
           "config": {
             "status": 200,
             "headers": {
               "Content-Type": [ "text/plain; charset=UTF-8" ]
             },
             "entity": "{\"id_token\":\n\"${contexts.oauth2Info.idToken}\"} \n\n\n{\"saml_assertions\":\n\"${contexts.sts.issuedToken}\"}"
           }
         }
       }
     }
   }
   ```

   Source: [50-idtoken.json](../_attachments/config/routes/50-idtoken.json)

   Learn how to use Studio to set up the PingGateway route in [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 PingGateway to act as an OIDC relying party:

     * The client endpoint is `/home/id_token`, so the service URIs for this filter in PingGateway are `/home/id_token/login`, `/home/id_token/logout`, and `/home/id_token/callback`.

     * For convenience in this test, `requireHttps` is false.

     * The context for storing authorization state information is `${contexts.oauth2Info}`. Later filters and handlers can find access tokens and user information in this context.

   * The `ClientRegistration` holds configuration provided in [AM as OIDC provider](oidc-am.html). PingGateway uses it to connect to AM and verify the ID token signature with the client secret.

   * The `TokenTransformationFilter` transforms an ID token into a SAML assertion:

     * The `id_token` parameter defines where this filter gets the ID token.

       The `TokenTransformationFilter` puts the result in `${contexts.sts.issuedToken}`.

   * On success, a `StaticResponseHandler` displays the ID token and SAML assertion.

## Validation

1. In your browser's privacy or incognito mode, go to <https://ig.example.com:8443/home/id_token>.

   AM displays the login screen.

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

   AM prompts you to allow access to your account information.

3. Select Allow.

   The `StaticResponseHandler` displays the ID token and SAML assertion:

   ```none
   {"id_token":
   "<id-token>"}


   {"saml_assertions":
   "…​<saml:Subject><saml:NameID…​>demo</saml:NameID>…​"}
   ```

|   |                                                                                                                                                     |
| - | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|   | If a request returns an HTTP 414 URI Too Long error, refer to [URI Too Long error](../maintenance-guide/troubleshooting.html#troubleshoot-HTTP414). |
