Scripted decision node API
A Scripted Decision node calls server-side JavaScript to set the outcome for the node programmatically and determine the path the authentication journey takes. The script can perform actions before setting the outcome.
Create or edit a journey decision node script directly from within in the journey editor.
When configuring the Scripted Decision node, add the two outcomes true
and false
in Outcomes.
Each outcome appears as a node endpoint that you connect to other nodes in the journey.
The outcome, which can be either a String or a CharSequence type, aren’t limited to true
and false
.
Specify a value for each possible outcome and as many outcomes as required by the journey.
Use the action
object to set the outcome.
Script bindings
In addition to the common bindings, a decision node script has access to the following specific bindings, predefined objects that PingOne Advanced Identity Cloud injects into the script execution context.
PingOne Advanced Identity Cloud has introduced a next-generation scripting engine that offers several benefits, including enhanced script bindings. The availability and usage of bindings depend on the script engine version of the script: legacy or next-generation. Both versions are described in this section. For information about migrating to the enhanced scripting engine, refer to Migrating to next-generation scripts. |
Binding | Description | Further information |
---|---|---|
|
Set the script outcome and perform script-related actions. |
|
|
Add information to the PingOne Advanced Identity Cloud audit logs. |
|
|
Request additional data from the user using a callback. |
|
|
If the user has previously authenticated and has a session, use this binding to access the properties of that session. |
|
|
Access the data stored in the user’s profile. |
|
|
Generate JWT assertions in scripts. |
|
|
Validate JWT assertions in scripts. |
|
|
Access data set by previous nodes in the journey or store data to be used by subsequent nodes. |
|
|
Access the request cookies in the login request. |
|
|
Access the HTTP headers provided in the login request. |
|
|
Access the HTTP request parameters provided in the login request. |
|
|
Boolean to indicate whether evaluation has resumed after suspension. |
For example, the value returns |
|
Access information about the SAML 2.0 request if the scripted decision node is part of a federated journey associated with an application. |
|
|
Access the secrets configured for PingOne Advanced Identity Cloud in the environment. |
(1) Not available in Legacy scripts.
Set script outcome and script attributes
Use the action
object to define the outcome and access functions to set script attributes,
such as error or lockout messages, verified identities, or session properties.
Use |
-
Next-generation
-
Legacy
Call the goTo
method directly on the action
binding. Optionally, chain with ActionWrapper
functions.
Supported ActionWrapper
functions:
Method | Information |
---|---|
|
Add a session property. Refer to Set session properties. |
|
Remove an existing session property. |
|
Set a description for this action. |
|
Set an error message to display to the end user when the journey reaches the Failure node. |
|
Set a header for this action. |
|
Set the identity, authenticated or not, of the user or agent verified to exist in an identity store. Use these methods to record the type of identified user. |
|
Set an error message to display to the end user when the account is locked or inactive. |
|
Set a stage name to return to the client to aid the rendering of the UI. |
// Evaluation continues along the "false" outcome
action.goTo("false").withErrorMessage(error);
// No effect
outcome = "true";
Import the Action
class to access the goTo
method. Optionally, chain with ActionBuilder
functions.
public static ActionBuilder goTo(String outcome)
Supported Action.ActionBuilder
functions:
Method | Information |
---|---|
|
Add a session property. Refer to Set session properties. |
|
Remove an existing session property. |
|
Set a description for this action. |
|
Set an error message to display to the end user when the journey reaches the Failure node. |
|
Set a header for this action. |
|
Set the identity, authenticated or not, of the user or agent verified to exist in an identity store. Use these methods to record the type of identified user. |
|
Set an error message to display to the end user when the account is locked or inactive. |
|
Set a stage name to return to the client to aid the rendering of the UI. The property is only sent if the script also sends callbacks. |
You can find information on all supported methods in ActionBuilder.
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action);
// Evaluation continues along the "false" outcome
action = fr.Action.goTo("false").withErrorMessage(error).build();
// No effect
outcome = "true";
A script can also use the action
object to set session properties
and callbacks.
Access request cookies
Use the requestCookies
binding to access the cookies in the login request without
having to perform string manipulation of the request header.
The requestCookies
binding stores the cookie names and values in the format Map<String, String>
.
-
Next-generation
-
Legacy
if (typeof requestCookies === "undefined") {
action.goTo("false");
}
if (Object.keys(requestCookies).length === 0) {
action.goTo("false");
}
if (requestCookies.containsKey("amlbcookie")) {
logger.debug("amlbcookie: " + requestCookies.amlbcookie);
}
action.goTo("true");
var fr = JavaImporter(
org.forgerock.openam.auth.node.api.Action);
var cookieHeader = requestHeaders.get("Cookie");
var cookies = cookieHeader.get(0).split(";");
var cookieValue = null;
cookies.forEach(
function(cookie) {
var cookieSpec = cookie.split("=");
var cookieName = cookieSpec[0].trim();
nodeState.putShared("cookieName", cookieName);
if (cookieName == "amlbcookie") {
cookieValue = cookieSpec[1].trim();
logger.message("amlbcookie: " + cookieValue);
}
}
)
if(cookieValue == null) {
action = fr.Action.goTo("false").build();
}
action = fr.Action.goTo("true").build();
Access request header
A script can access the headers in the login request with the methods of the requestHeaders
object.
The script has access to a copy of the headers. Changing their values does not affect the request.
String[] requestHeaders.get(String headerName)
-
Returns an array of the values in the named request header, or
null
, if the property is not set.Header names are case-sensitive.
-
Next-generation
-
Legacy
if (requestHeaders.get("user-agent").get(0).indexOf("Chrome") !== -1) { action.goTo("true"); } else { action.goTo("false"); }
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); if (requestHeaders.get("user-agent").get(0).indexOf("Chrome") !== -1) { action = fr.Action.goTo("true").build(); } else { action = fr.Action.goTo("false").build(); }
-
Access request parameters
A script can access the query parameters in the login request with the methods of the requestParameters
object.
The script has access to a copy of the parameters. Changing their values doesn’t affect the request.
String[] requestParameters.get(String parameterName)
-
Return an array of the values in the named request parameter, or
null
, if the parameter is not available.-
Next-generation
-
Legacy
var service; var authIndexType = requestParameters.get("authIndexType"); if (authIndexType && String(authIndexType.get(0)) === "service") { service = requestParameters.get("authIndexValue").get(0); } if (service) { nodeState.putShared("service", service); } action.goTo("true");
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); var service; var authIndexType = requestParameters.get("authIndexType"); if (authIndexType && String(authIndexType.get(0)) === "service") { service = requestParameters.get("authIndexValue").get(0); } if (service) { nodeState.putShared("service", service); } action = fr.Action.goTo("true").build();
In JavaScript, the data type (
typeof
) ofrequestParameters
values is object.Convert the values to strings before using strict equality comparisons.
-
Access shared state data
A script can access the shared state of the journey with the methods of the nodeState
object.
The You can still access the different states with the For example, use |
There are three types of state:
- Shared
-
Non-sensitive state.
- Transient
-
Sensitive state.
Transient state data is never sent back to the user’s browser in a callback so doesn’t need to be encrypted. The transient state is for sensitive data and for data not required after the next callback.
- Secure
-
Encrypted sensitive state.
Secure state data is sent back to the user’s browser encrypted as part of the shared state object.
Transient state data is promoted to secure state data when:
-
A callback to the user is about to occur.
-
A downstream node is detected in the journey, requesting data in the transient state as script input.
Unless the downstream node explicitly requests the secure state data by name, the authentication journey removes it from the node state after processing the next callback. For example, a node in a registration journey stores a user’s password in transient state. The node sends a callback to
the user before an inner tree node, downstream in the journey, consumes that password. As part of the callback, the
journey assesses what to add to the secure state. It does this by checking the state inputs that downstream nodes in the
journey require. Nodes that only request If a downstream node requires the password, it must therefore explicitly request it as state input, even if it lists the
|
<returnvalue> nodeState.get(String propertyName)
-
Returns the value of the named property.
The value may come from the transient, secure, or shared states, in that order. If the same property is available in several states, the method returns the value of the property in the transient state first.
If the property isn’t set, the method returns
null
.-
Next-generation
-
Legacy
Returns the value as an
Object
. The JavaScript return type is coerced, so you no longer need to call functions such asasString()
orasMap()
.var currentAuthLevel = nodeState.get("authLevel"); var thePassword = nodeState.get("password"); action.goTo("true");
Returns the value as a
JsonValue
. Use functions such asasString()
orasMap()
to convert to the expected type.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); var currentAuthLevel = nodeState.get("authLevel"); var thePassword = nodeState.get("password").asString(); action = fr.Action.goTo("true").build();
-
<returnvalue> nodeState.getObject(String propertyName)
-
Returns the value of the named property.
If the value is a map, this method combines and returns the values stored in transient, secure, and shared state.
If the property isn’t set, the method returns
null
.The
getObject
method returns an immutable object. If you want to update the object, usenodeState.get(String propertyName)
instead. Theget
method doesn’t combine values but returns the first value found in transient, secure, or shared state (in that order). You can then modify the returned object.For example:
var attributes = nodeState.get("objectAttributes"); if (attributes) { attributes.put("mail", "test@example.com"); nodeState.putShared("objectAttributes", attributes); } action.goTo("true");
-
Next-generation
-
Legacy
Returns the value as an
Object
. The JavaScript return type is coerced.var attributes = nodeState.getObject("objectAttributes"); action.goTo("true");
Returns the value as a
JsonValue
. Use functions such asasString()
orasMap()
to convert to the expected type.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); var attributes = nodeState.getObject("objectAttributes").asMap(); action = fr.Action.goTo("true").build();
-
<returnvalue> nodeState.putShared(String propertyName, String propertyValue)
-
Sets the value of the named shared state property.
-
Next-generation
-
Legacy
Returns a modified instance of
NodeStateScriptWrapper
.try { var thePassword = nodeState.get("password"); } catch (e) { nodeState.putShared("errorMessage", e.toString()); } action.goTo("true");
Returns a modified instance of
NodeState
.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); try { var thePassword = nodeState.get("password").asString(); } catch (e) { nodeState.putShared("errorMessage", e.toString()); } action = fr.Action.goTo("true").build();
-
<returnvalue> nodeState.putTransient(String propertyName, String propertyValue)
-
Sets the value of the named transient state property.
-
Next-generation
-
Legacy
Returns a modified instance of
NodeStateScriptWrapper
.nodeState.putTransient("sensitiveKey", "sensitiveValue"); action.goTo("true");
Returns a modified instance of
NodeState
.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); nodeState.putTransient("sensitiveKey", "sensitiveValue"); action = fr.Action.goTo("true").build();
-
<returnvalue> nodeState.mergeShared(Map<String, Object> object)
-
Adds the specified object to shared state by merging the keys. If a key already exists, in any state, its value is replaced by the specified value.
You can only merge a nested object if it’s a registered state object, such as
objectAttributes
. If you provide new values forobjectAttributes
, they’re merged with the existing values. For example, if the followingobjectAttributes
values already exist in secure state:{ "key1": "a", "key2": "b" }
and you call
mergeShared()
with this set of values:{ "key1": "z", "key3": "c"}
then the merged result contains these values when you call
getObject("objectAttributes")
:"objectAttributes": { "key1": "z", "key2": "b", "key3": "c"}
where
key1
is removed from secure and added to shared state along withkey3
, andkey2
remains in secure state.-
Next-generation
-
Legacy
Returns a modified instance of
NodeStateScriptWrapper
.// specify new values for objectAttributes as a JSON object nodeState.mergeShared({ "objectAttributes": { "mail": "bjensen@example.com", "userName": "bjensen" } }) action.goTo("true");
Returns a modified instance of
NodeState
.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); // specify new values for objectAttributes as a JSON object nodeState.mergeShared({ "objectAttributes": { "mail": "bjensen@example.com", "userName": "bjensen" } }) action = fr.Action.goTo("true").build();
-
<returnvalue> nodeState.mergeTransient(Map<String, Object> object)
-
Adds the specified object to transient state by merging the keys. If a key already exists, in any state, its value is replaced by the specified value.
You can only merge a nested object if it’s a registered state object, such as
objectAttributes
. If you provide new values forobjectAttributes
, they’re merged with the existing values. For example, if the followingobjectAttributes
values already exist in secure state:{ "key1": "a", "key2": "b" }
and you call
mergeTransient()
with this set of values:{ "key1": "z", "key3": "c"}
then the merged result contains these values when you call
getObject("objectAttributes")
:"objectAttributes": { "key1": "z", "key2": "b", "key3": "c"}
where
key1
is removed from secure and added to transient state along withkey3
, andkey2
remains in secure state.-
Next-generation
-
Legacy
Returns a modified instance of
NodeStateScriptWrapper
.// specify new values for objectAttributes as a JSON object nodeState.mergeTransient({ "objectAttributes": { "mail": "bjensen@example.com", "userName": "bjensen" } }) action.goTo("true");
Returns a modified instance of
NodeState
.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); // specify new values for objectAttributes as a JSON object nodeState.mergeTransient({ "objectAttributes": { "mail": "bjensen@example.com", "userName": "bjensen" } }) action = fr.Action.goTo("true").build();
-
Access profile data
A script can access profile data through the methods of the idRepository
object.
In these examples, an Identity Store Decision node with Username as Universal Identifier enabled
precedes the Scripted Decision node that uses the script. As a result, when you call
|
- Get attribute values
-
Returns the values of the named attribute for the named user.
-
Next-generation
-
Legacy
// Get UUID by enabling `Username as Universal Identifier` // in a preceding Identity Store Decision node var uuid = nodeState.get("username"); var attribute = "mail"; // Get identity object separately var identity = idRepository.getIdentity(uuid); // Returns all values as an array, for example: ["test@example.com", "user@example.com"] identity.getAttributeValues(attribute); // Returns the first value, for example: test@example.com identity.getAttributeValues(attribute)[0]; // If no attribute by this name is found, the result is an empty array: [] identity.getAttributeValues("non-existent-attribute"); action.goTo("true");
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); // Get UUID by enabling `Username as Universal Identifier` // in a preceding Identity Store Decision node var uuid = nodeState.get("username").asString(); var attribute = "mail"; // Returns all values, for example: [test@example.com, user@example.com] idRepository.getAttribute(uuid, attribute).toString(); // Returns the first value, for example: test@example.com idRepository.getAttribute(uuid, attribute).iterator().next(); // Returns a value at the specified index, for example: user@example.com idRepository.getAttribute(uuid, attribute).toArray()[1]; // If no attribute by this name is found, the result is an empty array: [] idRepository.getAttribute(uuid, "non-existent-attribute").toString(); action = fr.Action.goTo("true").build();
-
- Set attribute values
-
Sets the named attribute as specified by the attribute value for the named user, and persists the result in the user’s profile.
-
Next-generation
-
Legacy
// Get UUID by enabling `Username as Universal Identifier` // in a preceding Identity Store Decision node var uuid = nodeState.get("username"); // Get identity separately var identity = idRepository.getIdentity(uuid); // Set the attribute directly on the identity object identity.setAttribute("mail", ["test@example.com"]); try { // Explicitly persist data // throws an exception if setAttribute failed identity.store(); action.goTo("true"); } catch(e) { logger.error("Unable to persist attribute. " + e); action.goTo("false"); }
You must explicitly call
store()
to persist changes to attribute values.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); // Get UUID by enabling `Username as Universal Identifier` // in a preceding Identity Store Decision node var uuid = nodeState.get("username").asString(); // Set attribute using the idRepository object idRepository.setAttribute(uuid, "mail", ["test@example.com"]); try { // check setAttribute was successful var mail = idRepository.getAttribute(uuid, "mail").iterator().next(); action = fr.Action.goTo("true").build(); } catch(e) { logger.error("Unable to persist attribute. " + e); action = fr.Action.goTo("false").build(); }
-
- Add attribute values
-
Add an attribute value to the list of attribute values associated with the attribute name for a particular user.
-
Next-generation
-
Legacy
// Get UUID by enabling `Username as Universal Identifier` // in a preceding Identity Store Decision node var uuid = nodeState.get("username"); // Get identity separately var identity = idRepository.getIdentity(uuid); // Add a value as a string. identity.addAttribute("mail", "user@example.com"); try { // Explicitly persist data // throws an exception if addAttribute failed identity.store(); action.goTo("true"); } catch(e) { logger.error("Unable to persist attribute. " + e); action.goTo("false"); }
You must explicitly call
store()
to persist changes to attribute values.var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); // Get UUID by enabling `Username as Universal Identifier` // in a preceding Identity Store Decision node var uuid = nodeState.get("username").asString(); var attr = "mail"; // get number of existing attribute values var mail = idRepository.getAttribute(uuid, attr).toArray(); // Add attribute using the idRepository object idRepository.addAttribute(uuid, attr, ["test@example.com"]); // check addAttribute was successful var updatedmail = idRepository.getAttribute(uuid, attr).toArray(); if (updatedmail.length > mail.length) { action = fr.Action.goTo("true").build(); } else { logger.error("Unable to add attribute."); action = fr.Action.goTo("false").build(); }
-
Set session properties
A script can set session properties with the fields and methods of the Action interface.
The following example sets the outcome to true
, and adds a custom session property:
-
Next-generation
-
Legacy
action.goTo("true").putSessionProperty("mySessionProperty", "myPropertyValue");
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action);
action = fr.Action.goTo("true").putSessionProperty("mySessionProperty", "myPropertyValue").build();
When adding a session property, make sure it’s in the PingOne Advanced Identity Cloud allowlist. PingOne Advanced Identity Cloud does not allow the script to add it to sessions unless it is in the allowlist.
Learn more in Session Property Whitelist Service. |
Add the script to a Scripted Decision node in your authentication journey. When a user authenticates successfully, PingOne Advanced Identity Cloud adds the property to their session, as shown in the following output for session introspection:
{
"username": "014c54bd-6078-4639-8316-8ce0e7746fa4",
"universalId": "id=014c54bd-6078-4639-8316-8ce0e7746fa4,ou=user,o=alpha,ou=services,ou=am-config",
"realm": "/alpha",
"latestAccessTime": "2022-10-31T11:03:25Z",
"maxIdleExpirationTime": "2022-10-31T11:33:25Z",
"maxSessionExpirationTime": "2022-10-31T13:03:24Z",
"properties": {
"AMCtxId": "de5abe95-db97-4354-9d32-aab660ea23a3-4252446",
"mySessionProperty": "myPropertyValue"
}
}
Query SAML application and authentication request
The samlApplication
binding is only present when a journey runs in a SAML 2.0 context and an application journey is configured. You can access the binding object for the duration of the authentication session
(determined by the Max duration setting.
Use the samlApplication
binding to query the SAML 2.0 authentication request properties and
identity provider (IDP) and service provider (SP) configuration attributes.
You can check and debug the configuration values, but changes to properties are confined to the scope of the script and won’t affect the underlying objects.
- String getFlowInitiator()
-
Returns
IDP
orSP
depending on which provider initiates the SAML 2.0 flow. - Map<String, Object> getAuthnRequest()
-
Returns an object containing the properties of the SAML 2.0 authentication request.
Example
authnRequest
objectThe following example is formatted to display the object structure:
{ "destination": "https://<tenant-env-fqdn>/am/SSORedirect/metaAlias/idp1", "signature": null, "subject": null, "issueInstant": 1724341924000, "consent": "", "forceAuthn": false, "protocolBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "mutable": false, "issuer": { "value": "sp1", "nameQualifier": "", "format": "", "mutable": false, "spnameQualifier": "", "spprovidedID": "" }, "assertionConsumerServiceURL": "https://<tenant-env-sp-fqdn>/am/Consumer/metaAlias/sp1", "@class": "com.sun.identity.saml2.protocol.impl.AuthnRequestImpl", "extensions": null, "passive": false, "version": "2.0", "requestedAuthnContext": { "@class": "com.sun.identity.saml2.protocol.impl.RequestedAuthnContextImpl", "elementName": "RequestedAuthnContext", "mutable": false, "authnContextClassRef": [ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" ], "authnContextDeclRef": [], "comparison": "exact" }, "nameIDPolicy": { "@class": "com.sun.identity.saml2.protocol.impl.NameIDPolicyImpl", "format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "allowCreate": true, "mutable": false, "spnameQualifier": "sp1" }, "attributeConsumingServiceIndex": null, "conditions": null, "scoping": null, "signed": false, "id": "s2c72252b5b2ce2b43e5150c8ee8aba0401f3ef390", "providerName": "", "assertionConsumerServiceIndex": null }
- Map<String, List<String>> getIdpAttributes()
-
Returns a map containing the extended configuration for the hosted IDP.
Example IDP attributes
IDP attribute Example value assertionEffectiveTime
[ "600" ]
idpAuthncontextClassrefMapping
[ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport|0||default" ]
assertionNotBeforeTimeSkew
[ "600" ]
metaAlias
[ "/idp1" ]
idpECPSessionMapper
[ "com.sun.identity.saml2.plugins.DefaultIDPECPSessionMapper" ]
idpAccountMapper
[ "com.sun.identity.saml2.plugins.DefaultIDPAccountMapper" ]
nameIDFormatMap
[ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress|mail" ]
idpAttributeMapper
[ "com.sun.identity.saml2.plugins.DefaultIDPAttributeMapper" ]
saeIDPUrl
[ "https://<tenant-env-fqdn>/am/idpsaehandler/metaAlias/idp1" ]]
idpAuthncontextMapper
[ "com.sun.identity.saml2.plugins.DefaultIDPAuthnContextMapper" ]
cotlist
[ "cot1" ]
- Map<String, List<String>> getSpAttributes()
-
Returns a map containing the extended configuration for the remote SP.
Example SP attributes
SP attribute Example value metaAlias
[ ]
treeName
[ "test" ]
idpProxyCount
[ "0" ]
cotlist
[ "cot1" ]
- Example
-
The following script calls the
samlApplication
methods to log the authentication request and configuration values:-
Next-generation
-
Legacy
function logObject(objType, obj) { var logMsg = objType + " \n"; for (attr in obj) { logMsg += "\t" + attr + " = " + obj[attr] + "\n"; } logger.info(logMsg); } if (typeof (samlApplication) === "undefined") { logger.error("Journey is not associated with a SAML application"); action.goTo("false"); } else { var flowType = samlApplication.getFlowInitiator(); logger.info("Flow type: " + flowType); if (flowType == 'SP') { var authnRequest = samlApplication.getAuthnRequest(); logObject("authnRequest", authnRequest); } logObject("idpAttributes", samlApplication.getIdpAttributes()); logObject("spAttributes", samlApplication.getSpAttributes()); action.goTo("true"); }
Not available in Legacy bindings.
-
Existing session properties
The existingSession
binding is only present when performing a session upgrade.
Any properties added by nodes earlier in the journey only appear on the user’s new session when the journey completes;
such properties aren’t available in the existingSession
binding.
A script can access existing session properties during a session upgrade request with the existingSession.get
method.
String existingSession.get(String propertyName)
-
Returns the string value of the named existing session property, or
null
, if the property is not set.If the current request isn’t a session upgrade and doesn’t provide an existing session, the
existingSession
variable isn’t declared. Check for a declaration before attempting to access the variable.-
Next-generation
-
Legacy
if (typeof existingSession !== 'undefined') { var existingAuthLevel = existingSession.get("AuthLevel") } else { nodeState.putShared("errorMessage", "Variable existingSession not declared - not a session upgrade.") } action.goTo("true");
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action); if (typeof existingSession !== 'undefined') { var existingAuthLevel = existingSession.get("AuthLevel") } else { nodeState.putShared("errorMessage", "Variable existingSession not declared - not a session upgrade.") } action = fr.Action.goTo("true").build();
-
Customize account lockout message
A script can display a custom error message to the end user when their account is locked or inactive. To do this, use a script with the .withLockoutMessage
method on the Action.goto()
object.
The following example changes the account lockout message to a custom error message:
-
Next-generation
-
Legacy
action.goTo("true")
.withLockoutMessage("Custom lockout message");
var fr = JavaImporter(org.forgerock.openam.auth.node.api.Action);
action = fr.Action.goTo("true")
.withLockoutMessage("Custom lockout message")
.build();
Add the script to a Scripted Decision node in your authentication journey.
This Scripted Decision node must go after a node that checks the status of the user (such as the Identity Store Decision node or the Account Active Decision node) because it is triggered when a user’s account is locked or inactive. |
Generate and validate JWTs
Use the jwtAssertion
and jwtValidator
bindings to include JWT operations in your script.
The jwtAssertion
binding lets you generate a signed and/or encrypted JWT, for example,
for securely transporting JWT claims that can be verified later in the journey via an emailed magic link. JWTs are signed using the HS256 or RS256 algorithms.
The jwtValidator
binding lets you decrypt and validate JWTs; for example,
to verify JWT claims from another system such as PingGateway.
String jwtAssertion.generateJwt(Map<String, Object> jwtData)
-
Provide the following data for generating a JWT assertion:
Data field Description jwtType
(Required)
The type of JWT to generate, one of:
-
SIGNED
-
SIGNED_THEN_ENCRYPTED
-
ENCRYPTED_THEN_SIGNED
Encrypted options are only supported for HS256 JWTs.
jwsAlgorithm
(Required)
HS256
orRS256
(jwtType
must beSIGNED
).issuer
The JWT issuer (
iss
) claim. You must provide a value for eitherissuer
orstableId
at a minimum.audience
The JWT audience (
aud
) claim.subject
The JWT subject (
sub
) claim.type
The JWT type (
typ
) claim, typicallyJWT
.jwtId
Specify a value for the JWT ID (
jti
).claims
Custom claims in the format
"key":"value"
.stableId
Stable ID. You must provide a value for either
issuer
orstableId
at a minimum.accountId
Deprecated. To support backwards compatibility, the account ID (if specified) is set as the
issuer
,stableId
andsubject
when these values aren’t provided.validityMinutes
The duration of the validity of the JWT in minutes.
privateKey
The RSA private key in JSON Web Key (JWK) format. For example, generate your own private key with https://www.openssl.org.
Required for
RS256
-signed JWTs.signingKey
The base64-encoded JSON Web Key used to sign/verify the JWT.
Required for
HS256
-signed JWTS.encryptionKey
The JSON Web Encryption (JWE) public key used to encrypt/decrypt the JWT. JWTs are encrypted using the A128CBC-HS256 algorithm.
Generation of RS256-encrypted signed JWT assertions isn’t currently supported.
-
Next-generation
-
Legacy
JWT generation example 1: HS256 encrypted then signed:
var hmacJwtData = { jwtType: "ENCRYPTED_THEN_SIGNED", jwsAlgorithm: "HS256", issuer: "issuer", subject: "subject", audience: "https://am.localtest.me:8080/am/oauth2/access_token", type: "JWT", validityMinutes: 10, claims: { "key1": "value1", "key2": "value2" }, signingKey: "cGFzc3dvcmQ=", encryptionKey: "Syz1K5XQCZtq7FkE+GNvgZPeFyvUXJdemIW7CQjM18U=" }; // returns a string representation of the JWT var assertionJwt = jwtAssertion.generateJwt(hmacJwtData); if (assertionJwt !== null && assertionJwt.length > 0) { logger.debug("AssertionJwt: " + assertionJwt); // store JWT in nodeState for validation later in journey nodeState.putShared("assertionJwt" , assertionJwt); action.goTo("true"); } else { action.goTo("false"); }
JWT generation example 2: RS256 signed:
var rsaJwtData = { jwtType: "SIGNED", jwsAlgorithm: "RS256", accountId: "accountId", audience: "https://am.localtest.me:8080/am/oauth2/access_token", validityMinutes: 10, privateKey: { "p": "0BRJ6TrTpeT3XM1aXj9ZiTDfVTD0Aqufhm0a2Cm7Zr3ObqkBbZrm2KH9BY23nfY_TFbd6kx31YHqjpoV4KeThn2uvZ7gPw_ILljk5WQwgFq_gdDvAq3Iw4MlwGoR51nSaGGqKU6Dt2PvVOB4I3azVJr1f9Vsm47L6Llp8YjKtBE", "kty": "RSA", "q": "ugtiWmODuy3XuWGf3u5hvF4RglZZK5IPYRkJjSz8j90DxYVPD_CBNJx8j_FGUJ6BBNRTs2yylgMJdcFs-WxcFR7iQD2OzgzSdeRgrh34RmLjAXEq-X0OF9P3lqnBXIx-uVL3-rG8jQWSzc016o3PWjclOKqLx7oBmYs7w6WbJLE", "d": "c2DR3SOQkzu6f5eRLFLURphQCbrN4JCAlOo2S_D07UJmMiYtIFpOezbLazQYXdebiV-pPv-zZcOWHhOr3HMgYLu8JBN29mrJS8kDIWMjCx8vMJgrNLfBZO4az--t_Kyow4p0n3HdaYRu0K4lqskXe68Syl0XAlHfnd7q-bB-UUGB89j3E-FfXIIjKktn_koBc8hX2DhUnJgduFi9CcAQVO9wSjjfyB6ksn01_YaMt5MHsHKhqDvZeCdEHfwyFer8vvDRCTru_msl_fu_MqQi15igTJu6f7eBsVQQLnb7L1fQ0BURck7oVMjTvyYz_nRbnMkKSrWpa-j1d1Z4TTucAQ", "e": "AQAB", "use": "sig", "kid": "iUE-em7bU5j1yJN6hEMKx6ZmtWBYGZycCuO94X1sssE", "qi": "rUqLFjJ2L3FNrj65tdUCusQ-_7g1rKsTOGnQUrVcgYSsHb3aYR90zV651MiL0X4gp6mzgc8QcSdzc1KbEmR5VHm5IH4N6f9yBNb2yO_8sftmS8PiRjZjVLUORnwmouJ4cPsob0RPx9mwGLIURLxQDstE4UQ0j6iMWF74iezwxO8", "dp": "f60DURXkijV9RrdGjPAIK3MOhoJ8JytRvjUyNJMex0MN6L7Q_oT-wsxaqc60bTuMyXW_wyVanmqSFyAa7ndEwVBbKUTUSj2P0kh_YvXgANIuEiS2k4k42CafwnoTNEbcIWpT8_aWQbATSZxWe0Q5c1-F5gN6GdU77zfd9vO9lVE", "alg": "RS256", "dq": "A8RdPnVLYovgFVnbsdjj07uX4Sq8bXxsoUuvfNNPXd5cyDAV1L3K7_THNObuxI0hEab29ugZiZ4QH_lFqps-FhNlA2X7sUJjNI3mQ0BKGarA6_ONqjWVBnh9R-iyCJyzqC785G-a4MQfH9mq4M_0ReBd-ZLCd83VYHWIRULiLFE", "n": "lzf-g94TfT9JeQ7kRZCkSOKwP0rtpe2IzX6C42NN1EE4hejwCjIAj4oUHrcNyrcVZ6hIu3a5r09fnzp5w9RS6ooMkp-AQ_9C5T8hFbf61J4lT3kmIp-jv50MF7oJCGOidLPCiJ-3EsnBKFjomRcCQa_r-sHF9hfORvhypBcj_U1OMMRM28yX7JVggG7f_YPlmx03c7meKPE-4xXnfdFDxbjbntlWSul6qm0YOTRVfuZHubwu-HOZEDB4dKkA3-0qAev6FULW7tNOqoMj5KLlejeiuCOQeAMTqPnBqOdH8iC4u6WHArsRqkmOdLgVs0uLlVPz9796qIKlosP5EMPjwQ" } }; var assertionJwt = jwtAssertion.generateJwt(rsaJwtData); if (assertionJwt !== null && assertionJwt.length > 0) { logger.debug("AssertionJwt: " + assertionJwt); // store JWT in nodeState for validation later in journey nodeState.putShared("assertionJwt" , assertionJwt); action.goTo("true"); } else { action.goTo("false"); }
Not available in Legacy bindings
-
Map<String, Object> jwtValidator.validateJwtClaims(Map<String, Object> jwtData)
-
Provide values for the following supported fields to validate the specified JWT assertion:
Data field Description jwtType
(Required) The type of JWT to validate:
SIGNED
,SIGNED_THEN_ENCRYPTED
, orENCRYPTED_THEN_SIGNED
.jwt
(Required) The JWT object to validate.
issuer
The JWT issuer (
iss
) claim. You must provide a value for eitherissuer
orstableId
at a minimum.audience
The JWT audience (
aud
) claim value.subject
The JWT subject (
sub
) claim.type
The JWT type (
typ
) claim.claims
Custom claims in the format
"key":"value"
.stableId
Stable ID. You must provide a value for either
issuer
orstableId
at a minimum.accountId
Deprecated. To support backwards compatibility, the account ID (if specified) is used to validate the
issuer
,stableId
andsubject
when these values aren’t provided.signingKey
The base64-encoded JSON Web Key used to sign/verify the JWT.
JWTS are signed using the HS256 or RS256 algorithms.
encryptionKey
The JSON Web Encryption (JWE) public key used to encrypt/decrypt the JWT.
JWTs are encrypted using the A128CBC-HS256 algorithm.
Validation of RS256-encrypted signed JWT assertions isn’t currently supported.
-
Next-generation
-
Legacy
JWT validation example 1: HS256 encrypted then signed:
var jwtData = { jwtType: "ENCRYPTED_THEN_SIGNED", issuer: "issuer", subject: "subject", audience: "https://am.localtest.me:8080/am/oauth2/access_token", type: "JWT", signingKey: "cGFzc3dvcmQ=", encryptionKey: "Syz1K5XQCZtq7FkE+GNvgZPeFyvUXJdemIW7CQjM18U=" }; // get JWT from nodeState (JWT generation example 1) var assertionJwt = nodeState.get("assertionJwt"); if (assertionJwt !== null && assertionJwt.length > 0) { jwtData["jwt"] = assertionJwt; try { // returns a map of JWT claims or null if required claims are missing // throws NoSuchSecretException if verification key is missing var jwtClaims = jwtValidator.validateJwtClaims(jwtData); if (jwtClaims !== null) { // retrieve and log some JWT claim values logger.debug("Audience: " + jwtClaims.get("audience")); logger.debug("Subject: " + jwtClaims.get("subject")); logger.debug("Expiration Time: " + jwtClaims.get("expirationTime")); logger.debug("Issued At: " + jwtClaims.get("issuedAt")); logger.debug("JWT ID: " + jwtClaims.get("jwtId")); logger.debug("key1: " + jwtClaims.get("key1")); // custom claim action.goTo("true"); } else { logger.error("Invalid JWT claims"); action.goTo("false"); } } catch(e) { logger.error("Invalid JWT signing key"); action.goTo("false"); } } else { logger.error("Error getting assertionJwt"); action.goTo("false"); }
JWT validation example 2: RS256 signed:
var jwtData = { jwtType: "SIGNED", accountId: "accountId", audience: "https://am.localtest.me:8080/am/oauth2/access_token", signingKey: "cGFzc3dvcmQ=" }; // get JWT from nodeState (JWT generation example 2) var assertionJwt = nodeState.get("assertionJwt"); if (assertionJwt !== null && assertionJwt.length > 0) { jwtData["jwt"] = assertionJwt; try { // returns a map of JWT claims or null if required claims are missing // throws NoSuchSecretException if verification key is missing var jwtClaims = jwtValidator.validateJwtClaims(jwtData); if (jwtClaims !== null) { // retrieve and log JWT claim values logger.debug("Audience: " + jwtClaims.get("audience")); action.goTo("true"); } else { logger.error("Invalid JWT claims"); action.goTo("false"); } } catch(e) { logger.error("Invalid JWT signing key"); action.goTo("false"); } } else { logger.error("Error getting assertionJwt"); action.goTo("false"); }
Not available in Legacy bindings
-
Use callbacks
A script can use callbacks to provide or prompt for additional information during the authentication process.
The following script checks for a password and stores the username to shared state for use by subsequent nodes in the authentication journey.
-
Next-generation
-
Legacy
Next-generation callback functionality is split between two bindings:
-
callbacksBuilder
: Request callbacks. Callbacks are sent automatically when the script completes.callbacksBuilder
request methods-
public void textOutputCallback(int messageType, String message)
-
public void suspendedTextOutputCallback(int messageType, String message)
-
public void hiddenValueCallback(String id, String value)
-
public void choiceCallback(String prompt, String[] choices, int defaultChoice, boolean multipleSelectionsAllowed)
-
public void nameCallback(String prompt)
-
public void nameCallback(String prompt, String defaultName)
-
public void passwordCallback(String prompt, boolean echoOn)
-
public void textInputCallback(String prompt)
-
public void textInputCallback(String prompt, String defaultText)
-
public void scriptTextOutputCallback(String message)
-
public void redirectCallback(String redirectUrl, Map redirectData, String method)
-
public void redirectCallback(String redirectUrl, Map redirectData, String method, String statusParameter, String redirectBackUrlCookie)
-
public void metadataCallback(Object outputValue)
-
public void stringAttributeInputCallback(String name, String prompt, String value, Boolean required)
-
public void stringAttributeInputCallback(String name, String prompt, String value, Boolean required, List<String> failedPolicies)
-
public void stringAttributeInputCallback(String name, String prompt, String value, Boolean required, Object policies, Boolean validateOnly)
-
public void stringAttributeInputCallback(String name, String prompt, String value, Boolean required, Object policies, Boolean validateOnly, List<String> failedPolicies)
-
public void numberAttributeInputCallback(String name, String prompt, Double value, Boolean required)
-
public void numberAttributeInputCallback(String name, String prompt, Double value, Boolean required, List<String> failedPolicies)
-
public void numberAttributeInputCallback(String name, String prompt, Double value, Boolean required, Object policies, Boolean validateOnly)
-
public void numberAttributeInputCallback(String name, String prompt, Double value, Boolean required, Object policies, Boolean validateOnly, List<String> failedPolicies)
-
public void booleanAttributeInputCallback(String name, String prompt, Boolean value, Boolean required)
-
public void booleanAttributeInputCallback(String name, String prompt, Boolean value, Boolean required, List<String> failedPolicies)
-
public void booleanAttributeInputCallback(String name, String prompt, Boolean value, Boolean required, Object policies, Boolean validateOnly)
-
public void booleanAttributeInputCallback(String name, String prompt, Boolean value, Boolean required, Object policies, Boolean validateOnly, List<String> failedPolicies)
-
public void confirmationCallback(int messageType, int optionType, int defaultOption)
-
public void confirmationCallback(int messageType, String[] options, int defaultOption)
-
public void confirmationCallback(String prompt, int messageType, int optionType, int defaultOption)
-
public void confirmationCallback(String prompt, int messageType, String[] options, int defaultOption)
-
public void languageCallback(String language, String country)
-
public void idPCallback(String provider, String clientId, String redirectUri, List<String> scope, String nonce, String request, String requestUri, List<String> acrValues, boolean requestNativeAppForUserInfo)
-
public void idPCallback(String provider, String clientId, String redirectUri, List<String> scope, String nonce, String request, String requestUri, List<String> acrValues, boolean requestNativeAppForUserInfo, String token, String tokenType)
-
public void pollingWaitCallback(String waitTime, String message)
-
public void validatedPasswordCallback(String prompt, boolean echoOn, Object policies, Boolean validateOnly)
-
public void validatedPasswordCallback(String prompt, boolean echoOn, Object policies, Boolean validateOnly, List<String> failedPolicies)
-
public void validatedUsernameCallback(String prompt, Object policies, Boolean validateOnly)
-
public void validatedUsernameCallback(String prompt, Object policies, Boolean validateOnly, List<String> failedPolicies)
-
public void httpCallback(String authorizationHeader, String negotiationHeader, String errorCode)
-
public void httpCallback(String authRHeader, String negoName, String negoValue, int errorCode)
-
public void x509CertificateCallback(String prompt)
-
public void x509CertificateCallback(String prompt, X509Certificate certificate)
-
public void x509CertificateCallback(String prompt, X509Certificate certificate, boolean requestSignature)
-
public void consentMappingCallback(Object config, String message, Boolean isRequired)
-
public void consentMappingCallback(String name, String displayName, String icon, String accessLevel, List<String> titles, String message, Boolean isRequired)
-
public void deviceProfileCallback(Boolean metadata, Boolean location, String message)
-
public void kbaCreateCallback(String prompt, List<String> predefinedQuestions, boolean allowUserDefinedQuestions)
-
public void selectIdPCallback(Object providers)
-
public void termsAndConditionsCallback(String version, String terms, String createDate)
-
-
callbacks
: UseisEmpty()
to check if callbacks have returned to PingOne Advanced Identity Cloud. Use publicget
methods to retrieve input from interactive and backchannel callbacks.callbacks
publicget
methodsMethod Return type Description getBooleanAttributeInputCallbacks()
List<Boolean>
The collected Boolean value
getChoiceCallbacks()
List<int[]>
The selected choices as indexes
getConfirmationCallbacks()
List<Integer>
The selected confirmation option
getConsentMappingCallbacks()
List<Boolean>
The collected Boolean value
getDeviceProfileCallbacks()
List<String>
The device information in escaped JSON format
getHiddenValueCallbacks()
Map<String, String>
The map of hiddenValueCallbacks
getHttpCallbacks()
List<String>
The authorization token
getIdpCallbacks()
List<Map<String, Object>>
A map of IDP callback data. Values are:
-
nodeName
(String) -
provider
(String) -
clientId
(String) -
redirectUri
(String) -
scope
(List<String>) -
nonce
(String) -
request
(String) -
requestUri
(String) -
acrValues
(List<String>) -
token
(String) -
tokenType
(String) -
userInfo
(String) -
requestNativeAppForUserInfo
(boolean)
getKbaCreateCallbacks()
List<Map<String, String>>
A map of knowledge-based authentication (KBA) responses. Values are
selectedQuestion
andselectedAnswer
.getLanguageCallbacks()
List<String>
The selected locale
getNameCallbacks()
List<String>
The collected name value
getNumberAttributeInputCallbacks()
List<Double>
The collected numeric value
getPasswordCallbacks()
List<String>
The collected password value
getSelectIdPCallbacks()
List<Map<String, Object>>
A map of selected IDP providers in JSON format
getStringAttributeInputCallbacks()
List<String>
The collected String value
getTermsAndConditionsCallbacks()
List<Boolean>
The Boolean acceptance value
getTextInputCallbacks()
List<String>
The collected text as a String value (can be null)
getValidatedPasswordCallbacks()
List<Map<String, Object>>
A map with the password returned as
value
(String) andvalidateOnly
(whether to only validate input, or validate and continue journey)getValidatedUsernameCallbacks()
List<Map<String, Object>>
A map with the username returned as
value
(String) andvalidateOnly
(whether to only validate input, or validate and continue journey)getX509CertificateCallbacks()
List<Map<String, Object>>
A map of X.509 certificate callback data. Values are:
-
certificate
(X509Certificate) -
signature
(String) -
reqSignature
(Boolean)
-
The following examples show how to use the callbacksBuilder
and callbacks
objects to request and receive input from callbacks:
Example nameCallback and passwordCallback
if (callbacks.isEmpty()) {
// Request callbacks
callbacksBuilder.nameCallback("User Name", "User Name");
callbacksBuilder.passwordCallback("Password", false);
} else {
// Callbacks returned from browser, save username and password
var username = callbacks.getNameCallbacks().get(0);
var password = callbacks.getPasswordCallbacks().get(0);
nodeState.putShared("username", username);
if (password === null || !password) {
action.goTo("false");
} else {
nodeState.putTransient("password", password);
action.goTo("true");
}
}
Example metadataCallback and textOutputCallback
var data = {
mfaType: "email"
};
// Get UUID by enabling `Username as Universal Identifier`
// in a preceding Identity Store Decision node
var uuid = nodeState.get("username");
var identity = idRepository.getIdentity(uuid);
var firstName = identity.getAttributeValues('givenName')[0];
if (callbacks.isEmpty()) {
// Adds the key-value pair to the JSON response
callbacksBuilder.metadataCallback(data);
// Displays a message to the end user
// Message type: INFORMATION (0), WARNING(1), or ERROR (2)
callbacksBuilder.textOutputCallback(1, "Hi " + firstName + ", please update your postal code");
}
action.goTo("true");
Example choiceCallback
var titles = ["Mr", "Mrs", "Ms", "Mx", "Other"];
if (callbacks.isEmpty()) {
// Request choice from the user
callbacksBuilder.choiceCallback("Select a title", titles, 0, false);
} else {
// Callbacks returned from browser, save selected choice
var index = callbacks.getChoiceCallbacks().get(0)[0];
var title = titles[index];
if (title === null || !title) {
action.goTo("false");
} else {
nodeState.putShared("title", title);
action.goTo("true");
}
}
Example hiddenValueCallback and scriptTextOutputCallback
var script = "document.getElementById('clientScriptOutputData').value = navigator.language \n" +
"document.getElementById('loginButton_0').click()";
try {
if (callbacks.isEmpty()) {
callbacksBuilder.hiddenValueCallback("clientScriptOutputData", "false");
callbacksBuilder.scriptTextOutputCallback(script);
} else {
//Browser language can be used to localize custom callback messages
var lang = callbacks.getHiddenValueCallbacks().get("clientScriptOutputData");
nodeState.putShared("preferredLanguage", lang);
action.goTo("true");
}
action.goTo("true");
} catch (error) {
action.goTo("false");
logger.error("Error getting browser language: " + error.toString());
}
var fr = JavaImporter(
org.forgerock.openam.auth.node.api.Action,
javax.security.auth.callback.NameCallback,
javax.security.auth.callback.PasswordCallback,
java.lang.String
);
if (callbacks.isEmpty()) {
// Request callbacks
action = fr.Action.send(
new fr.NameCallback("User Name"),
new fr.PasswordCallback("Password", false)).build();
} else {
// Callbacks returned with credentials
var username = fr.String(callbacks.get(0).getName());
var password = fr.String(callbacks.get(1).getPassword());
sharedState.put("username", username);
if (password === null || !password) {
action = fr.Action.goTo("false").build();
} else {
transientState.put("password", password);
action = fr.Action.goTo("true").build();
}
}
You can find a list of supported callbacks in Supported callbacks.
Suspend and resume journeys
You can use the action
object to suspend an authentication journey and send
a message to display to the user. Evaluate the resumedFromSuspend
script binding to determine whether to continue the journey.
ActionWrapper suspend(String message)
-
Suspend the current authentication session and send a message to display to the user.
ActionWrapper suspend(String message, SuspensionLogic logic)
-
Suspend the current authentication session and send a message to display to the user. This method also accepts a callable function that references the resume URI at the point of suspension.
-
Next-generation
-
Legacy
// Use the resumedFromSuspend object to check if the journey has resumed after suspension
// For example, it returns true after sending an email and receiving a response
if (resumedFromSuspend) {
action.goTo("true");
} else {
// Send a message to display to the user and the URL to resume the journey
action.suspend('Check your email for a magic sign-in link.', (resumeUri) => {
requestOptions = {
"method": "POST"
"body": {
"resumeUri": resumeUri
}
}
// Use internal mail server to send email
httpClient.send("https://my.email.service/sendEmail", requestOptions));
}
}
Not supported for Legacy bindings.
Add information to authentication audit log entries
A script can add information to audit log entries with the auditEntryDetail
variable.
PingOne Advanced Identity Cloud appends the value of the variable to the authentication audit logs, which
corresponds to the am-authentication
log source.
The auditEntryDetail
variable can hold either a string or a JSON object:
-
To add a string to authentication audit log entries:
auditEntryDetail = "Additional audit information"
-
To add a JSON object to authentication audit log entries:
auditEntryDetail = { "transactionStatus": "Success" }
Ping Identity recommends that you don’t put spaces in the keys of JSON objects as it prevents you from using a query filter to search additional audit information. For example, the following JSON would not be searchable:
auditEntryDetail = { "Transaction Status": "Success" }
PingOne Advanced Identity Cloud adds the value of auditEntryDetail
to the
payload > entries > info > nodeExtraLogging > auditInfo
field of the audit log entry:
{
"payload": {
"_id": "de5abe95-db97-4354-9d32-aab660ea23a3-4392064",
"component": "Authentication",
"entries": [{
"info": {
"authLevel": "0",
"displayName": "Scripted Decision",
"nodeExtraLogging": {
"auditInfo": {
"transactionStatus": "Success"
}
},
"nodeId": "d3a9a765-f5d7-41dd-b936-f862c8b672a2",
"nodeOutcome": "true",
"nodeType": "ScriptedDecisionNode",
"treeName": "Test"
}
}],
"eventName": "AM-NODE-LOGIN-COMPLETED",
"level": "INFO",
"principal": [
"014c54bd-6078-4639-8316-8ce0e7746fa4"
],
"realm": "/alpha",
"source": "audit",
"timestamp": "2022-10-31T13:06:38.195Z",
"topic": "authentication",
"trackingIds": [
"de5abe95-db97-4354-9d32-aab660ea23a3-4391910"
],
"transactionId": "bd52ff36-8b34-4819-8e50-68ca1961275b-request-4/0"
},
"timestamp": "2022-10-31T13:06:38.195401296Z",
"type": "application/json",
"source": "am-authentication"
}