PingGateway 2024.9

OIDC ID tokens to SAML assertions

This page builds on the example in OpenID Connect 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.
  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 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 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:

ttf-idtoken

Update AM settings

Before you start, set up and test the example in AM as OIDC provider.

The following example uses an authentication tree for the Security Token Service (STS) 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:

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

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

    // 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 Authentication > Trees > + Create Tree.

  2. Name the new tree TransformIdToken.

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

    TransformIdToken tree
    • The OIDC ID Token Validator node 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 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:

    $ export OIDC_SECRET_ID='cGFzc3dvcmQ='
    $ export AGENT_SECRET_ID='cGFzc3dvcmQ='
  2. Add the following route to PingGateway:

    • Linux

    • Windows

    $HOME/.openig/config/routes/50-idtoken.json
    %appdata%\OpenIG\config\routes\50-idtoken.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"
                        }
                      },
                      "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": "${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}\"}"
            }
          }
        }
      }
    }

    Learn how to set up the PingGateway route in Studio in Token transformation in Structured Editor.

    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 target for storing authorization state information is ${attributes.openid}. Later filters and handlers can find access tokens and user information at this target.

    • The ClientRegistration holds configuration provided in AM as OIDC provider. 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:

    {"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.