PingGateway 2024.11

Audit the deployment

The following sections describe how to set up auditing for your deployment. You can find information on how to include user ID in audit logs in Recording User ID in Audit Events.

You can find more information about configuring the JMS event handler in Audit framework.

Record access audit events in CSV

This section describes how to record access audit events in a CSV file, using tamper-evident logging. For information about the CSV audit event handler, refer to CsvAuditEventHandler.

The CSV handler doesn’t sanitize messages when writing to CSV log files.

Don’t open CSV logs in spreadsheets or other applications that treat data as code.

Before you start, prepare PingGateway and the sample application as described in the Quick install.

  1. Set up secrets for tamper-evident logging:

    1. Locate a directory for secrets, and go to it:

      $ cd /path/to/secrets
    2. Generate a key pair in the keystore.

      The CSV event handler expects a JCEKS-type keystore with a key alias of signature for the signing key, where the key is generated with the RSA key algorithm and the SHA256withRSA signature algorithm:

      $ keytool \
       -genkeypair \
       -keyalg RSA \
       -sigalg SHA256withRSA \
       -alias "signature" \
       -dname "CN=ig.example.com,O=Example Corp,C=FR" \
       -keystore audit-keystore \
       -storetype JCEKS \
       -storepass password \
       -keypass password
      Because keytool converts all characters in its key aliases to lowercase, use only lowercase in alias definitions of a keystore.
    3. Generate a secret key in the keystore.

      The CSV event handler expects a JCEKS-type keystore with a key alias of csv-key-2 for the symmetric key, where the key is generated with the HmacSHA256 key algorithm and 256-bit key size:

      $ keytool \
       -genseckey \
       -keyalg HmacSHA256 \
       -keysize 256 \
       -alias "password" \
       -keystore audit-keystore \
       -storetype JCEKS \
       -storepass password \
       -keypass password
    4. Verify the content of the keystore:

      $ keytool \
       -list \
       -keystore audit-keystore \
       -storetype JCEKS \
       -storepass password
      
      Keystore type: JCEKS
      Keystore provider: SunJCE
      
      Your keystore contains 2 entries
      
      password, ... SecretKeyEntry,
      signature, ... PrivateKeyEntry,
      Certificate fingerprint (SHA1): 4D:...:D1
  2. Set up PingGateway

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway, replacing /path/to/secrets/audit-keystore with your path:

      • Linux

      • Windows

      $HOME/.openig/config/routes/30-csv.json
      %appdata%\OpenIG\config\routes\30-csv.json
      {
        "name": "30-csv",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/home/csv-audit')}",
        "heap": [
          {
            "name": "AuditService",
            "type": "AuditService",
            "config": {
              "eventHandlers": [
                {
                  "class": "org.forgerock.audit.handlers.csv.CsvAuditEventHandler",
                  "config": {
                    "name": "csv",
                    "logDirectory": "/tmp/logs",
                    "security": {
                      "enabled": "true",
                      "filename": "/path/to/secrets/audit-keystore",
                      "password": "password",
                      "signatureInterval": "1 day"
                    },
                    "topics": [
                      "access"
                    ]
                  }
                }
              ],
              "config": { }
            }
          }
        ],
        "auditService": "AuditService",
        "handler": "ForgeRockClientHandler"
      }

      The route calls an audit service configuration for publishing log messages to the CSV file, /tmp/logs/access.csv.

      When a request matches audit, audit events are logged to the CSV file.

      The route uses the ForgeRockClientHandler as its handler, to send the X-ForgeRock-TransactionId header with its requests to external services.

  3. Test the setup:

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

      The home page of the sample application is displayed, and the file /tmp/logs/tamper-evident-access.csv is updated.

Record access audit events with a JMS audit event handler

This procedure is an example of how to record access audit events with a JMS audit event handler configured to use the ActiveMQ message broker. This example isn’t tested on all configurations, and can be more or less relevant to your configuration.

You can find information about configuring the JMS event handler in JmsAuditEventHandler.

Before you start, prepare PingGateway as described in the Quick install.

  1. Download the following files:

  2. Add the files to the configuration:

    • Create the directory $HOME/.openig/extra, where $HOME/.openig is the instance directory, and add .jar files to the directory.

  3. Create a consumer that subscribes to the audit topic.

    From the ActiveMQ installation directory, run the following command:

    $ ./bin/activemq consumer --destination topic://audit
  4. Set up PingGateway

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/30-jms.json
      %appdata%\OpenIG\config\routes\30-jms.json
      {
        "name": "30-jms",
        "MyCapture" : "all",
        "baseURI": "http://app.example.com:8081",
        "condition" : "${request.uri.path == '/activemq_event_handler'}",
        "heap": [
          {
            "name": "AuditService",
            "type": "AuditService",
            "config": {
              "eventHandlers" : [
                {
                  "class" : "org.forgerock.audit.handlers.jms.JmsAuditEventHandler",
                  "config" : {
                    "name" : "jms",
                    "topics": [ "access" ],
                    "deliveryMode" : "NON_PERSISTENT",
                    "sessionMode" : "AUTO",
                    "jndi" : {
                      "contextProperties" : {
                        "java.naming.factory.initial" : "org.apache.activemq.jndi.ActiveMQInitialContextFactory",
                        "java.naming.provider.url" : "tcp://am.example.com:61616",
                        "topic.audit" : "audit"
                      },
                      "topicName" : "audit",
                      "connectionFactoryName" : "ConnectionFactory"
                    }
                  }
                }
              ],
              "config" : { }
            }
          }
        ],
        "auditService": "AuditService",
        "handler" : {
          "type" : "StaticResponseHandler",
          "config" : {
            "status" : 200,
            "headers" : {
              "Content-Type" : [ "text/plain; charset=UTF-8" ]
            },
            "entity" : "Message from audited route"
          }
        }
      }

      When a request matches the /activemq_event_handler route, this configuration publishes JMS messages containing audit event data to an ActiveMQ managed JMS topic, and the StaticResponseHandler displays a message.

  5. Test the setup:

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

      Depending on how ActiveMQ is configured, audit events are displayed on the ActiveMQ console or written to file.

Record access audit events with a JSON audit event handler

This section describes how to record access audit events with a JSON audit event handler. You can find more information about configuring the JSON event handler in JsonAuditEventHandler.

  1. Set up PingGateway

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/30-json.json
      %appdata%\OpenIG\config\routes\30-json.json
      {
        "name": "30-json",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/home/json-audit')}",
        "heap": [
          {
            "name": "AuditService",
            "type": "AuditService",
            "config": {
              "eventHandlers": [
                {
                  "class": "org.forgerock.audit.handlers.json.JsonAuditEventHandler",
                  "config": {
                    "name": "json",
                    "logDirectory": "/tmp/logs",
                    "topics": [
                      "access"
                    ],
                    "rotationRetentionCheckInterval": "1 minute",
                    "buffering": {
                      "maxSize": 100000,
                      "writeInterval": "100 ms"
                    }
                  }
                }
              ]
            }
          }
        ],
        "auditService": "AuditService",
        "handler": "ReverseProxyHandler"
      }

      Notice the following features of the route:

      • The route calls an audit service configuration for publishing log messages to the JSON file, /tmp/audit/access.audit.json. When a request matches /home/json-audit, a single line per audit event is logged to the JSON file.

      • The route uses the ForgeRockClientHandler as its handler, to send the X-ForgeRock-TransactionId header with its requests to external services.

  2. Test the setup:

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

      The home page of the sample application is displayed and the file /tmp/logs/access.audit.json is created or updated with a message. The following example message is formatted for easy reading, but it is produced as a single line for each event:

      {
        "_id": "830...-41",
        "timestamp": "2019-...540Z",
        "eventName": "OPENIG-HTTP-ACCESS",
        "transactionId": "830...-40",
        "client": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 51666
        },
        "server": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 8080
        },
        "http": {
          "request": {
            "secure": false,
            "method": "GET",
            "path": "http://ig.example.com:8080/home/json-audit",
            "headers": {
              "accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8"],
              "host": ["ig.example.com:8080"],
              "user-agent": ["Mozilla/5.0 ... Firefox/66.0"]
            }
          }
        },
        "response": {
          "status": "SUCCESSFUL",
          "statusCode": "200",
          "elapsedTime": 212,
          "elapsedTimeUnits": "MILLISECONDS"
        },
        "ig": {
          "exchangeId": "b3f...-29",
          "routeId": "30-json",
          "routeName": "30-json"
        }
      }

Record access audit events to standard output

This section describes how to record access audit events to standard output. You can find more information about the event handler in JsonStdoutAuditEventHandler.

Before you start, prepare PingGateway and the sample application as described in the Quick install.

  1. Set up PingGateway

    1. Set up PingGateway for HTTPS, as described in Configure PingGateway for TLS (server-side).

    2. Add the following route to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/config/routes/30-jsonstdout.json
      %appdata%\OpenIG\config\routes\30-jsonstdout.json
      {
        "name": "30-jsonstdout",
        "baseURI": "http://app.example.com:8081",
        "condition": "${find(request.uri.path, '^/home/jsonstdout-audit')}",
        "heap": [
          {
            "name": "AuditService",
            "type": "AuditService",
            "config": {
              "eventHandlers": [
                {
                  "class": "org.forgerock.audit.handlers.json.stdout.JsonStdoutAuditEventHandler",
                  "config": {
                    "name": "jsonstdout",
                    "elasticsearchCompatible": false,
                    "topics": [
                      "access"
                    ]
                  }
                }
              ],
              "config": {}
            }
          }
        ],
        "auditService": "AuditService",
        "handler": "ReverseProxyHandler"
      }

      Notice the following features of the route:

      • The route matches requests to /home/jsonstdout-audit.

      • The route calls the audit service configuration for publishing access log messages to standard output. When a request matches /home/jsonstdout-audit, a single line per audit event is logged.

  2. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/jsonstdout-audit.

    The home page of the sample application is displayed, and a message like this is published to standard output:

    {
      "_id": "830...-61",
      "timestamp": "2019-...89Z",
      "eventName": "OPENIG-HTTP-ACCESS",
      "transactionId": "830...-60",
      "client": {
        "ip": "0:0:0:0:0:0:0:1",
        "port": 51876
      },
      "server": {
        "ip": "0:0:0:0:0:0:0:1",
        "port": 8080
      },
      "http": {
        "request": {
          "secure": false,
          "method": "GET",
          "path": "http://ig.example.com:8080/home/jsonstdout-audit",
          "headers": {
            "accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8"],
            "host": ["ig.example.com:8080"],
            "user-agent": ["Mozilla/5.0 ... Firefox/66.0"]
          }
        }
      },
      "response": {
        "status": "SUCCESSFUL",
        "statusCode": "200",
        "elapsedTime": 10,
        "elapsedTimeUnits": "MILLISECONDS"
      },
      "ig": {
        "exchangeId": "b3f...-41",
        "routeId": "30-jsonstdout",
        "routeName": "30-jsonstdout"
      },
      "source": "audit",
      "topic": "access",
      "level": "INFO"
    }

Trust transaction IDs from other products

Each audit event is identified by a unique transaction ID that can be communicated across products and recorded for each local event. By using the transaction ID, requests can be tracked as they traverse the platform, making it easier to monitor activity and to enrich reports.

The X-ForgeRock-TransactionId header is automatically set in all outgoing HTTP calls from one ForgeRock product to another. Customers can also set this header themselves from their own applications or scripts that call into the Ping Identity Platform.

To reduce the risk of malicious attacks, by default PingGateway doesn’t trust transaction ID headers from client applications.

If you trust the transaction IDs sent by your client applications, consider setting Java system property org.forgerock.http.TrustTransactionHeader to true.

Add the following system property in env.sh:

# Specify a JVM option
TX_HEADER_OPT="-Dorg.forgerock.http.TrustTransactionHeader=true"

# Include it into the JAVA_OPTS environment variable
export JAVA_OPTS="${TX_HEADER_OPT}"

All incoming X-ForgeRock-TransactionId headers are trusted, and monitoring or reporting systems that consume the logs can allow requests to be correlated as they traverse multiple servers.

Safelist audit event fields for the logs

To prevent logging of sensitive data for an audit event, the Common Audit Framework uses a safelist to specify which audit event fields appear in the logs.

By default, only safelisted audit event fields are included in the logs. You can find information about how to include non-safelisted audit event fields or exclude safelisted audit event fields in Including or excluding audit event fields in logs.

Audit event fields use JSON pointer notation, and are taken from the JSON schema for the audit event content. The following event fields are safelisted:

  • /_id

  • /timestamp

  • /eventName

  • /transactionId

  • /trackingIds

  • /userId

  • /client

  • /server

  • /ig/exchangeId

  • /ig/routeId

  • /ig/routeName

  • /http/request/secure

  • /http/request/method

  • /http/request/path

  • /http/request/headers/accept

  • /http/request/headers/accept-api-version

  • /http/request/headers/content-type

  • /http/request/headers/host

  • /http/request/headers/user-agent

  • /http/request/headers/x-forwarded-for

  • /http/request/headers/x-forwarded-host

  • /http/request/headers/x-forwarded-port

  • /http/request/headers/x-forwarded-proto

  • /http/request/headers/x-original-uri

  • /http/request/headers/x-real-ip

  • /http/request/headers/x-request-id

  • /http/request/headers/x-requested-with

  • /http/request/headers/x-scheme

  • /request

  • /response

Include or exclude audit event fields in logs

The safelist is designed to prevent logging of sensitive data for audit events by specifying which audit event fields appear in the logs. You can add or remove messages from the logs as follows:

  • To include audit event fields in logs that aren’t safelisted, configure the includeIf property of AuditService.

    Before you include non-safelisted audit event fields in the logs, consider the impact on security. Including some headers, query parameters, or cookies in the logs could cause credentials or tokens to be logged, and allow anyone with access to the logs to impersonate the holder of these credentials or tokens.
  • To exclude safelisted audit event fields from the logs, configure the excludeIf property of AuditService. You can find an example in Exclude safelisted audit event fields from logs.

Exclude safelisted audit event fields from logs

Before you start, set up and test the example in Recording access audit events in JSON. Note the audit event fields in the log file access.audit.json.

  1. Replace 30-json.json with the following route:

    • Linux

    • Windows

    $HOME/.openig/config/routes/30-json-excludeif.json
    %appdata%\OpenIG\config\routes\30-json-excludeif.json
    {
      "name": "30-json-excludeif",
      "baseURI": "http://app.example.com:8081",
      "condition": "${find(request.uri.path, '^/home/json-audit-excludeif$')}",
      "heap": [
        {
          "name": "AuditService",
          "type": "AuditService",
          "config": {
            "config": {
              "filterPolicies": {
                "field": {
                  "excludeIf": [
                    "/access/http/request/headers/host",
                    "/access/http/request/path",
                    "/access/server",
                    "/access/response"
                  ]
                }
              }
            },
            "eventHandlers": [
              {
                "class": "org.forgerock.audit.handlers.json.JsonAuditEventHandler",
                "config": {
                  "name": "json",
                  "logDirectory": "/tmp/logs",
                  "topics": [
                    "access"
                  ],
                  "rotationRetentionCheckInterval": "1 minute",
                  "buffering": {
                    "maxSize": 100000,
                    "writeInterval": "100 ms"
                  }
                }
              }
            ]
          }
        }
      ],
      "auditService": "AuditService",
      "handler": "ReverseProxyHandler"
    }

    Notice that the AuditService is configured with an excludeIf property to exclude audit event fields from the logs.

  2. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/json-audit-excludeif.

    The home page of the sample application is displayed and the file /tmp/logs/access.audit.json is updated:

    {
      "_id": "830...-41",
      "timestamp": "2019-...540Z",
      "eventName": "OPENIG-HTTP-ACCESS",
      "transactionId": "830...-40",
      "client": {
        "ip": "0:0:0:0:0:0:0:1",
        "port": 51666
      },
      "http": {
        "request": {
          "secure": false,
          "method": "GET",
          "headers": {
            "accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],
            "user-agent": ["Mozilla/5.0 ... Firefox/66.0"]
          }
        }
      },
      "ig": {
        "exchangeId": "b3f...-56",
        "routeId": "30-json-excludeif",
        "routeName": "30-json-excludeif"
      }
    }
  3. Compare the audit event fields in access.audit.json with those produced in Recording access audit events in JSON, and note that the audit event fields specified by the excludeIf property no longer appear in the logs.

Record user ID in audit events

The following sections provide examples of how to capture the AM user ID in audit logs.

Sample scripts are available in the openig-samples.jar file, to capture the user ID after SSO, CDSSO, OpenID, or SAML authentication. The scripts inject the user ID into the RequestAuditContext so that it is available when the audit event is written.

Using the notes in the sample scripts, adapt the script for your deployment. For example, configure which user_info field to capture in the audit event.

The audit service in these examples use a JsonStdoutAuditEventHandler, which writes audit events to standard output, but can be any other audit service.

Record user ID in audit logs after SSO authentication

Before you start, set up and test the example in Cross-domain single sign-on (CDSSO).

  1. Add the following script to PingGateway:

    • Linux

    • Windows

    $HOME/.openig/scripts/groovy/InjectUserIdSso.groovy
    %appdata%\OpenIG\scripts\groovy\InjectUserIdSso.groovy
    package scripts.groovy
    
    import org.forgerock.openig.openam.SsoTokenContext
    import org.forgerock.services.context.RequestAuditContext
    
    /**
     * Sample ScriptableFilter implementation to capture the user id from the session
     * and inject it into the RequestAuditContext for later use when the audit event
     * is written.
     *
     * This ScriptableFilter should be added in the filter chain at whatever point the
     * desired user id is available - e.g. on the session after SSO.
     *
     * "handler": {
     *   "type": "Chain",
     *   "config": {
     *     "filters": [ {
     *        "name": "SingleSignOnFilter-1",
     *         "type": "SingleSignOnFilter",
     *         "config": {
     *           "amService": "AmService-1"
     *         }
     *       }, {
     *         "type" : "ScriptableFilter",
     *         "config" : {
     *           "file" : "InjectUserIdSso.groovy",
     *           "type": "application/x-groovy"
     *         }
     *       }
     *     ],
     *     "handler" : "ReverseProxyHandler",
     * }
     *
     * When using the SSO/ CDSSO flow then the SsoTokenContext is guaranteed to exist and
     * be populated if there was no error. The RequestAuditContext is also guaranteed to
     * be available. Note also that if the SessionInfoFilter is present in the route then
     * a SessionInfoContext would be available in the context chain and could be queried
     * for user info.
     *
     * Implementors may decide which user id field to capture in the audit event:
     * - The sessionInfo universalId - 'universalId' - is always available as
     *   provided by AM and resembles -
     *   e.g. "id=bonnie,ou=user,o=myrealm,ou=services,dc=openam,dc=forgerock,dc=org".
     * - The sessionInfo username - mapped to 'username') resembles - e.g. "bonnie".
     *   Field 'username' should be preferred to 'uid', which also points to 'username'.
     *
     * Additional error handling may be required.
     *
     * @see RequestAuditContext
     * @see SsoTokenContext
     * @see org.forgerock.openig.openam.SessionInfoContext
     */
    
    def requestAuditContext = context.asContext(RequestAuditContext.class)
    def ssoTokenContext = context.asContext(SsoTokenContext.class)
    
    // The sessionInfo 'universalId' is always available, though 'username' may be unknown
    requestAuditContext.setUserId(ssoTokenContext.universalId)
    
    // Propagate the request to the next filter/ handler in the chain
    next.handle(context, request)

    The script captures the user ID after SSO or CDSSO authentication, and injects it into the RequestAuditContext so that it is available when the audit event is written.

  2. Replace sso.json with the following route:

    • Linux

    • Windows

    $HOME/.openig/config/routes/audit-sso.json
    %appdata%\OpenIG\config\routes\audit-sso.json
    {
      "name": "audit-sso",
      "baseURI": "http://app.example.com:8081",
      "condition": "${find(request.uri.path, '^/home/audit-sso$')}",
      "heap": [
        {
          "name": "AuditService",
          "type": "AuditService",
          "config": {
            "eventHandlers": [
              {
                "class": "org.forgerock.audit.handlers.json.stdout.JsonStdoutAuditEventHandler",
                "config": {
                  "name": "jsonstdout",
                  "elasticsearchCompatible": false,
                  "topics": [
                    "access"
                  ]
                }
              }
            ],
            "config": {}
          }
        },
        {
          "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/"
          }
        }
      ],
      "auditService": "AuditService",
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [
            {
              "name": "SingleSignOnFilter-1",
              "type": "SingleSignOnFilter",
              "config": {
                "amService": "AmService-1"
              }
            },
            {
              "type" : "ScriptableFilter",
              "config" : {
                "file" : "InjectUserIdSso.groovy",
                "type": "application/x-groovy"
              }
            }
          ],
          "handler": "ReverseProxyHandler"
        }
      }
    }

    Notice the following features of the route compared to sso.json:

    • The route matches requests to /home/audit-sso.

    • An audit service is included to publish access log messages to standard output.

    • The chain includes a scriptable filter that refers to InjectUserIdSso.groovy.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/audit-sso. The SingleSignOnFilter redirects the request to AM for authentication.

    2. Log in to AM as user demo, password Ch4ng31t, and then allow the application to access user information.

      The profile page of the sample application is displayed. The script captures the user ID from the session, and the audit service includes it with the audit event.

    3. Search the standard output for a message like this, containing the user ID:

      {
        "_id": "23a...-23",
        "timestamp": "...",
        "eventName": "OPENIG-HTTP-ACCESS",
        "transactionId": "23a...-22",
        "userId": "id=demo,ou=user,dc=openam,dc=forgerock,dc=org",
        "client": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 57843
        },
        "server": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 8080
        },
        "http": {
          "request": {
            "secure": false,
            "method": "GET",
            "path": "http://ig.example.com/home/audit-sso",
            "headers": {
              "accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8"],
              "host": ["ig.example.com:8080"],
              "user-agent": [...]
            }
          }
        },
        "response": {
          "status": "SUCCESSFUL",
          "statusCode": "200",
          "elapsedTime": 276,
          "elapsedTimeUnits": "MILLISECONDS"
        },
        "ig": {
          "exchangeId": "1dc...-26",
          "routeId": "audit-sso",
          "routeName": "audit-sso"
        },
        "source": "audit",
        "topic": "access",
        "level": "INFO"
      }

Record user ID in audit logs after OpenID connect authentication

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

  1. Set up the script:

    1. Add the following example script to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/scripts/groovy/InjectUserIdOpenId.groovy
      %appdata%\OpenIG\scripts\groovy\InjectUserIdOpenId.groovy
      package scripts.groovy
      
      import org.forgerock.services.context.AttributesContext
      import org.forgerock.services.context.RequestAuditContext
      
      /**
       * Sample script implementation supporting user id injection in an OpenId scenario.
       * This sample captures the user id and injects it into the RequestAuditContext for
       * later use when the audit event is written.
       *
       * This ScriptableFilter should be added in the filter chain at whatever point the
       * desired user id is available - e.g. after OpenId client authentication (in the
       * OAuth2 authentication filter chain) - as follows:
       *
       * "handler" : {
       *   "type" : "Chain",
       *   "config" : {
       *     "filters" : [ {
       *       "type" : "OAuth2ClientFilter",
       *       "config" : {
       *         ...
       *         "target" : "${attributes.target}",
       *         "registrations" : [ "ClientRegistrationWithOpenIdScope" ],
       *       }
       *     }, {
       *       "type" : "ScriptableFilter",
       *       "config" : {
       *         "file" : "InjectUserIdOpenId.groovy",
       *         "type": "application/x-groovy"
       *       }
       *     } ],
       *     "handler" : "display-user-info-groovy-handler"
       *   }
       * }
       *
       * The ClientRegistration associated with the above OAuth2ClientFilter config will
       * require the 'openid' scope. The OAuth2SessionContext is guaranteed to exist and
       * be populated on successful authentication. The userinfo will then be populated
       * according to the OAuth2ClientFilter OpenId 'target' configuration (e.g. in this
       * sample, on the AttributesContext). The 'target' referenced will be populated
       * with a 'user_info' JSON value containing the userinfo. It should be noted that
       * the OAuth2ClientFilter 'target' config is a config-time expression, and cannot
       * be used in a ScriptableFilter to read runtime data. The RequestAuditContext is
       * also guaranteed to be available.
       *
       * Implementors may decide which 'user_info' field to capture in the audit event:
       * - The userinfo 'sub' field is the user's "complex" ID marked with a type - e.g.
       *   "(usr!bonnie)".
       * - The userinfo 'subName' field is the user's username (or resource name) - e.g.
       *   "bonnie".
       * - To capture the universalId (consistent with the session info universalId),
       *   it is necessary to configure AM to provide it as a claim in the id-token. To
       *   do this, edit the OIDC Claims Script to include the following line just prior
       *   to the UserInfoClaims creation:
       *       computedClaims["universalId"] = identity.universalId
       * - This will include 'universalId' in the userinfo which we can use with audit
       *   e.g. "id=bonnie,ou=user,o=myrealm,ou=services,dc=openam,dc=forgerock,dc=org"
       *
       * Additional error handling may be required.
       *
       * @see RequestAuditContext
       * @see AttributesContext
       */
      
      def requestAuditContext = context.asContext(RequestAuditContext.class)
      def attributesContext = context.asContext(AttributesContext.class)
      
      // The OAuth2ClientFilter captures userinfo based on its 'target' configuration.
      // In this sample 'target' is configured as the AttributesContext with key "target".
      // We can query this for 'user_info' values: 'sub', 'subName' or anything else
      // made available via the OIDC Claims Script (see above).
      def oauth2UserInfo = attributesContext.getAttributes().get("target")
      requestAuditContext.setUserId(oauth2UserInfo.get("user_info").get("sub"))
      
      // Propagate the request to the next filter/ handler in the chain
      next.handle(context, request)

      The script captures the user ID from the AuthorizationCodeOAuth2ClientFilter target object, by default at ${attributes.openid}, and injects it into the RequestAuditContext so that it is available when the audit event is written.

    2. Edit the script to get the attributes from the openid target:

      Replace attributesContext.getAttributes().get("target")

      with attributesContext.getAttributes().get("openid").

  2. Replace 07-openid.json with the following route:

    • Linux

    • Windows

    $HOME/.openig/config/routes/audit-oidc.json
    %appdata%\OpenIG\config\routes\audit-oidc.json
    {
      "name": "audit-oidc",
      "baseURI": "http://app.example.com:8081",
      "condition": "${find(request.uri.path, '^/home/id_token')}",
      "heap": [
        {
          "name": "AuditService",
          "type": "AuditService",
          "config": {
            "eventHandlers": [
              {
                "class": "org.forgerock.audit.handlers.json.stdout.JsonStdoutAuditEventHandler",
                "config": {
                  "name": "jsonstdout",
                  "elasticsearchCompatible": false,
                  "topics": [
                    "access"
                  ]
                }
              }
            ],
            "config": {}
          }
        },
        {
          "name": "SystemAndEnvSecretStore-1",
          "type": "SystemAndEnvSecretStore"
        }
      ],
      "auditService": "AuditService",
      "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": "Error in OAuth 2.0 setup."
                  }
                },
                "registrations": [
                  {
                    "name": "oidc-user-info-client",
                    "type": "ClientRegistration",
                    "config": {
                      "clientId": "oidc_client",
                      "clientSecretId": "oidc.secret.id",
                      "issuer": {
                        "name": "Issuer",
                        "type": "Issuer",
                        "config": {
                          "wellKnownEndpoint": "http://am.example.com:8088/openam/oauth2/.well-known/openid-configuration"
                        }
                      },
                      "scopes": [
                        "openid",
                        "profile",
                        "email"
                      ],
                      "secretsProvider": "SystemAndEnvSecretStore-1",
                      "tokenEndpointAuthMethod": "client_secret_basic"
                    }
                  }
                ],
                "requireHttps": false,
                "cacheExpiration": "disabled"
              }
            },
            {
              "type" : "ScriptableFilter",
              "config" : {
                "file" : "InjectUserIdOpenId.groovy",
                "type": "application/x-groovy"
              }
            }
          ],
          "handler": "ReverseProxyHandler"
        }
      }
    }

    Notice the following features of the route compared to 07-openid.json:

    • An audit service is included to publish access log messages to standard output.

    • The chain includes a scriptable filter that refers to InjectUserIdOpenId.groovy.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to https://ig.example.com:8443/home/id_token. The AM login page is displayed.

    2. Log in to AM as user demo, password Ch4ng31t, and then allow the application to access user information.

      The home page of the sample application is displayed. The script captures the user ID from the openid target, and the audit service includes it with the audit event.

    3. Search the standard output for a message like this, containing the user ID:

      {
        "_id": "b64...-25",
        "timestamp": "2021...",
        "eventName": "OPENIG-HTTP-ACCESS",
        "transactionId": "b64...-24",
        "userId": "(usr!demo)",
        "client": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 64443
        },
        "server": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 8080
        },
        "http": {
          "request": {
            "secure": false,
            "method": "GET",
            "path": "http://ig.example.com:8080/home/id_token",
            "headers": {
              "accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8"],
              "host": ["ig.example.com:8080"],
              "user-agent": [...]
            }
          }
        },
        "response": {
          "status": "SUCCESSFUL",
          "statusCode": "200",
          "elapsedTime": 199,
          "elapsedTimeUnits": "MILLISECONDS"
        },
        "ig": {
          "exchangeId": "1dc...-26",
          "routeId": "audit-oidc",
          "routeName": "audit-oidc"
        },
        "source": "audit",
        "topic": "access",
        "level": "INFO"
      }

Record user ID in audit logs after SAML authentication

This example uses the deprecated SamlFederationHandler. The SamlFederationHandler is replaced by the SamlFederationFilter and will be removed in a future release.

Before you start, set up and test the SamlFederationHandler example under SAML.

  1. Set up the script:

    1. Add the following example script to PingGateway:

      • Linux

      • Windows

      $HOME/.openig/scripts/groovy/InjectUserIdSaml.groovy
      %appdata%\OpenIG\scripts\groovy\InjectUserIdSaml.groovy
      package scripts.groovy
      
      import org.forgerock.http.session.SessionContext
      import org.forgerock.services.context.RequestAuditContext
      
      /**
       * Sample ScriptableFilter implementation to capture the user id obtained from a
       * SAML assertion. The IG SamlFederationHandler captures this and locates it on
       * the SessionContext with the key as the configured SAML 2 user id key. We then
       * take this and inject it into the RequestAuditContext for later use when the
       * audit event is written.
       *
       * This ScriptableFilter should be added in the filter chain together with the
       * SamlFederationHandler, as follows. Note that the InjectUserIdSaml.groovy script
       * operates on the response, injecting the userId as captured by the handler.
       *
       * {
       *     "condition" : "${matches(request.uri.path,'^/api/saml')}",
       *     "handler" : {
       *         "type" : "Chain",
       *         "config" : {
       *             "filters" : [ {
       *                 "type" : "ScriptableFilter",
       *                 "config" : {
       *                     "file" : "InjectUserIdSaml.groovy",
       *                     "type": "application/x-groovy"
       *                 }
       *             } ],
       *             "handler" : {
       *                 "name" : "saml_handler_SPOne",
       *                 "type" : "SamlFederationHandler",
       *                 "config" : {
       *                      "assertionMapping" : {
       *                          "SPOne_userName" : "uid",
       *                          "SPOne_password" : "mail"
       *                      },
       *                      "redirectURI" : "/api/home",
       *                      "logoutURI" : "http://openig.example.com:8082/api/after_logout",
       *                      "subjectMapping" : "SubjectName_SPOne",
       *                      "authnContext" : "AuthnContext_SPOne",
       *                      "sessionIndexMapping" : "SessionIndex_SPOne"
       *                 }
       *             }
       *         }
       *     }
       * }
       *
       * The SessionContext and RequestAuditContext are guaranteed to be available and the
       * SessionContext will have been populated with userinfo on successful authentication.
       *
       * Implementors may decide which user id field to capture in the audit event:
       * - This should be based on SAML attribute mappings and/ or the subject mapping (if
       *   transient names are not used).
       * - Other attributes are available, such as 'uid' and 'userName', though  it must be
       *   noted that there is an expectation that the IDP makes available the user id.
       * - In this sample, 'SPOne_userName' maps to the 'uid'.
       *
       * Additional error handling may be required.
       *
       * @see RequestAuditContext
       * @see SessionContext
       */
      
      // Propagate the request to the next filter/ handler in the chain
      next.handle(context, request)
          .then({ response ->
              def requestAuditContext = context.asContext(RequestAuditContext.class)
              def sessionContext = context.asContext(SessionContext.class)
      
              // Inject the user id as captured by the SamlFederationHandler
              requestAuditContext.setUserId(sessionContext.getSession().get("SPOne_userName"))
              return response
          })

      The script captures the user ID from the SessionContext subject or attribute mappings, provided by the SamlFederationHandler from the inbound assertions. It injects the user ID into the RequestAuditContext so that it is available when the audit event is written.

    2. Replace get("SPOne_userName")) with get("username")).

      The script captures the user ID from the assertionMapping username, which is mapped in the route to cn.

  2. Replace saml.json with the following route:

    • Linux

    • Windows

    $HOME/.openig/config/routes/audit-saml.json
    %appdata%\OpenIG\config\routes\audit-saml.json
    {
      "name": "audit-saml",
      "condition": "${find(request.uri.path, '^/saml')}",
      "heap": [
        {
          "name": "AuditService",
          "type": "AuditService",
          "config": {
            "eventHandlers": [
              {
                "class": "org.forgerock.audit.handlers.json.stdout.JsonStdoutAuditEventHandler",
                "config": {
                  "name": "jsonstdout",
                  "elasticsearchCompatible": false,
                  "topics": [
                    "access"
                  ]
                }
              }
            ],
            "config": {}
          }
        }
      ],
      "auditService": "AuditService",
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [
            {
              "type" : "ScriptableFilter",
              "config" : {
                "file" : "InjectUserIdSaml.groovy",
                "type": "application/x-groovy"
              }
            }
          ],
          "handler": {
            "type": "SamlFederationHandler",
            "config": {
              "useOriginalUri": true,
              "assertionMapping": {
                "username": "cn",
                "password": "sn"
              },
              "subjectMapping": "sp-subject-name",
              "redirectURI": "/home/federate"
            }
          }
        }
      }
    }

    Notice the following features of the route compared to saml.json:

    • An audit service is included to publish access log messages to standard output.

    • The main Handler is a Chain that includes a scriptable filter to refer to InjectUserIdSaml.groovy.

    • The script uses the assertionMapping username to capture the user ID.

  3. Test the setup:

    1. In your browser’s privacy or incognito mode, go to IDP-initiated SSO.

    2. Log in to AM with username demo and password Ch4ng31t.

      PingGateway returns the response page showing that the demo user has logged in. The script captures the user ID from the session, and the audit service includes it with the audit event.

    3. Search the standard output for a message like this, containing the user ID:

      {
        "_id": "82f...-14",
        "timestamp": "2021-...",
        "eventName": "OPENIG-HTTP-ACCESS",
        "transactionId": "82f...-13",
        "userId": "demo",
        "client": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 60655
        },
        "server": {
          "ip": "0:0:0:0:0:0:0:1",
          "port": 8080
        },
        "http": {
          "request": {
            "secure": false,
            "method": "POST",
            "path": "http://sp.example.com:8080/saml/fedletapplication/metaAlias/sp",
            "headers": {
              "accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8"],
              "content-type": ["application/x-www-form-urlencoded"],
              "host": ["sp.example.com:8080"],
              "user-agent": [...]
            }
          }
        },
        "response": {
          "status": "SUCCESSFUL",
          "statusCode": "302",
          "elapsedTime": 2112,
          "elapsedTimeUnits": "MILLISECONDS"
        },
        "ig": {
          "exchangeId": "1dc...-26",
          "routeId": "audit-saml",
          "routeName": "audit-saml"
        },
        "source": "audit",
        "topic": "access",
        "level": "INFO"
      }