Recording Custom Audit Events
This section describes how to record a custom audit event to standard output. The example is based on the example in "Validating Access_Tokens Through the Introspection Endpoint", adding an audit event for the custom topic OAuth2AccessTopic
.
To record custom audit events to other outputs, adapt the route in the following procedure to use another audit event handler.
For information about how to configure supported audit event handlers, and exclude sensitive data from log files, see Auditing Your Deployment. For more information about audit event handlers, see Audit Framework.
Before you start, prepare IG and the sample application as described in Getting Started Guide.
Set up AM as described in "Validating Access_Tokens Through the Introspection Endpoint".
Define the schema of an event topic called
OAuth2AccessTopic
by adding the following route to IG:$HOME/.openig/audit-schemas/OAuth2AccessTopic.json
%appdata%\OpenIG\audit-schemas/OAuth2AccessTopic.json
{ "schema": { "$schema": "http://json-schema.org/draft-04/schema#", "id": "OAuth2Access", "type": "object", "properties": { "_id": { "type": "string" }, "timestamp": { "type": "string" }, "transactionId": { "type": "string" }, "eventName": { "type": "string" }, "accessToken": { "type": "object", "properties": { "scopes": { "type": "array", "items": { "type": "string" } }, "expiresAt": "number", "sub": "string" }, "required": [ "scopes" ] }, "resource": { "type": "object", "properties": { "path": { "type": "string" }, "method": { "type": "string" } } } } }, "filterPolicies": { "field": { "includeIf": [ "/_id", "/timestamp", "/eventName", "/transactionId", "/accessToken", "/resource" ] } }, "required": [ "_id", "timestamp", "transactionId", "eventName" ] }
Notice that the schema includes the following fields:
Mandatory fields
_id
,timestamp
,transactionId
, andeventName
.accessToken
, to include the access_token scopes, expiry time, and the subject.resource
, to include the path and method.filterPolicies
, to specify additional event fields to include in the logs.
Define a script to generate audit events on the topic named
OAuth2AccessTopic
, by adding the following file to the IG configuration as:$HOME/.openig/scripts/groovy/OAuth2Access.groovy
%appdata%\OpenIG\scripts\groovy/OAuth2Access.groovy
import static org.forgerock.json.resource.Requests.newCreateRequest; import static org.forgerock.json.resource.ResourcePath.resourcePath; // Helper functions def String transactionId() { return contexts.transactionId.transactionId.value; } def JsonValue auditEvent(String eventName) { return json(object(field('eventName', eventName), field('transactionId', transactionId()), field('timestamp', clock.instant().toEpochMilli()))); } def auditEventRequest(String topicName, JsonValue auditEvent) { return newCreateRequest(resourcePath("/" + topicName), auditEvent); } def accessTokenInfo() { def accessTokenInfo = contexts.oauth2.accessToken; return object(field('scopes', accessTokenInfo.scopes as List), field('expiresAt', accessTokenInfo.expiresAt), field('sub', accessTokenInfo.info.sub)); } def resourceEvent() { return object(field('path', request.uri.path), field('method', request.method)); } // -------------------------------------------- // Build the event JsonValue auditEvent = auditEvent('OAuth2AccessEvent') .add('accessToken', accessTokenInfo()) .add('resource', resourceEvent()); // Send the event auditService.handleCreate(context, auditEventRequest("OAuth2AccessTopic", auditEvent)); // Continue onto the next filter return next.handle(context, request)
The script generates audit events named
OAuth2AccessEvent
, on a topic namedOAuth2AccessTopic
. The events conform to the topic schema.Set an environment variable for the IG agent password, and then restart IG:
$
export AGENT_SECRET_ID='cGFzc3dvcmQ='
The password is retrieved by a SystemAndEnvSecretStore, and must be base64-encoded.
Add the following route to IG:
$HOME/.openig/config/routes/30-custom.json
%appdata%\OpenIG\config\routes\30-custom.json
{ "name": "30-custom", "baseURI": "http://app.example.com:8081", "condition": "${matches(request.uri.path, '^/rs-introspect-audit')}", "heap": [ { "name": "AuditService-1", "type": "AuditService", "config": { "config": {}, "eventHandlers": [ { "class": "org.forgerock.audit.handlers.json.stdout.JsonStdoutAuditEventHandler", "config": { "name": "jsonstdout", "elasticsearchCompatible": false, "topics": [ "OAuth2AccessTopic" ] } } ] } }, { "name": "SystemAndEnvSecretStore-1", "type": "SystemAndEnvSecretStore" }, { "name": "AmService-1", "type": "AmService", "config": { "agent": { "username": "ig_agent", "passwordSecretId": "agent.secret.id" }, "secretsProvider": "SystemAndEnvSecretStore-1", "url": "http://openam.example.com:8088/openam/", "version": "7" } } ], "handler": { "type": "Chain", "config": { "filters": [ { "name": "OAuth2ResourceServerFilter-1", "type": "OAuth2ResourceServerFilter", "config": { "scopes": [ "mail", "employeenumber" ], "requireHttps": false, "realm": "OpenIG", "accessTokenResolver": { "name": "token-resolver-1", "type": "TokenIntrospectionAccessTokenResolver", "config": { "amService": "AmService-1", "providerHandler": { "type": "Chain", "config": { "filters": [ { "type": "HttpBasicAuthenticationClientFilter", "config": { "username": "ig_agent", "passwordSecretId": "agent.secret.id", "secretsProvider": "SystemAndEnvSecretStore-1" } } ], "handler": "ForgeRockClientHandler" } } } } } }, { "type": "ScriptableFilter", "config": { "type": "application/x-groovy", "file": "OAuth2Access.groovy", "args": { "auditService": "${heap['AuditService-1']}", "clock": "${heap['Clock']}" } } } ], "handler": { "type": "StaticResponseHandler", "config": { "status": 200, "headers": { "Content-Type": [ "text/html" ] }, "entity": "<html><body><h2>Decoded access_token: ${contexts.oauth2.accessToken.info}</h2></body></html>" } } } } }
Notice the following features of the route:
The route matches requests to
/rs-introspect-audit
.The
accessTokenResolver
uses the token introspection endpoint to validate the access_token.The HttpBasicAuthenticationClientFilter adds the credentials to the outgoing token introspection request.
The ScriptableFilter uses the Groovy script
OAuth2Access.groovy
to generate audit events namedOAuth2AccessEvent
, with a topic namedOAuth2AccessTopic
.The audit service publishes the custom audit event to the JsonStdoutAuditEventHandler. A single line per audit event is published to standard output.
In a terminal window, use a curl command similar to the following to retrieve an access_token:
$
mytoken=$(curl -s \ --user "client-application:password" \ --data "grant_type=password&username=george&password=C0stanza&scope=mail%20employeenumber" \ http://openam.example.com:8088/openam/oauth2/access_token | jq -r ".access_token")
Access the route, with the access_token returned in the previous step:
$
curl -v http://openig.example.com:8080/rs-introspect-audit --header "Authorization: Bearer ${mytoken}"
Information about the decoded access_token is returned.
Search the standard output for an audit message like the following example, that includes an audit event on the topic
OAuth2AccessTopic
:{ "_id": "fa2...-14", "timestamp": 155...541, "eventName": "OAuth2AccessEvent", "transactionId": "fa2...-13", "accessToken": { "scopes": ["employeenumber", "mail"], "expiresAt": 155...000, "sub": "george" }, "resource": { "path": "/rs-introspect-audit", "method": "GET" }, "source": "audit", "topic": "OAuth2AccessTopic", "level": "INFO" }