Scripted Decision Node API Functionality
The scripted decision node lets you write a server-side script in JavaScript or Groovy, to determine the path the authentication journey takes.
These scripts have access to a number of bindings, which provide the context to help you make the decision.
The primary role of a scripted decision node is to specify the possible paths a user can take. There are two methods to define these paths within the script:
outcome
The simplest method is to assign one or more string values to the
outcome
variable.if (...) { outcome = "true" } else { outcome = "false" }
When configuring the scripted decision node in an authentication tree, add the two outcomes
true
andfalse
, and connect them to other parts of the tree, so that tree evaluation can continue.You can specify as many outcomes as required in your scripts; for example, you might have
hours
,days
, andmonths
. Be sure to specify each possible outcome when designing your authentication journey.Action
You can use the
Action
interface to define the script outcome and/or specify an operation to perform.Example:
var fr = JavaImporter( org.forgerock.openam.auth.node.api.Action ) if (...) { // Set outcome to "true", and create and populate a custom session property: action = fr.Action.goTo("true").putSessionProperty("customKey", "customValue").build() } else { // Set outcome to "false". If supported by the UI, the error message is displayed: action = fr.Action.goTo("false").withErrorMessage("Friendly error description.").build() }
Tip
You can also use the
Action
interface for other functionality:For more information on the
Action
interface, see Action in the AM 7.1.4 Public API Javadoc.
Note
An outcome specified as an Action
takes precedence over the value set for the outcome
variable:
action = Action.goTo("false").build() // Tree continues along "false" outcome. outcome = "true" // No effect.
For more information on specifying outcomes when using the scripted decision node, see "Scripted Decision Node".
The following table lists the bindings accessible to scripted decision node scripts:
Binding | Information |
---|---|
| Add information to the AM audit logs. |
| Request additional data from the user, by sending any of the supported callbacks. See "Using Callbacks". |
| If the user has previously authenticated and has a session, use this variable to access the properties of that session. The user will only have an existing session when performing a session upgrade. Any properties that may have been added by nodes earlier in the current tree will not appear on the user's new session until the authentication tree is completed, and are therefore not available to the |
| Make outbound HTTP calls. |
| Access the data stored in the user's profile. |
| Write information to the AM debug logs. See "Debug Logging". |
| Return the name of the realm to which the user is authenticating as a string. For example, authenticating to the top-level realm returns a string value of |
| Access the HTTP headers provided in the login request. |
| Access the HTTP request parameters provided in the login request. |
| Access the secrets configured in an AM instance. |
| Access data set by previous nodes in the tree, or store data to be used by subsequent nodes. |
Accessing Request Header Data
Scripted Decision Node scripts can access the headers provided by the login request by using the methods of the requestHeaders
object.
Note that the script has access to a copy of the headers. Changing their values does not affect the request itself.
-
String[] requestHeaders.get(String Header Name)
Return a java.util.ArrayList of the values in the named request header, or
null
, if the property is not set. Note that header names are case-sensitive.Example:
var headerName = "user-agent" if (requestHeaders.get(headerName).get(0).indexOf("Chrome") !== -1) { outcome = "true" } else { outcome = "false" }
Accessing Request Parameter Data
Scripted Decision Node scripts can access any query parameters provided by the login request by using the methods of the requestParameters
object.
Note that the script has access to a copy of the parameters. Changing their values does not affect the request itself.
-
String[] requestParameters.get(String Parameter Name)
Return a java.util.ArrayList of the values in the named request parameter, or
null
, if the parameter is not available.Example:
var service var authIndexType = requestParameters.get("authIndexType") if (authIndexType && String(authIndexType.get(0)) === "service") { service = requestParameters.get("authIndexValue").get(0) }
def service def authIndexType = requestParameters.get("authIndexType") if (authIndexType && authIndexType.get(0) == "service") { service = requestParameters.get("authIndexValue").get(0) }
Note
In JavaScript, the values stored in
requestParameters
have atypeof
of object, and represent thejava.lang.String
class. Convert the value to a string in order to use strict equality comparisons.
Accessing Shared State Data
Scripted Decision Node scripts can get access to the shared state within the tree by using the nodeState
object.
-
JsonValue nodeState.get(String Property Name)
Returns the value of the named property. The value may come from the transient, secure, or shared states, in that order. For example, if the same property is available in several states, the method will return the value of the property in the transient state first.
If the property is not set, the method returns
null
.Note that property names are case-sensitive.
Example:
var currentAuthLevel = nodeState.get("authLevel").asString() var givenPassword = nodeState.get("password").asString()
-
nodeState nodeState.putShared(String Property Name, String Property Value)
Sets the value of the named shared state property. Note that property names are case-sensitive.
Example:
try { var currentAuthLevel = nodeState.get("authLevel").asString() } catch (e) { nodeState.putShared("errorMessage", e.toString()) }
-
nodeState nodeState.putTransient(String Property Name, String Property Value)
Sets the value of the named transient state property. Note that property names are case-sensitive.
Example:
nodeState.putTransient("sensitiveKey", "sensitiveValue")
Accessing Profile Data
Scripted decision nodes can access profile data through the methods of the idRepository
object.
-
Set idRepository.getAttribute(String User Name, String Attribute Name)
Return the values of the named attribute for the named user.
-
Void idRepository.setAttribute(String User Name, String Attribute Name, Array Attribute Values)
Set the named attribute as specified by the attribute value for the named user, and persist the result in the user's profile.
Example:
var username = nodeState.get("username") var attribute = "mail" idRepository.setAttribute(username, attribute, ["user.0@a.com", "user.0@b.com"])
def username = nodeState.get("username") def attribute = "mail" idRepository.setAttribute(username, attribute, ["user.0@a.com", "user.0@b.com"] as String[])
-
Void idRepository.addAttribute(String User Name, String Attribute Name, String Attribute Value)
Add an attribute value to the list of attribute values associated with the attribute name for a particular user.
Example:
var username = nodeState.get("username") var attribute = "mail" // Add a value as a String. idRepository.addAttribute(username, attribute, "user.0@c.com") logger.error(idRepository.getAttribute(username, attribute).toString()) // > ERROR: [user.0@a.com, user.0@c.com] // Get the first value. logger.error(idRepository.getAttribute(username, attribute).iterator().next()) // > ERROR: user.0@a.com // Get a value at the specified index. logger.error(idRepository.getAttribute(username, attribute).toArray()[1]) // > ERROR: user.0@c.com logger.error(idRepository.getAttribute(username, "non-existing-attribute").toString()) // > ERROR: []: If no attribute by this name is found, an empty Set is returned.
Setting Session Properties
Scripted Decision Node scripts can create session properties by using the Action
interface. The following examples set the outcome to true
, and add a custom session property:
var goTo = org.forgerock.openam.auth.node.api.Action.goTo action = goTo("true").putSessionProperty("mySessionProperty","myPropertyValue").build()
import org.forgerock.openam.auth.node.api.Action action = Action.goTo("true").putSessionProperty("mySessionProperty","myPropertyValue").build()
Note
Add the property name to the Whitelisted Session Property Names list in the Session Property Whitelist Service; otherwise, it will not be added to sessions. For more information on this service, see "Session Property Whitelist Service".
Add the script to a scripted decision node in your authentication tree. Users that authenticate successfully using that tree will have the property added to their session, as shown in the following output when introspecting a session:
{ "username": "15249a65-8f9a-4063-9586-a2465963cee4", "universalId": "id=15249a65-8f9a-4063-9586-a2465963cee4,ou=user,o=alpha,ou=services,ou=am-config", "realm": "/alpha", "latestAccessTime": "2020-10-22T15:01:14Z", "maxIdleExpirationTime": "2020-10-22T15:31:14Z", "maxSessionExpirationTime": "2020-10-22T17:01:13Z", "properties": { "AMCtxId": "dffed74d-f203-469c-9ed2-34738915baea-5255", "mySessionProperty": "myPropertyValue" } }
Accessing Existing Session Properties
Scripted Decision Node scripts can access any existing session properties during a session upgrade request, by using the existingSession
object.
The following table lists the methods of the existingSession
object:
-
String existingSession.get(String Property Name)
Return the string value of the named existing session property, or
null
, if the property is not set. Note that property names are case-sensitive.Warning
If the current request is not a session upgrade and does not provide an existing session, the
existingSession
variable is not declared. Check for a declaration before attempting to access the variable.Example:
if (typeof existingSession !== 'undefined') { existingAuthLevel = existingSession.get("AuthLevel") } else { logger.error("Variable existingSession not declared - not a session upgrade.") }
Using Callbacks
The scripted decision node can use callbacks to provide or request additional information during the authentication process.
Example:
The following scripts use the NameCallBack
callback to request a "Nickname" value from the user, and adds the returned value to the nodeState
map for use elsewhere in the authentication tree:
import org.forgerock.openam.auth.node.api.* import javax.security.auth.callback.NameCallback if (callbacks.isEmpty()) { action = Action.send(new NameCallback("Enter Your Nickname")).build() } else { nodeState.putShared("Nickname", callbacks.get(0).getName()) action = Action.goTo("true").build() }
var fr = JavaImporter( org.forgerock.openam.auth.node.api, javax.security.auth.callback.NameCallback ) with (fr) { if (callbacks.isEmpty()) { action = Action.send(new NameCallback("Enter Your Nickname")).build() } else { nodeState.putShared("Nickname", callbacks.get(0).getName()) action = Action.goTo("true").build() } }
For a list of supported callbacks, see "Supported Callbacks".
Accessing Credentials and Secrets
Scripts used in a scripted decision node can access the secrets configured in AM secret stores.
For example, a script can access credentials or secrets defined in a file system secret volume in order to make outbound calls to a third-party REST service, without hard-coding those credentials in the script.
-
String secrets.getGenericSecret(String Secret ID)
Returns the value of the specified secret ID.
If the secret ID is defined at the realm level, its value is returned; otherwise, the script returns the value defined at the global level.
Only secret IDs that begin with the string
scripted.node.
are accessible to scripts.For more information on creating secret IDs in a secret store, see "Configuring Secret Stores".Use the following functions to format the returned secret value:
-
getAsBytes()
Retrieve the secret value in
byte[]
format.-
getAsUtf8()
Retrieve the secret value in UTF-8 format.
Example:
The following example scripts show how to get the value (
passwd
) from a secret ID namedscripted.node.secret.id
. They use the value in a basic authentication header to access the http://httpbin.org/basic-auth/user/passwd service:var username = "demoUser" var password = secrets.getGenericSecret("scripted.node.secret.id").getAsUtf8() var auth = java.util.Base64.getEncoder().encodeToString(java.lang.String(username + ":" + password).getBytes()) var request = new org.forgerock.http.protocol.Request() request.setMethod("GET") request.setUri("http://httpbin.org/basic-auth/demoUser/passwd") request.getHeaders().add("content-type","application/json; charset=utf-8") request.getHeaders().add("Authorization", "Basic " + auth) var response = httpClient.send(request).get() var jsonResult = JSON.parse(response.getEntity().getString()) logger.error("Script result: " + JSON.stringify(jsonResult)) if (jsonResult.hasOwnProperty("authenticated")) { logger.error("outcome = success") outcome = "success" } else { logger.error("outcome = failure") outcome = "failure" }
def username = "demoUser" def password = secrets.getGenericSecret("scripted.node.secret.id").getAsUtf8() def auth = java.util.Base64.getEncoder().encodeToString((username + ":" + password).getBytes()) def request = new org.forgerock.http.protocol.Request() request.setMethod("GET") request.setUri("http://httpbin.org/basic-auth/demoUser/passwd") request.getHeaders().add("content-type","application/json; charset=utf-8") request.getHeaders().add("Authorization", "Basic " + auth) def response = httpClient.send(request).get() if (response.status.successful) { logger.error("outcome = success") outcome = "success" } else { logger.error("outcome = failure") outcome = "failure" }
Note
To use these sample scripts, you may need to add the following classes to the class whitelist property in the
AUTHENTICATION_TREE_DECISION_NODE
scripting engine configuration:org.mozilla.javascript.ConsString
java.util.Base64
java.util.Base64$Encoder
See "Security".
-
Adding Audit Information
The scripted decision node can add information to audit log entries, by using the auditEntryDetail
variable.
AM appends the value of the variable to the authentication audit logs. For JavaScript, the variable must be a string.
Example:
The following scripts add the user's email address to the authentication.audit.json
audit log file:
var currentUser = nodeState.get("username").asString() var email = idRepository.getAttribute(currentUser,"mail").iterator().next().toString() auditEntryDetail="Extra Audit: " + currentUser + " email address: " + email outcome = "true"
var currentUser = nodeState.get("username").asString() var email = idRepository.getAttribute(currentUser,"mail").iterator().next().toString() var detailStr = ("Extra Audit: " + currentUser + " email address: " + email).toString() auditEntryDetail = detailStr outcome = "true"
AM records the audit string in the auditInfo
field of the authentication audit log entry:
{ "_id":"f036618e-e318-4134-ac2a-13e860396103-545013", "timestamp":"2020-08-13T18:20:25.202Z", "eventName":"AM-NODE-LOGIN-COMPLETED", "transactionId":"f036618e-e318-4134-ac2a-13e860396103-544998", "trackingIds":[ "f036618e-e318-4134-ac2a-13e860396103-544956" ], "principal":[ "demo" ], "entries":[ { "info":{ "nodeOutcome":"true", "treeName":"Example", "displayName":"Audit Entry", "nodeType":"ScriptedDecisionNode", "nodeId":"13d40add-137c-4564-ad3c-7d98f7c180c1", "authLevel":"0", "nodeExtraLogging":{ "auditInfo":"Extra Audit: demo email address: demo@example.com" } } } ] }
For more information about auditing, see Setting Up Audit Logging.