PingGateway 2024.11

Password replay with AM

Password replay brings SSO to legacy web applications without the need to edit, upgrade, or recode them.

Use PingGateway with an appropriate AM authentication tree to capture and replay username password credentials. PingGateway and AM share a secret key to encrypt and decrypt the password and keep it safe.

When running PingOne Advanced Identity Cloud, read Password replay instead.

Request flow

The following figure illustrates the flow of requests when an unauthenticated user accesses a protected application. After authenticating with AM, the user is logged into the application with the username and password from the AM login session.

Password replay sequence diagram
Figure 1. Password replay sequence diagram
  • PingGateway intercepts the browser’s HTTP GET request.

  • PingGateway redirects the user to AM for authentication.

  • AM authenticates the user and stores the encrypted password in a JWT.

  • AM redirects the browser back to the protected application with the JWT.

  • PingGateway intercepts the browser’s HTTP GET request again:

    • The user is authenticated.

    • PingGateway gets the password from the JWT and decrypts it.

    • PingGateway gets the username from AM based on the user _id.

  • PingGateway replaces the request with an HTTP POST to the application, taking the credentials from the context.

  • The sample application validates the credentials from the HTTP POST request.

  • The sample application responds with the user’s profile page.

  • PingGateway passes the response from the sample application to the browser.

Tasks

Before you begin

Before you begin, prepare AM, PingGateway, and the sample application. Learn more in the example installation for this guide.

Task 1: Prepare a shared secret

PingGateway and AM share a secret symmetric key to encrypt and decrypt the password.

The following tasks use the AM default aestest 256-bit AES test key as the shared secret.

Don’t use the test key in production. In a production deployment, generate a random AES 256-bit key to use as a shared secret. How you generate the secret key is up to you. For example:

$ openssl rand -base64 32
YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd5eHowMTIzNDU=

Keep the secret safe.

You’ll set the AM secret label scripted.node.aes.key to reference it in a script to encrypt the user’s credentials. You’ll also share the secret with PingGateway to decrypt the user’s credentials before replaying them.

Task 2: Configure AM

This example uses the default-passwords-store secret store for the shared secret. In a production deployment, use your own secret store and shared secret:

  1. Go to http://am.example.com:8088/openam/encode.jsp and use the page to encode the base64-encoded AES key.

    In this example, the base64-encoded AES key is YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd5eHowMTIzNDU=.

  2. Write the result to a default-passwords-store file named scripted.node.aes.key:

    $ echo -n <encoded-aes-key> > /path/to/openam/security/secrets/encrypted/scripted.node.aes.key

    Only secret labels that begin with the string scripted.node. are accessible to scripts.

  3. Select Realms > Top Level Realm > 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/*?*

  4. In the agent profile for PingGateway, add a redirect URI for CDSSO that PingGateway can use in the route for password replay.

    Go to Applications > Agents > Identity Gateway > ig_agent, set Redirect URLs for CDSSO to https://ig.example.com:8443/replay/redirect, and click Save Changes.

  5. Go to Scripts, click + New Script and create a Decision node script for authentication trees with the Legacy engine named Password replay and the following JavaScript content.

    The script adds a JWT encrypted with the shared secret key to the session:

    Show script
    var fr = JavaImporter(
      org.forgerock.openam.auth.node.api.Action,
      org.forgerock.openam.auth.node.api,
      javax.security.auth.callback.TextOutputCallback,
      org.forgerock.json.jose.builders.JwtBuilderFactory,
      org.forgerock.json.jose.jwt.JwtClaimsSet,
      org.forgerock.json.jose.jwe.JweAlgorithm,
      org.forgerock.json.jose.jwe.EncryptionMethod,
      javax.crypto.spec.SecretKeySpec,
      org.forgerock.secrets.SecretBuilder,
      org.forgerock.util.encode.Base64
    );
    
    var NodeOutcome = {
      ERROR: 'false',
      SUCCESS: 'true'
    };
    
    var config = {
      encryptionKey: secrets.getGenericSecret("scripted.node.aes.key").getAsUtf8()
    };
    
    function getKey () {
      logger.message("encKey: " + config.encryptionKey)
      return new fr.SecretKeySpec(fr.Base64.decode(config.encryptionKey), 'AES');
    }
    
    function buildJwt (claims) {
      logger.message('Building response JWT');
      var encryptionKey = getKey();
      var jwtClaims = new fr.JwtClaimsSet;
      jwtClaims.setClaims(claims);
      var jwt = new fr.JwtBuilderFactory()
                      .jwe(encryptionKey)
                      .headers()
                      .alg(fr.JweAlgorithm.DIRECT)
                      .enc(fr.EncryptionMethod.A128CBC_HS256)
                      .done()
                      .claims(jwtClaims)
                      .build();
      return jwt;
    }
    
    try {
      password=nodeState.get("password").asString()
      var registrationClaims = { password: password };
      passwordJwt = buildJwt(registrationClaims);
      action = fr.Action.goTo(NodeOutcome.SUCCESS).putSessionProperty("sunIdentityUserPassword", passwordJwt).build();
    } catch (e) {
      logger.error('ERROR ' + e);
      action = fr.Action.send(new fr.TextOutputCallback(fr.TextOutputCallback.ERROR, e.toString())).build();
    }

    You can download the script as password-replay-am.js.

  6. Go to Authentication > Trees, click + Create Tree, and create an authentication tree named Password replay configured with the nodes shown in this screenshot:

    Password replay journey

    • The Page node presents a page with input fields to prompt for the username and password.

      • The Username collector node collects and injects the userName into the shared node state.

      • The Password collector node collects and injects the password into the shared node state.

    • The Data Store Decision node uses the username and password to determine whether authentication is successful.

    • The Scripted Decision node references your script and has the same outcomes: true and false.

  7. Make sure the allowlist for scripted decision nodes includes all the classes the script requires.

    Go to Configure > Global Services > Scripting > Secondary Configurations > AUTHENTICATION_TREE_DECISION_NODE > Secondary Configurations > engineConfiguration, add these Java classes to the allowlist, then click Save Changes:

    • javax.crypto.spec.SecretKeySpec

    • org.forgerock.json.jose.builders.EncryptedJwtBuilder

    • org.forgerock.json.jose.builders.JweHeaderBuilder

    • org.forgerock.json.jose.builders.JwtBuilderFactory

    • org.forgerock.json.jose.jwe.EncryptionMethod

    • org.forgerock.json.jose.jwe.JweAlgorithm

    • org.forgerock.json.jose.jwt.JwtClaimsSet

    • org.forgerock.secrets.SecretBuilder

    • org.forgerock.util.encode.Base64

  8. Make sure the test user can log in with the tree before continuing.

    To verify the tree works as expected, in your browser’s privacy or incognito mode, go to http://am.example.com:8088/openam/XUI/?service=Password%20replay#login and log in as test user demo with password Ch4ng31t.

Task 3: Configure PingGateway

The password replay configuration includes the PingGateway password to connect to AM, the shared secret, and the route for password replay.

  1. Set up PingGateway for HTTPS.

  2. Set an environment variable locally on the computer running PingGateway to access the agent password:

    # The base64-encoded PingGateway agent "password":
    $ export AGENT_SECRET_ID='cGFzc3dvcmQ='

    PingGateway retrieves the password with a SystemAndEnvSecretStore, which requires it to be base64-encoded.

    The password must match the agent profile password you set in AM. PingGateway uses the password to connect to AM.

  3. Set an environment variable locally on the computer running PingGateway to access the shared secret key:

    # The base64-encoded shared secret key:
    $ export AES_KEY='YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd5eHowMTIzNDU='

    PingGateway retrieves the shared secret with a SystemAndEnvSecretStore, which requires it to be base64-encoded.

    The shared secret key must match the secret key the AM script uses to encrypt the password to replay. PingGateway uses the secret key to decrypt the password to replay.

  4. Restart PingGateway to read the secrets from the environment.

  5. Add a route to PingGateway to serve the sample application CSS and other static resources:

    • Linux

    • Windows

    $HOME/.openig/config/routes/00-static-resources.json
    %appdata%\OpenIG\config\routes\00-static-resources.json
    {
      "name" : "00-static-resources",
      "baseURI" : "http://app.example.com:8081",
      "condition": "${find(request.uri.path,'^/css') or matchesWithRegex(request.uri.path, '^/.*\\\\.ico$') or matchesWithRegex(request.uri.path, '^/.*\\\\.gif$')}",
      "handler": "ReverseProxyHandler"
    }
  6. Add a route for password replay:

    • Linux

    • Windows

    $HOME/.openig/config/routes/04-replay.json
    %appdata%\OpenIG\config\routes\04-replay.json
    {
      "name": "04-replay",
      "condition": "${find(request.uri.path, '^/replay')}",
      "properties": {
        "amInstanceUrl": "http://am.example.com:8088/openam/"
      },
      "heap": [
        {
          "name": "SystemAndEnvSecretStore-1",
          "type": "SystemAndEnvSecretStore",
          "config": {
            "mappings": [
              {
                "secretId": "aes.key",
                "format": {
                  "type": "SecretKeyPropertyFormat",
                  "config": {
                    "format": "BASE64",
                    "algorithm": "AES"
                  }
                }
              }
            ]
          }
        },
        {
          "name": "AmService-1",
          "type": "AmService",
          "config": {
            "agent": {
              "username": "ig_agent",
              "passwordSecretId": "agent.secret.id"
            },
            "secretsProvider": "SystemAndEnvSecretStore-1",
            "url": "&{amInstanceUrl}"
          }
        },
        {
          "name": "CapturedUserPasswordFilter-1",
          "type": "CapturedUserPasswordFilter",
          "config": {
            "ssoToken": "${contexts.ssoToken.value}",
            "keySecretId": "aes.key",
            "keyType": "AES",
            "secretsProvider": "SystemAndEnvSecretStore-1",
            "amService": "AmService-1"
          }
        }
      ],
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [
            {
              "name": "CrossDomainSingleSignOnFilter-1",
              "type": "CrossDomainSingleSignOnFilter",
              "config": {
                "redirectEndpoint": "/replay/redirect",
                "authCookie": {
                  "path": "/replay",
                  "name": "ig-token-cookie"
                },
                "amService": "AmService-1",
                "authenticationService": "Password replay"
              }
            },
            {
              "name": "UserProfileFilter-1",
              "type": "UserProfileFilter",
              "config": {
                "username": "${contexts.ssoToken.info.uid}",
                "userProfileService": {
                  "type": "UserProfileService",
                  "config": {
                    "amService": "AmService-1",
                    "profileAttributes": [
                      "username"
                    ]
                  }
                }
              }
            },
            {
              "name": "PasswordReplayFilter-1",
              "type": "PasswordReplayFilter",
              "config": {
                "loginPage": "${true}",
                "credentials": "CapturedUserPasswordFilter-1",
                "request": {
                  "method": "POST",
                  "uri": "http://app.example.com:8081/login",
                  "form": {
                    "username": [
                      "${contexts.userProfile.username}"
                    ],
                    "password": [
                      "${contexts.capturedPassword.value}"
                    ]
                  }
                }
              },
              "capture": [
                "all"
              ]
            }
          ],
          "handler": "ReverseProxyHandler"
        }
      }
    }

    The route:

    • Matches requests whose path starts with /replay.

    • Sets an amInstanceUrl property to the URL for AM.

    • Loads the aes.key secret key from the local AES_KEY environment variable.

    • Connects to AM as ig_agent with the agent.secret.id password from the local AGENT_SECRET_ID environment variable.

    • Extracts the captured password from the SSO token context and decrypts it with the aes.key secret key.

    • Uses a CrossDomainSingleSignOnFilter to redirect to the AM Password replay tree for authentication, getting the authentication information from the ig-token-cookie.

    • Queries AM to retrieve the username for logging in.

      You can retrieve other profile attributes with the UserProfileFilter, such as the email address or first and last names. The sample application expects the username in this example, so the route gets the username.

    • Logs in to the sample application with the username and password.

    • Returns the result to the browser.

    In production, remove "capture": ["all"] from the PasswordReplayFilter to avoid recording credentials in the logs.

Validation

  1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/replay/.

    PingGateway redirects to the AM authentication tree.

  2. Log in as test user demo with password Ch4ng31t.

    PingGateway successfully replays the credentials against the sample application. The sample application displays its user profile page:

    Successful password replay
  3. Review the PingGateway output.

    On success, the output displays the credentials and the profile page:

    ...INFO  o.f.o.d.c.C.c.PasswordReplayFilter-1 @04-replay -
    
    [CONTINUED]--- (filtered-request) exchangeId:<id> - transactionId:<id> --->
    
    [CONTINUED]POST http://app.example.com:8081/login HTTP/1.1
    [CONTINUED]Content-Length: 31
    [CONTINUED]Content-Type: application/x-www-form-urlencoded
    
    [CONTINUED]password=Ch4ng31t&username=demo
    
    ...INFO  o.f.o.d.c.C.c.PasswordReplayFilter-1 @04-replay -
    
    [CONTINUED]<--- (response) exchangeId:<id> - transactionId:<id> ---
    
    [CONTINUED]HTTP/1.1 200 OK
    ...
    
    [CONTINUED]<!DOCTYPE html>
    ...

You have successfully demonstrated password replay with PingGateway and AM.

If password replay fails, consider the outcome of the HTTP POST from PingGateway to the sample application:

HTTP 401 Unauthorized

PingGateway is not replaying the credentials.

Review the PingGateway output to determine whether the username or password is missing when PingGateway replays the credentials.

If the password is missing, make sure PingGateway and AM share the same AES secret key.

HTTP 403 Forbidden

PingGateway is not replaying the right credentials.

Make sure you’re using a username-password combination known to the sample application.