The Action Interface

The Action interface encapsulates changes to authentication tree state and flow control.

The Action interface uses the following fields:

callbacks

A list of the callbacks that have been requested by the node. This list may be null.

errorMessage

A custom error message string that will be included in the response JSON if the authentication tree reaches the Failure authentication node.

Each node in a tree can replace or update the error message string as the user traverses through the authentication tree.

Note that your custom node or custom UI is responsible for localization of the error string, if required.

outcome

The result of the node.

sessionProperties

A map of properties that will be added to the final session if the authentication tree completes successfully.

Use the putSessionProperty(String key, String value) and removeSessionProperty(String key) methods to add or remove entries from the map.

sharedState

A JSON representation of the shared state - the properties set so far by nodes in the tree.

See "Storing Values in Shared Tree State".

transientState

A JSON representation of any transient state - properties with security or secret values set by previous nodes in the tree. Transient tree state is not persisted to the authentication session, and is only available until the authentication flow reaches the next node requiring user interaction.

See "Storing Values in Shared Tree State".

The methods provided within the Action interface are as follows:

goTo()

Specify the exit path to take, and move on to the next node in the tree.

For example:

return goTo(false).build();

See the goTo method in the AM 7.0.2 Public API Javadoc.

send()

Send the specified callbacks to the user for them to interact with.

For example, the Username Collector Node uses the following code to send the NameCallback callback to the user to request the USERNAME value:

return send(new NameCallback(bundle.getString("callback.username"))).build();
        

See the send methods in the AM 7.0.2 Public API Javadoc.

sendingCallbacks()

Returns true if the action is a request for input from the user.

See the sendingCallbacks method in the AM 7.0.2 Public API Javadoc.

suspend()

Suspends the authentication tree, and allows the user to resume it from the point it was suspended.

For example, the following call is taken from the Email Suspend Node:

return suspend(resumeURI -> createSuspendOutcome(context, resumeURI, recipient, templateObject)).build();

Use the SuspensionHandler interface for handling the suspension request.

For more information, see the suspend method in the AM 7.0.2 Public API Javadoc.

For example, the following is the Action implementation from the Auth Level Decision Node:

@Override
public Action process(TreeContext context) throws NodeProcessException {
    JsonValue authLevel = context.getState(AUTH_LEVEL);
    boolean authLevelSufficient = !authLevel.isNull()
                                  && authLevel.asInteger()
                                  >= config.authLevelRequirement();
    return goTo(authLevelSufficient).build();
}

For more information, see the Action class in the AM 7.0.2 Public API Javadoc.

Storing Values in Shared Tree State

Tree state exists for the lifetime of the authentication session. Once tree execution is complete, the authentication session is terminated and a user session is created. The purpose of tree state is to hold state between the nodes.

A good example is the Username Collector Node, which gets the user name from the user and stores it the shared tree state. Later, the Data Store Decision Node can pull this value from shared tree state and use it to authenticate the user.

Authentication sessions when using chains and modules are stateful - the AM server that starts the authentication flow must not change. A load balancer cookie is set on the responses to the user to ensure the same AM server is used.

In contrast, authentication trees can be made stateless, so that any AM instance in a deployment can continue the authentication session.

For more information on configuring sessions, see Sessions Guide.

Storing Secret Values in Transient Tree State and the Secure State

Authentication state should always be stored in the TreeContext sharedState object, which is then passed in as a parameter to the process method. AM ensures that this shared state is made available to downstream nodes.

To avoid storing sensitive information, such as passwords, a distinction is made between authentication state which can be written to CTS or to a JWT, and state which should only be available for the duration of the current HTTP request.

Sensitive information, such as passwords, should be stored in the TreeContext transientState.

This approach, however, presents a challenge on nodes that need to process sensitive information later on the authentication flow, such as IDM user self-service nodes.

For these cases, AM checks if data stored in the transientState object would be used later in the tree by another node. If this is the case, AM promotes the relevant data stored in the transientState object to the secureState object and encrypts it using the key stored in the am.authn.trees.transientstate.encryption secret ID.

In spite of the existence of the secure state, we recommend that you code your nodes using the shared and transient states to ensure that the authentication flow is not bloated with calls to encrypt/decrypt data, and to ensure the authentication session size stays on the small side, especially when the realm is configured for client-based authentication sessions.

Setting and Getting Values Stored in Tree State

AM distinguishes the following tree states:

  • Shared state, where nodes store non-sensitive information that needs to be available during the authentication flow.

  • Transient state, where nodes store sensitive information that needs to be available for the duration of the current HTTP request.

  • Secure state, where AM stores encrypted sensitive information that needs to be available during the authentication flow.

To Set and Get Values in Tree State
  • Use the following code examples to set and get values stored in any of the tree states:

    • To store a value in the shared state, create a copy of the existing tree state, insert the new value into the copy, and then replace the existing state with the copy before returning the Action from the process method.

      For example:

      public Action process(TreeContext context) {
        JsonValue copyState = context.sharedState.copy().put(USERNAME, name);
        goToNext().replaceSharedState(copyState).build();
      }

      For the transientState, replace sharedState.copy() with transientState.copy().

      Note that sharedState and transientState should be updated by using the replaceSharedState and replaceTransientState methods on Action.Builder respectively. For example:

      Action action = goToNext()
      .replaceTransientState(context.transientState.copy().put(PASSWORD, pwd))
      .build();

      Note

      AM will copy relevant data from the transient state object to the secure state object if required during the authentication flow. No additional code is required.

    • To get a value from any of the tree states, use the following code:

      public Action process(TreeContext context) {
        context.getState(USERNAME).asString()
      }

      For a given variable, the context.getState() method tries to retrieve data from the different states in the following order:

      1. transientState

      2. secureState

      3. sharedState

Accessing an Identity's Profile

AM allows a node to read and write data to and from an identity's profile. This is useful if a node needs to store information more permanently than when using either the authentication trees' sharedState, or the identity's session.

Warning

Any node which reads or writes to an identity's profile must only occur in a tree after the identity has been verified. For example, as the final step in a tree, or directly after a Data Store Decision Node.

To read an identity's profile from a realm, use the IdUtils static class:

AMIdentity id = IdUtils.getIdentity(username, realm);

Tip

Wrap the method call in an instantiable class to ease testing.

If AM is configured to search for the identity's profile using a different search attribute to the default, provide the attributes as a third argument to the method.

To obtain the attributes you could request them in the configuration of the node, or obtain them from the realm's authentication service configuration.

The following example demonstrates how to obtain the user alias:

public AMIdentity getIdentityFromSearchAlias(String username, String realm) {
    ServiceConfig serviceConfig = coreWrapper
        .getServiceConfigManager(ISAuthConstants.AUTH_SERVICE_NAME,
            AccessController.doPrivileged(AdminTokenAction.getInstance()))
        .getOrganizationConfig(realm);

    Set<String> realmAliasAttrs = serviceConfig.getAttributes()
        .get(ISAuthConstants.AUTH_ALIAS_ATTR);

   return IdUtils.getIdentity(username, realm, realmAliasAttrs);
}

By combining these approaches, you can search for an identity by using the ID and whichever configured attribute field(s) as necessary.

Reading Values from an Identity's Profile
  • To read individual specific attribute values from a profile, after obtaining the profile, use the AMIdentity#getAttribute(String name) method.

Writing Values to an Identity's Profile
  • To write a value into an identity's profile, create a Map<String, Set<String>> structure of the attributes you wish to write, as follows:

    Map<String, Set<String>> attrs = new HashMap<>();
    attrs.put("attribute", Collections.singleton("value");
    user.setAttributes(attrs);
    user.store();

Creating and Managing Callbacks

Callbacks are the method that nodes use to obtain interaction with the authenticating user.

Calling the getCallbacks() method on a TreeContext - the sole argument to the process() method of a node - returns a list of all the callbacks that have just been displayed in the UI.

Callbacks must implement the javax.security.auth.callback.Callback interface, although there are already many convenient existing implementations in the package and you may not need to create your own.

Below is an example of multiple callbacks that have been created by a node and passed to the UI:

Example of multiple callbacks

In order to process responses to callbacks, it is necessary to know which callback is at which position in the list. You can find the position of the callbacks created by the current node by using the constant properties for each callback position in the processing node.

If the callbacks were created in previous nodes, their positions must be stored in the shared state before subsequent nodes can use them.

The following is the code that created the UI displayed in the previous image:

ImmutableList.of(
  new TextOutputCallback(messageType, message.toUpperCase()),
  new PasswordCallback(bundle.getString("oldPasswordCallback"), false),
  new PasswordCallback(bundle.getString("newPasswordCallback"), false),
  new PasswordCallback(bundle.getString("confirmPasswordCallback"), false),
  confirmationCallback
);

Note that the order of callbacks defined in code is preserved in the UI.

Sending and Executing JavaScript in a Callback

A node can provide JavaScript for execution on the client side browser.

For example, the following is a simple JavaScript script named hello-world.js:

alert("Hello, World!");

Execute the script on the client by using the following code:

String helloScript = getScriptAsString("hello-world.js");
ScriptTextOutputCallback scriptCallback = new ScriptTextOutputCallback(helloScript);
ImmutableList<Callback> callbacks = ImmutableList.of(scriptCallback);
return send(callbacks).build();

Variables can be injected using your favourite Java String utilities, such as String.format(script, myValue).

To retrieve the data back from the script, add HiddenValueCallback to the list of callbacks sent to the user, as follows:

        HiddenValueCallback hiddenValueCallback = new HiddenValueCallback("myHiddenOutcome", "false");

The JavaScript needs to add the required data to the HiddenValueCallback and submit the form, for example:

document.getElementById('myHiddenOutcome').value = "client side data"    document.getElementById("loginButton_0").click();

In the process method of the node, retrieve the hidden callback as follows:

Optional<String> result = context.getCallback(HiddenValueCallback.class)
  .map(HiddenValueCallback::getValue)
  .filter(scriptOutput -> !Strings.isNullOrEmpty(scriptOutput));

if (result.isPresent()) {
  String myClientSideData = result.get();
}

Handling Multiple Visits to the Same Node

Authentication flow can return to the same decision node by using two different methods.

The first method is to route the failure outcome through a Retry Limit Decision Node. This node can limit how many times a user can enter incorrect authentication details. In these instances, the user is returned to re-enter their information; for example, back to an earlier Username Collector Node.

The second method involves routing directly back to the currently processing node. To achieve this, use the Action.send() method, rather than Action.goTo(). The Action.goTo method passes control onto the next node in the tree. The Action.send() method takes a list of callbacks which you can construct in the current node. The return value is an ActionBuilder, which can be used to create an Action, as follows:

ActionBuilder action = Action.send(ImmutableList.of(new ChoiceCallback(), new ConfirmationCallback()));

A typical example of returning to the same node is a password change screen where the user must enter their current password, new password, and new password confirmation. The node that processes these callbacks needs to remain on the screen and display an error message if any of the data entered by the user is incorrect. For example, if the new password and password confirmation do not match.

When a ConfirmationCallback is invoked on a screen that was produced by Action.send(), it will always route back to the node that created it. Once the details are valid, return an Action created using Action.goTo() and tree processing can continue as normal.

Read a different version of :