PingAM 8.0.1

Custom Java nodes

These topics provide guidance and best practices for developing and maintaining Java authentication nodes in AM.

To create your own custom scripted nodes, use the Node Designer.

You can find information on configuring and using authentication trees in Configure trees.

Name changes for ForgeRock products

Product names changed when ForgeRock became part of Ping Identity.

The following name changes have been in effect since early 2024:

Old name New name

ForgeRock Identity Cloud

PingOne Advanced Identity Cloud

ForgeRock Access Management

PingAM

ForgeRock Directory Services

PingDS

ForgeRock Identity Management

PingIDM

ForgeRock Identity Gateway

PingGateway

Learn more about the name changes in New names for ForgeRock products in the Knowledge Base.

Authentication nodes

Authentication trees provide fine-grained authentication by allowing multiple paths and decision points throughout the authentication flow.

Authentication trees are made up of authentication nodes that define actions taken during authentication. Each node performs a single task during authentication, such as collecting a username or making a simple decision. Nodes can have multiple outcomes rather than just success or failure.

You can create complex yet customer-friendly authentication experiences by linking nodes together, creating loops, and nesting nodes within a tree, as follows:

Create multiple paths for authentication by linking nodes within trees.
Figure 1. Example authentication tree

Node types

Nodes are designed to have a single responsibility. Where appropriate, they should be loosely coupled with other nodes, enabling reuse in multiple situations.

For example, if a newly written node requires a username value, it should not collect it itself, but rely on another node, namely the Username Collector node.

There are two broad node types: collector nodes and decision nodes.

Collector nodes

Collector nodes capture data from a user during the authentication process. This data is often captured by a callback that is rendered in the UI as a text field, drop-down list, or other form component.

Examples of collector nodes include the Username Collector node and Password Collector node.

The output from a username collector node.

Collector nodes can perform basic processing of the collected data, before making it available to subsequent nodes in the authentication tree.

The Choice Collector node provides a drop-down list, populated with options defined when the tree is created, or edited.

The output from a choice collector node.

Not all collector nodes use callbacks. For example, the Zero Page Login Collector node retrieves a username and password value from the request headers, if present.

Decision nodes

Decision nodes do the following:

  • Retrieve the state produced by one or more nodes.

  • Perform some processing on that state.

  • Optionally, store some derived information in the shared state.

  • Provide one or more outcomes, depending on the result.

The simplest decision node returns a boolean outcome - true, or false.

Complex nodes may have additional outcomes. For example, the LDAP Decision node provides the additional outcomes Locked, Expired, and Cancelled. The tree administrator decides what to do with each outcome; for example, the True outcome is often routed to a Success node, or to additional nodes for further authentication.

In the following example tree, two collector nodes are connected before a Data Store Decision node. The node then uses the credentials to authenticate the user against the identity stores configured for the realm. In this instance, an unsuccessful login attempt leads directly to failure; the user must restart the process from scratch.

A tree containing a data store decision node.

A more user-friendly approach might route unsuccessful attempts to a Retry Limit Decision node. In the following example, unsuccessful authentication attempts at the Data Store Decision node stage are routed into a Retry Limit Decision node. Depending on how many retries have been configured, the node either retries or rejects the new login attempt. Rejected attempts lead to a locked account.

A tree containing a data store decision node, with multiple retries and account lockout.
Nodes can have prerequisite stages

Some Decision nodes are only applicable when used in conjunction with other nodes. For example, the Persistent Cookie Decision node looks for a persistent cookie that has been set in the request, typically by the Set Persistent Cookie node. The OTP Collector Decision node, which is both a collector and a decision node, only works when used in conjunction with a one-time password generated by a HOTP Generator node.

Restrict a node’s functionality

To determine the functionality of a node, reduce the node’s responsibility to its core purpose. For example, the Password Collector node just captures the user’s password.

Before you create a set of nodes, assess the level of granularity the nodes should produce. For example, a customer’s environment may require a series of utility nodes that, on their own, don’t perform authentication actions, but have multiple use cases in many authentication journeys. In this case, you can create nodes that take values from the shared state and save it to the user’s profile.

Individual nodes can respond to a variety of inputs and outputs, and return different sets of callbacks to the user without leaving the node.

The following guidelines help a node developer determine the best point at which to split a node into multiple instances:

  • If a node’s process method takes input from the user, and immediately processes it:

    Consider splitting the functionality over two nodes:

    • A collector node returns callbacks to the user and stores the response in the shared state.

    • A decision node uses the inputs collected so far in the tree to determine the next course of action.

    A node that takes input from the user and makes a decision should only be designed as a single node if there is no possible additional use for the data gathered, other than making that specific decision.

  • If a processing stage in a node is duplicated in other nodes:

    In this case, take the repeating stage out and place it in its own node. Connect this node appropriately to each of the other nodes.

    If multiple nodes contain the same step in processing, such as returning a set of callbacks to ask the user for a set of data before processing it in different ways, the common functionality should be pulled out into its own node.

  • If a single function within the node has obvious use cases in other authentication journeys:

    In this case, the functionality should be written into a single, reusable node. For example, in multi-factor authentication, a mechanism for reporting a lost device is applicable to many node types, such as mobile push, OATH, and others.

Prepare for development

This page explains the prerequisites for building custom authentication nodes, and shows how to use either a Maven archetype, or the samples provided with AM, to set up a project for building nodes.

You can find information about customizing post-authentication hooks for a tree in Create tree hooks.

Prepare an environment for building custom authentication nodes

  1. Make sure your Backstage account is part of a subscription:

    • In a browser, go to the Backstage website and sign on or register for an account.

    • Confirm or request your account is added to a subscription. Learn more in Getting access to product support in the Knowledge Base.

  2. Install Apache Maven 3.6.3 or later, and Oracle JDK or OpenJDK version 17 or later.

    To verify the installed versions, run the mvn --version command:

    $ mvn --version
    Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
    Maven home: /opt/homebrew/Cellar/maven/3.9.6/libexec
    Java version: 17.0.12, vendor: Homebrew, runtime: /opt/homebrew/Cellar/openjdk/17.0.12/libexec/openjdk.jdk/Contents/Home
    Default locale: en_GB, platform encoding: UTF-8
    OS name: "mac os x", version: "14.7", arch: "aarch64", family: "mac"
  3. Configure Maven to be able to access the proprietary repositories by adding your Backstage credentials to the Maven settings.xml file. Learn more in How do I access the proprietary protected Maven repositories?.

    If you want to use the archetype to create a project for custom authentication nodes, you also need access to the forgerock-private-releases repository. Ensure your settings.xml file contains a profile similar to the following:

    <profiles>
      <profile>
      <id>forgerock</id>
      <repositories>
          <repository>
              <id>forgerock-private-releases</id>
              <url>https://maven.forgerock.org/artifactory/private-releases</url>
              <releases>
                  <enabled>true</enabled>
                  <checksumPolicy>fail</checksumPolicy>
              </releases>
              <snapshots>
                  <enabled>false</enabled>
                  <checksumPolicy>warn</checksumPolicy>
              </snapshots>
          </repository>
      </repositories>
      </profile>
    </profiles>
    <activeProfiles>
      <activeProfile>forgerock</activeProfile>
    </activeProfiles>

Set up a Maven project to build custom authentication nodes

Ping Identity provides a Maven archetype that creates a starter project, suitable for building an authentication node. You can also download the projects used to build the authentication nodes included with AM and modify those to match your requirements.

Complete the steps in Prepare an environment for building custom authentication nodes before proceeding.

Complete either of the following steps to set up or download a Maven project to build custom authentication nodes:

  1. To use the auth-tree-node-archetype archetype to generate a starter Maven project:

    • In a terminal window, go to a folder where you’ll create the new Maven project. For example:

      $ cd ~/Repositories
    • Run the mvn archetype:generate command, providing the following information:

      groupId

      A domain name you control, used for identifying the project.

      artifactId

      The name of the JAR created by the project, without version information. Also the name of the folder created to store the project.

      version

      The version assigned to the project.

      package

      The package name in which your custom authentication node classes are generated.

      authNodeName

      The name of the custom authentication node, also used in the generated README.md file and for class file names.

      AM stores installed nodes with a reference generated from the node’s class name. An installed node registered through a plugin is stored with the name returned as a result of calling Class.getSimpleName().

      AM doesn’t protect installed node names. The most recently installed node with a specific name will overwrite any previous installation of that node (including the nodes that are provided with AM by default). You must therefore choose a unique name for your custom node, and make sure the name isn’t already used for an existing node.

      For example:

      $ mvn archetype:generate \
        -DgroupId=com.example \
        -DartifactId=custom-auth-node \
        -Dversion=1.0.0-SNAPSHOT \
        -Dpackage=com.example.custom \
        -DauthNodeName=MyCustomAuthNode \
        -DarchetypeGroupId=org.forgerock.am \
        -DarchetypeArtifactId=auth-tree-node-archetype \
        -DarchetypeVersion=8.0.0 \
        -DinteractiveMode=false
      [INFO] Project created from Archetype in dir: /Users/Ping/Repositories/custom-auth-node
      [INFO] ------------------------------------------------------------------------
      [INFO] BUILD SUCCESS
      [INFO] ------------------------------------------------------------------------
      [INFO] Total time: 11.397 s
      [INFO] Finished at: 2024-09-25T15:45:06+00:00
      [INFO] ------------------------------------------------------------------------

      A new custom authentication node project is created; for example, in the /Users/Ping/Repositories/custom-auth-node folder.

      Example
      In this example, the archetype has created the basic structure required to create a custom authentication node.
      Figure 2. Node project created by using the archetype
  2. To download the project containing the default AM authentication nodes from the am-external repository:

    • Clone the am-external repository:

    • Check out the release/8.0.0 branch:

      $ cd am-external
      $ git checkout releases/8.0.0

      The AM authentication nodes project is located in the am-external/openam-auth-trees/auth-nodes/ folder.

      Example
      In this example, the project was cloned from the am-external repository.
      Figure 3. Node Project Cloned from the am-external repository

Files contained in the Maven project

pom.xml

Apache Maven project file for the custom authentication node.

This file specifies how to build the custom authentication node, and also specifies its dependencies on AM components.

Example

The following is an example pom.xml file from a node project:

<project>
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>example-node-plugin</artifactId>
  <version>1.0.0</version>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.forgerock.am</groupId>
        <artifactId>openam-bom</artifactId>
        <version>7.2.0-SNAPSHOT</version>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.forgerock.am</groupId>
      <artifactId>auth-node-api</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.forgerock.am</groupId>
      <artifactId>openam-annotations</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>26.0-jre</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <configuration>
          <shadedArtifactAttached>false</shadedArtifactAttached>
          <createDependencyReducedPom>true</createDependencyReducedPom>
          <relocations>
            <relocation>
              <pattern>com.google</pattern>
              <shadedPattern>com.example.node.guava</shadedPattern>
            </relocation>
          </relocations>
          <filters>
            <filter>
              <artifact>com.google.guava:guava</artifact>
              <excludes>
                <exclude>META-INF/**</exclude>
              </excludes>
            </filter>
          </filters>
          <transformers>
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
              <manifestEntries>
                <Import-Package>javax.annotation;resolution:=optional,sun.misc;resolution:=optional</Import-Package>
              </manifestEntries>
            </transformer>
          </transformers>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
authNodeName.java

Core class for the custom authentication node. Learn more in Node class.

authNodeNamePlugin.java

Plugin class for the custom authentication node. Learn more in Plugin class.

authNodeName.properties

Properties file containing the localized strings displayed by the custom authentication node. Learn more in Internationalize nodes.

You must include a nodeDescription property in your node to ensure it appears in the authentication tree designer. AM uses the nodeDescription property value as the name of your node.

The authNodeName reflects the name of your authentication node. For example, the auth-tree-node-archetype for Maven uses MyCustomAuthNode as the authNodeName.

Tips for custom authentication node projects

When you configure a project for creating custom nodes, consider the following points:

  • Your node may be deployed into a different AM version to the one you compiled against.

    Nodes from previous product versions should be compatible with subsequent product versions if you have only used @Supported APIs. Learn more in the PingAM Java API Specification.

    For example, a node built against AM 7.5 APIs can be deployed in an AM 8.0.0 instance.

  • Other custom nodes may depend on your node, which may be being built against a different version of the AM APIs.

  • Other custom nodes, or AM itself, may be using the same libraries as your node; for example, Guava or Apache Commons, and so on. This may cause version conflicts.

To help protect against some of these issues, consider the following recommendations:

  • Mark all product dependencies as provided in your build system configuration.

  • Repackage all external, non-proprietary dependencies inside your own .jar file. Repackaged dependencies will not clash with a different version of the same library from another source.

    If you are using Maven, use the maven-shade-plugin to repackage dependencies.

Security considerations

This section describes the security best practices you should implement when developing authentication nodes in AM.

Store sensitive data in secrets

When developing nodes that include sensitive data such as passwords and encryption keys, make sure the data is secured:

  • Use secret stores for sensitive data, such as passwords and encryption keys. Never store them directly in configuration or expose them in scripts.

    Learn more in Secret stores.

  • Store secrets using secure state. You can find information about secure state in Store values in shared tree state.

Update cryptography

Make sure you use well-known and trusted cryptographic libraries where appropriate.

Different algorithms and methods are discovered and tested over time, and communities of experts decide which are the most secure for different uses. Use up-to-date cryptographic methods and algorithms to generate keys.

Sanitize user input data

When developing nodes that accept user input data, sanitize the input data and remove any sensitive information, such as passwords, before using and storing the data. Don’t use unsanitized user input data for any purpose.

Where a node reads data from the shared state, always treat that data as user input data and sanitize accordingly.

Other considerations

  • If a node identifies a user before they sign in, make sure the node sets the identified identity on the node action using the withIdentifiedIdentity method.

  • Consider what data is being set in shared state and how subsequent nodes will use it. Make sure the data can’t be used in unintended ways. For example, consider data such as usernames and authentication levels that could result in information disclosure or elevation of privilege.

    Where a node reads data from the shared state, always treat that data as user input data and sanitize accordingly.

  • Make sure you don’t expose any sensitive information in your logs. For example, don’t log any plaintext secrets or personally identifiable information (PII).

Develop and maintain nodes

The following table describes the tasks involved in developing and maintaining custom nodes:

Task Resources

Create a class to implement the Node interface

This class is your custom authentication node. It should define all aspects of your node, such as configuration data, outcomes, and business logic.

Node class

Store values in shared tree state

Use the tree state to store values for use by downstream nodes.

Store values in shared tree state

Access an identity’s profile

Read or write data to and from an identity’s profile. The identity must be verified before it can be accessed.

Access an identity’s profile

Include callbacks

Use existing callbacks to interact with the authenticating user.

Include callbacks

Handle multiple visits to a node

Allow the authentication flow to return to the same node if needed.

Handle multiple visits to the same node

Create a plugin class

The plugin class provides details about your node to AM.

Plugin class

Node class

The Node class can access and modify the persisted state shared between the nodes within a tree, and can request input by using callbacks. The class also defines the possible exit paths from the node.

In Java terms, an authentication node is a class that implements the Node interface, org.forgerock.openam.auth.node.api.Node.

The SetSessionPropertiesNode class shows the steps to implement the Node interface:

package org.forgerock.openam.auth.nodes;

import java.util.Map;

import javax.inject.Inject;

import org.forgerock.openam.annotations.sm.Attribute;
import org.forgerock.openam.auth.node.api.Action;
import org.forgerock.openam.auth.node.api.Node;
import org.forgerock.openam.auth.node.api.SingleOutcomeNode;
import org.forgerock.openam.auth.node.api.TreeContext;
import org.forgerock.openam.auth.nodes.validators.SessionPropertyValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.assistedinject.Assisted;

/*
 * A node that defines a configurable set of properties to add to
 * the authenticated session if/when it is created.
 */
@Node.Metadata(outcomeProvider = SingleOutcomeNode.OutcomeProvider.class,
        configClass = SetSessionPropertiesNode.Config.class,
        tags = {"utilities"})                                           1
public class SetSessionPropertiesNode extends SingleOutcomeNode {       2

  /
   * Configuration for the node.
   /
  public interface Config {                                             3
    /
     * A map of property name to value.
     * @return a map of properties.
     /
    @Attribute(order = 100, validators = SessionPropertyValidator.class)
    Map<String, String> properties();
  }

  private final Config config;                                          4
  private final Logger logger = LoggerFactory.getLogger(SetSessionPropertiesNode.class);

  /
   * Constructs a new SetSessionPropertiesNode instance.
   * @param config Node configuration.
   */
  @Inject                                                               5
  public SetSessionPropertiesNode(@Assisted Config config) {
    this.config = config;
  }

  @Override
  public Action process(TreeContext context) {                          6
    logger.debug("SetSessionPropertiesNode started");
    Action.ActionBuilder actionBuilder = goToNext();
    config.properties().entrySet().forEach(property → {
      actionBuilder.putSessionProperty(property.getKey(), property.getValue());
      logger.debug("set session property {}", property);
    });
    return actionBuilder.build();
  }
}
Step Description Further information

1 Apply the @Node.Metadata annotation

The annotation specifies the outcome provider, configuration class, and optionally, the configuration validation class and tags.

Use an existing outcome provider such as SingleOutcomeNode.OutcomeProvider or AbstractDecisionNode.OutcomeProvider, or create a custom provider and reference the class from the annotation.

2 Implement the Node interface

Extend one of the following abstract classes to implement the Node interface:

SingleOutcomeNode

For nodes with a single exit path; for example Modify Auth Level node.

AbstractDecisionNode

For nodes with a boolean-type exit path (true/false, allow/deny); for example, Data Store Decision node.

Alternatively, write your own implementation of the OutcomeProvider interface to define custom exit paths.

Javadoc:

3 Implement the Config interface

The Config interface defines the configuration data for a node.

4 Define private constants and methods

Optional

5 Inject dependencies

Inject objects using Guice as this makes it easier to unit test your node.

This example specifies config as a parameter. You can also include supported AM classes, instances of third-party dependencies, or your own types.

6 Override the process method

The process method is where you store and retrieve state if required.

It takes a TreeContext parameter that you can use to access the request, callbacks, shared state and other input.

The method returns an Action object. This can be a response of callback to the user, an update of state, or a choice of outcome. The Action object encapsulates changes to state and flow control.

The choice of outcome in a simple decision node would be true or false, resulting in the authentication tree flow moving from the current node to a node at the relevant connection.

Metadata annotation

The annotation specifies two required attributes: the outcomeProvider and the configClass. Typically, the configClass attribute is an inner interface in the node implementation class.

You can also specify the following optional attributes: configValidator, extensions, and tags.

For example, the following is the @Node.Metadata annotation for the Data Store Decision node:

@Node.Metadata(outcomeProvider = AbstractDecisionNode.OutcomeProvider.class,
        configClass = DataStoreDecisionNode.Config.class,
        tags = {"basic authn", "basic authentication"})

Outcome provider

The outcomeProvider class defines the possible node outcomes.

The abstract implementations of the node interface, org.forgerock.openam.auth.node.api.SingleOutcomeNode and org.forgerock.openam.auth.node.api.AbstractDecisionNode, define outcome providers you can use for simple use cases. Provide your own implementation for more complex use cases.

To ensure the node is available to the Configuration Provider node, your outcome provider class must implement the StaticOutcomeProvider or the BoundedOutcomeProvider interfaces.

Learn more about these implementations and interfaces in the org.forgerock.openam.auth .node.api package.

For example, the following is the custom outcome provider from the LDAP Decision node, which has True, False, Locked, Cancelled, and Expired exit paths:

    /**
     * Defines the possible outcomes from this Ldap node.
     */
    public static class LdapOutcomeProvider implements StaticOutcomeProvider {
        @Override
        public List<Outcome> getOutcomes(PreferredLocales locales) {
            ResourceBundle bundle = locales.getBundleInPreferredLocale(LdapDecisionNode.BUNDLE,
                    LdapOutcomeProvider.class.getClassLoader());
            return ImmutableList.of(
                    new Outcome(LdapOutcome.TRUE.name(), bundle.getString("trueOutcome")),
                    new Outcome(LdapOutcome.FALSE.name(), bundle.getString("falseOutcome")),
                    new Outcome(LdapOutcome.LOCKED.name(), bundle.getString("lockedOutcome")),
                    new Outcome(LdapOutcome.CANCELLED.name(), bundle.getString("cancelledOutcome")),
                    new Outcome(LdapOutcome.EXPIRED.name(), bundle.getString("expiredOutcome")));
        }
    }

Configuration class

The configClass contains the configuration of any attributes requested by the node when using it as part of a tree.

Learn more in the Config interface.

Configuration validator

The optional configValidator class validates the provided configuration at the class level. This can be useful when you have two attributes that depend on each other for validation. For example, an attribute is only required if another attribute is set to true.

The configValidator class must implement the ServiceConfigValidator interface.

For example, the following is the @Node.Metadata annotation for the Message node:

@Node.Metadata(outcomeProvider = AbstractDecisionNode.OutcomeProvider.class,
        configClass = MessageNode.Config.class,
        configValidator = MessageNode.MessageNodeValidator.class,
        tags = {"utilities"})

Where the MessageNode.MessageNodeValidator.class validates the locales entered:

    /**
     * Validates the message node, ensuring all provided Locales are valid.
     */
    public static class MessageNodeValidator implements ServiceConfigValidator {

        private final Logger logger = LoggerFactory.getLogger(MessageNodeValidator.class);

        private static String getLocaleStringFromMessage(String message) {
            return StringUtils.substringBetween(message, "[", "]");
        }

        @Override
        public void validate(Realm realm, List<String> configPath, Map<String, Set<String>> attributes)
                throws ServiceConfigException, ServiceErrorException {
            for (String messageAttribute : MESSAGE_ATTRIBUTES) {
                validateMessageAttribute(attributes, messageAttribute);
            }
        }

        private void validateMessageAttribute(Map<String, Set<String>> attributes, String messageAttribute)
                throws ServiceConfigException {
            Set<String> attributesSet = attributes.get(messageAttribute);
            Set<Locale> messageLocales = attributesSet.stream()
                    .map(MessageNodeValidator::getLocaleStringFromMessage)
                    .map(com.sun.identity.shared.locale.Locale::getLocale)
                    .collect(Collectors.toSet());
            for (Locale messageLocale : messageLocales) {
                if (!LocaleUtils.isAvailableLocale(messageLocale)) {
                    logger.debug("Invalid messageLocale {} for {} attribute", messageLocale.toString(),
                            messageAttribute);
                    throw new ServiceConfigException("Invalid locale provided");
                }
            }
        }
    }

Extensions

The optional extensions class provides additional metadata information about the node. The Java class is serialized into JSON.

For example, the following is the @Node.Metadata annotation for the Username Collector node with an extensions class added:

@Node.Metadata(outcomeProvider = SingleOutcomeNode.OutcomeProvider.class,
        configClass = UsernameCollectorNode.Config.class,
        extensions = UsernameCollectorNode.ExtraMetadata.class,
        tags = {"basic authn", "basic authentication"})

Where the UsernameCollectorNode.ExtraMetadata.class adds extra metadata:

/**
     * Extra Metadata for the username collector node.
     */
    public static class ExtraMetadata {

        /**
         * The owner of the node.
         */
        public String owner = "Ping Identity";

    }

The getType() response for this node includes this metadata:

"metadata": {
    "owner": "Ping Identity",

Tags

The optional tags attribute contains a list of tags to categorize the node within the tree designer view.

Tags are made up of one or more text strings that let users find the node more easily when designing trees. For example, you could include common pseudonyms for the functionality the node provides, such as mfa for a node that provides multi-factor authentication functionality.

The tree designer view organizes nodes into a number of categories, based on the presence of certain tag values, as described in the table below:

Authentication node tag categories
Category Tag Example nodes

Basic Authentication

"basic authentication"

Data Store Decision node
Username Collector node

MFA

"mfa"

Push Sender node
WebAuthn Authentication node

Risk

"risk"

Account Lockout node
CAPTCHA node

Behavioral

"behavioral"

Increment Login Count node
Login Count Decision node

Contextual

"contextual"

Cookie Presence Decision node
Set Persistent Cookie node

Federation

"federation"

OAuth 2.0 node
OpenID Connect node

Identity Management

"identity management"

Anonymous User Mapping node
Terms and Conditions Decision node

Utilities

"utilities"

Choice Collector node
Scripted Decision node

Nodes that aren’t tagged with one of these tags appear in an Uncategorized section.

For example, the @Node.Metadata annotation for Timer Start node places it in the Utilities section:

@Node.Metadata(outcomeProvider = SingleOutcomeNode.OutcomeProvider.class,
        configClass = TimerStartNode.Config.class,
        tags = {"metrics", "utilities"})

Config interface

The Config interface defines the configuration data for a node. A node can’t have state, but it can have configuration data. Configuration is per node. Different nodes of the same type in the same tree have their own configuration.

You don’t need to write a class that implements the interface you define. AM automatically creates this as required.

Define node properties

Configure the node properties using methods. To provide a default value to the tree administrator, mark the method as default and define both a method and a value. To omit a default value, define the method’s signature but not the implementation.

For example:

public interface Config {

  //This will have no default value for the UI
  @Attribute(order = 10)
  String noDefaultAttribute();

  //This will default to the value LOCK.
  @Attribute(order = 20)
  default LockStatus lockAction() {
    return LockStatus.LOCK;
  }
}

For this Config example, a custom enum named LockStatus is returned.

The defined properties appear as configurable options in the tree designer view when adding a node of the relevant type. The options display to the user automatically.

Attribute names are used when localizing the node’s text. Learn more in Internationalize nodes.

The output from an example Config interface

You can find more information in the Config annotation type in the AM Public API Javadoc.

The @Attribute annotation

The @Attribute annotation is required. It defines the properties that appear as configurable options when you add or update a node.

Consider the following when using the @Attribute annotation:

  • You must specify an integer value for order to determine the position of the attribute in the UI.

  • Only use compatible Java types as attributes.

    Compatible Java types
    Java type Additional information

    java.lang.String

    java.lang.Integer

    java.lang.Long

    java.lang.Boolean

    java.lang.Enum

    Use any enum type.

    org.forgerock.json.JsonValue

    java.util.Locale

    java.net.URL

    char[] (char array)

    Use for passwords, but consider using org.forgerock.secrets.Purpose instead.

    java.util.List

    java.util.Set

    java.util.Map

    Use with the following elements:

    • Boolean

    • Duration

    • Integer

    • Locale

    • Long

    • String

    • URL

    • JsonValue

    • char[]

    Keys must be strings for java.util.Map.

    java.util.Optional

    Use with any of these compatible Java types.

    org.forgerock.secrets.Purpose

    Use for secret values such as passwords.

    You must annotate this type with @SecretPurpose to define the secret label.

  • Include requiredValue = true if the attribute is a required value.

    Any attributes that aren’t required should be an Optional attribute unless they’re already part of a collection through List, Map, or Set.

    Attributes aren’t required by default when either requiredValue is omitted or set to false.

  • Specify one or more validators if you need to validate the attribute values provided.

    Include a validator as follows:

    • Use an existing validator class from the org.forgerock.openam.auth.nodes.validators package, such as DecimalValidator or HMACKeyLengthValidator.

    • Create your own validator by implementing the ServiceAttributeValidator interface.

      Example

      The following example creates a validator called GreaterThanZeroValidator:

      public class GreaterThanZeroValidator implements ServiceAttributeValidator {
      
          @Override
          public boolean validate(Set<String> values) {
              boolean isValid = true;
              for (String value : values) {
                  if (Integer.parseInt(value) <= 0) {
                      isValid = false;
                      break;
                  }
              }
              return isValid;
          }
      }

For example:

public interface Config {
  @Attribute(order = 1)
  String domain();                                                    1

  @Attribute(order = 2, validators = {DecimalValidator.class,         2
                                      HMACKeyLengthValidator.class})
  int exampleNumber();

  @Attribute(order = 3, requiredValue = true)
  boolean isVerificationRequired();                                   3

  @Attribute(order = 4)
  @TextArea                                                           4
  String textBox();

  @Attribute(order = 5)
  @Password                                                           5
  char[] clientSecret();

  @Attribute(order = 6)
  default YourCustomEnum action() {
    return YourCustomEnum.LockScreen;                                 6

  @Attribute(order = 7, requiredValue = true, resourceName = "secretLabelIdentifier")
  @SecretPurpose("am.authentication.nodes.customauth.%s.secret")
  Purpose<GenericSecret> secretValuePurpose();                        7
  };
}

1 The domain attribute defines a String-type node property for display in the UI. Access the attribute in the process method by using a reference to the config interface; for example, config.domain().

2 Specify one or more validator classes as the validators parameter.

3 The boolean attribute is defined as a required value.

4 Use the TextArea annotation to indicate a String-type node property that needs a larger text input than a single line.

5 Use the Password annotation to mask the input characters and encrypt the value of the attribute.

6 A custom enum attribute. This provides type safety and negates the misuse of Strings as generic type-unsafe value holders. The UI will correctly handle the enum and only let the tree administrator choose from the defined enum values.

Learn more in the Attribute annotation type in the AM Public API Javadoc.

7 An identifier used to create a secret label for the node that maps to a secret in a secret store. The default custom authentication node secret label is am.authentication.nodes.customauth.%s.secret where %s is the value of the identifier. The identifier can only contain alphanumeric characters a-z, A-Z, 0-9, and periods (.). It can’t start or end with a period.

To retrieve the secret using the identifier from your custom node, use the Secrets class, for example:

var validSecrets = secrets.getRealmSecrets(realm)
        .getValidSecrets(config.secretValuePurpose())
        .getOrThrowIfInterrupted()
        .map(s → s.revealAsUtf8(String::new))
        .collect(Collectors.toList());

Learn more in:

Inject objects into a node instance

A node instance is constructed every time the node is reached in a tree and is discarded as soon as it’s been used to process the state once.

State stored in a node is lost when the node’s process method completes. To make state available for other nodes in the tree, nodes must return the state to the user or store it in the shared state.

AM uses Google’s Guice dependency injection framework for authentication nodes and uses Guice to manage most of its object lifecycles. Use just-in-time bindings from the constructor to inject an object from Guice.

The following node-specific instances are available from Guice:

@Assisted Realm

The realm that the node is in.

@Assisted UUID

The unique ID of the node instance.

@Assisted TreeMetadata

The metadata for the tree that the node belongs to.

<T> @Assisted T

The configuration object that is an instance of the interface specified in the configClass metadata parameter.

Any other objects in AM that are managed by Guice can also be obtained from within the constructor.

The following example is the configuration injection used by the Debug node:

@Inject
public DebugNode(@Assisted DebugNode.Config config) {
  this.config = config;
  ...
}

Learn more in the Inject and Assisted annotation types in the Google Guice Javadoc.

Use a cache

You can use Guice injection to cache information in a node by annotating the object that contains the cache with the @Singleton annotation.

The Guice injected singleton will be used by multiple threads, so it must be thread safe. The following example uses the Google LoadingCache, which is thread safe. Alternatively, use java.util.concurrent.ConcurrentMap if you prefer to use a built-in Java class.

For example:

@Node.Metadata(
  outcomeProvider = SingleOutcomeNode.OutcomeProvider.class,
  configClass = MyCustomNode.Config.class)
  public class MyCustomNode extends SingleOutcomeNode {

    public interface Config {
      String url();
    }

    private final Config config;
    private final MyCustomNodeCache cache;

    @Inject
    public MyCustomNode(@Assisted Config config, MyCustomNodeCache cache) {
      this.config = config;
      this.cache = cache;
    }

    @Override
    public Action process(TreeContext context) {
      CachedThing thing = cache.getThing(config.url());
      // implement node logic here
    }
}

@Singleton
class MyCustomNodeCache {
   private final LoadingCache<String, CachedThing> cache =
      CacheBuilder.newBuilder()
         .build(CacheLoader.from(url -> read(url)));

   public CachedThing get(String url) {
      return cache.get(url);
   }

   private CachedThing read(String url) {
      // Access resource and construct
   }
}

Send an HTTP request

You can use Guice injection to send an HTTP request by injecting the CloseableHttpClientHandler into the node instance. This means the node uses the standard AM HTTP client handler, and all the httpClient settings and tuning apply.

The following example demonstrates sending an HTTP POST request to the https://www.example.com/api endpoint:

@Inject
public MyCustomNode(@Named("CloseableHttpClientHandler") Handler httpClientHandler) {
  this.httpClientHandler = httpClientHandler;
}

@Override
public Action process(TreeContext context) {
   URI uri = URI.create("https://www.example.com/api");

   Request request = new Request()
           .setUri(uri)
           .setMethod(HttpConstants.Methods.POST);

   JsonValue body = json(object(field("sampleKey", "sampleValue")));
   request.getEntity().setJson(body);

   Response response = httpClientHandler.handle(new RootContext(), request).getOrThrow();
}

Custom Guice bindings

If just-in-time bindings aren’t sufficient for your use case, you can add your own Guice module into the injector configuration by implementing your own com.google.inject.Module and registering it using the service loader mechanism. For example:

// com/example/MyCustomModule.java
public class MyCustomModule extends AbstractModule {
   @Override
   protected void configure() {
      bind(Thing.class).to(MyThing.class);
      // and so on
   }
}
// META-INF/services/com.google.inject.Module
// Learn more in https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
com.example.MyCustomModule

The MyCustomModule object will then be automatically configured as part of the injector creation.

Action class

The Node class returns an Action instance from its process() method.

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

For example, the following implementation demonstrates an authentication level decision:

@Override
public Action process(TreeContext context) throws NodeProcessException {
  NodeState state = context.getStateFor(this);
  if (!state.isDefined(AUTH_LEVEL)) {
    throw new NodeProcessException("Auth level is required");
  }
  JsonValue authLevel = state.get(AUTH_LEVEL);
  boolean authLevelSufficient =
    !authLevel.isNull()
    && authLevel.asInteger() >= config.authLevelRequirement();
  return goTo(authLevelSufficient).build();
}

Learn more in the Action class.

Action fields and methods

The Action class uses the following fields:

Fields Description

callbacks

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

errorMessage

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

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

If required, your custom node or custom UI must localize the error string.

lockoutMessage

A custom lockout message string included in the response JSON when the user is locked out.

If required, your custom node or custom UI must localize the error string.

outcome

The result of the node.

returnProperties

A map of properties returned to the client.

Use the withHeader, withStage, and withDescription methods to add a property to the map.

sessionHooks

The list of classes implementing the TreeHook interface that run after a successful login.

sessionProperties

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

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

sharedState and transientState

Deprecated.

Use the NodeState object instead. Learn more in Store values in a tree’s node states.

webhooks

The list of webhooks that run after logout.

Use the addWebhook and addWebhooks methods to populate this list.

The Action class provides the following static methods to create an ActionBuilder:

Methods Description

goTo

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

For example:

return goTo(false).build();

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();

sendingCallbacks

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

suspend

Suspends the authentication tree and lets the user resume it from the point it was suspended. You can also control how long it is suspended for.

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.

The inner class ActionBuilder provides the following methods for constructing the Action object and setting action-related properties:

Methods Description

addNodeType

Add a node type to the session properties and shared state. Replace any existing shared state with the specified TreeContext’s shared state.

addSessionHook and addSessionHooks

Add one or more session hook classes for AM to run after a successful login.

addWebhook and addWebhooks

Add one or more webhook names to the list of webhooks.

build

Creates and returns an Action instance providing the mandatory fields are set.

putSessionProperty

Add a new session property.

removeSessionProperty

Remove the specified session property.

replaceSharedState and replaceTransientState

Deprecated.

Use the NodeState object instead. Learn more in Store values in a tree’s node states.

withDescription

Set a description for this action.

withErrorMessage

Set a custom message for when the authentication tree reaches the failure node.

withHeader

Set a header for this action.

withIdentifiedIdentity

Add an identity, authenticated or not, that is confirmed to exist in an identity store. Specify the username and identity type or an AMIdentity object.

Use this method to record the type of identified user. If the advanced server property, org.forgerock.am.auth.trees.authenticate.identified.identity is set to true, AM uses the stored identified identities to decide which user to log in.

This lets the authentication tree engine correctly resolve identities that have the same username.

withLockoutMessage

Set a custom message for when the user is locked out.

withMaxIdleTime

Set the maximum idle time for the authenticated session in minutes.

This overrides the maximum idle time set in the journey or the Session service.

If a user has session timeouts set, the user-specific settings are always used.

withMaxSessionTime

Set the maximum authenticated session time in minutes.

This overrides the maximum authenticated session time set in the journey or the Session service.

If a user has session timeouts set, the user-specific settings are always used.

withStage

Set a stage name to return to the client to aid the rendering of the UI. The property is only sent if the node also sends callbacks.

withUniversalId

Deprecated.

Use withIdentifiedIdentity instead.

Handle errors

This page covers error handling in authentication nodes, including how to report errors to end users and tree administrators, as well as handling unrecoverable errors.

Authentication trees provide a number of ways to output error messages to the user.

Authentication errors

The most common error to display is a message in the event of an unsuccessful authentication. In an authentication tree, this occurs when the authentication process terminates at the failure node:

Error message shown by a tree that terminated at the failure node.

Unrecoverable errors

By default, when a catastrophic error occurs during node processing, a NodeProcessException exception should be thrown, which halts the authentication journey immediately, and displays a generic error message. This may not be desirable, as it could create a negative user experience.

Instead, errors that occur during node processing should be caught within the processing block of the node’s code, and the user should be routed to an erroneous state outcome. It may be appropriate to have a single error outcome, multiple error outcomes, or no error outcome at all, depending on the node.

It is valuable to store information about the cause of the error in the shared state, in case a node further along the tree processes it. This information should include error text to display to the user. If the shared state is used for this purpose, it is important to document not only the meaning of the various outcomes, but also the keys used to store information in the shared state.

Configuration errors

You can display error messages to the tree administrator; for example, when a configuration property of a node is required, but not provided.

To automatically display an appropriate error message when required values are missing, add requiredValue=true to your config property, as follows:

@Attribute(order = 300, requiredValue = true)
Set<String> accountSearchBaseDn();

To control the messages displayed on error, ensure there is a .properties file under src/main/resources/org.forgerock.openam.auth.nodes with the same name as your node class. Learn more in Internationalize nodes.

Store values in shared tree state

Tree state exists for the lifetime of the journey session. When the tree has completed, the journey session is terminated and an authenticated 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 username from the user and stores it in the shared tree state. Later, the Data Store Decision node can pull this value from the shared tree state and use it to authenticate the user.

Authentication trees can be stateful or stateless. When they are stateful, the AM server that starts the authentication flow mustn’t change. A load balancer cookie is set on the responses to the user to ensure the same AM server is used. When they are stateless, any AM instance in a deployment can proceed with the journey session.

You can find more information on configuring sessions in Sessions.

Store values in a tree’s node states

Always store the authentication state in the NodeState object that AM lets you access from the TreeContext object passed to the node’s process() method. AM ensures the node state is made available to downstream nodes:

  • Store non-sensitive information with the NodeState.putShared() method.

  • Store sensitive information, such as passwords, with the NodeState.putTransient() method.

    AM encrypts the transient state with the key that has the am.authn.trees.transientstate.encryption secret label. Downstream nodes must have the same key to decrypt and read it.

    Limit what you store with the putShared() and putTransient() methods to make sure the authentication flow isn’t bloated with calls to encrypt or decrypt data, and the journey session size stays small. This is especially true when the realm is configured for client-side journey sessions.

Get and set values stored in tree state

Internally, AM distinguishes the following node state data:

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

    You store this with the NodeState.putShared() method.

  • Transient state, where nodes store sensitive information that AM encrypts on round trips to the client.

    You store this with the NodeState.putTransient() method.

  • Secure state, where nodes store decrypted transient state.

Learn more in NodeState.

Set values in the tree state

To set node state values, get the NodeState using the TreeContext.getStateFor(Node node) method. Then, use the NodeState.putShared() and NodeState.putTransient() methods as described above.

For example:

// Setting values in NodeState
public Action process(TreeContext context) {
  String username;
  String password;
  // ...
  NodeState state = context.getStateFor(this);
  state.putShared(USERNAME, username);      // Non-sensitive information
  state.putTransient(PASSWORD, password);   // Sensitive information
  if (!state.isDefined(OPTIONAL_NUMERIC)) { // Check before updating
    state.putShared(OPTIONAL_NUMERIC, 42);
  }
  goToNext().build();
}

Get values in the tree state

To read node state values, use the NodeState.isDefined(String key) and NodeState.get(String key) methods.

For example:

// Getting values from NodeState
public Action process(TreeContext context) {
  NodeState state = context.getStateFor(this);
  String username;
  if (state.isDefined(USERNAME)) {
      username = state.get(USERNAME);
  } else {
    throw new NodeProcessException("Username is required");
  }
  // ...
  goToNext().build();
}

The get(String key) method retrieves the state for the key from NodeState states in the following order:

  1. transient

  2. secure

  3. shared

For example, if the same property is stored in the transient and shared states, the method returns the value of the property in the transient state first.

Combine objects

To combine objects from shared, transient and secure state, use the getObject method.

For example, if the following objectAttributes value exists in shared state:

"objectAttributes": { "sharedKey": "value" }

and the following objectAttributes value exists in transient state:

"objectAttributes": { "transientKey": "value" }

when you call nodeState.getObject("objectAttributes");, the combined result is as follows:

{
   "sharedKey": "value",
   "transientKey": "value"
}

This object is read-only.

Merge keys

To merge keys in an object, use the mergeShared and mergeTransient methods.

Learn more in Access shared state data.

Access 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' NodeState or the identity’s session.

Any node that 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 store a verified identity in the journey session, call ActionBuilder.withIdentifiedIdentity(). This ensures identities with the same username are correctly resolved.

Read an identity’s profile

Use the IdUtils static class:

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

Use the IdUtilsWrapper class to assist with testing.

If AM is configured to search for the identity’s profile using a different search attribute than 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) {
    ServiceConfigManager mgr = new ServiceConfigManager(
            ISAuthConstants.AUTH_SERVICE_NAME,
            AccessController.doPrivileged(AdminTokenAction.getInstance());

    ServiceConfig serviceConfig = mgr.getOrganizationConfig(realm, null);

    Set<String> realmAliasAttrs = serviceConfig.getAttributes()
        .get("iplanet-am-auth-alias-attr-name");

   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.

Read attributes of an identity’s profile

After obtaining the profile, use the AMIdentity.getAttribute(String name) method.

Write a value into an identity’s profile

Create a Map<String, Set<String>> structure of the attributes you want to write, as follows:

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

Include callbacks

Nodes use callbacks to enable interaction with the authenticating user.

You can’t create your own callbacks, but there are many existing implementations available to you. Learn more in Supported callbacks.

Calling the getCallbacks(Class<t> callbackType) method on a TreeContext, the sole argument to the process() method of a node, returns all callbacks of a particular type for the most recent request from the current node. For example, calling context.getCallbacks(PasswordCallback.class) returns a list of the PasswordCallback callbacks displayed in the UI most recently.

The following is an example of multiple callbacks created by a node and passed to the UI:

An example of multiple callbacks created by a node and passed to the UI.

To process the responses to callbacks, you must know the order of the callbacks 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
);

The order of callbacks defined in the code is preserved in the UI.

Send and execute 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 favorite 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();
}

Handle multiple visits to the same node

An authentication flow can return to a decision node in the following ways:

  • Route the failure outcome through a Retry Limit Decision node.

    This node can limit how many times a user can enter incorrect authentication details when the user is directed to an earlier node in the tree to re-enter their information. For example, to an earlier Username Collector node.

  • Re-route directly to the current processing node.

    To achieve this, use the Action.send() method rather than Action.goTo(). The Action.goTo method passes control to the next node in the tree. The Action.send() method takes a list of callbacks that you can construct in the current node. The return value is an ActionBuilder that 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 don’t match.

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

Plugin class

The plugin class is responsible for informing AM about the details of the customized authentication node. There is little variation between the plugin class for each authentication node, other than the version number and class names within.

Authentication nodes are installed into the product using the AM plugin framework. All AM plugins are created by implementing org.forgerock.openam.plugins.AmPlugin interface and registering it using the Java service architecture - placing a file in META-INF/services.

For plugins that provide authentication nodes there is an abstract implementation of the AmPlugin interface named org.forgerock.openam.auth.node.api.AbstractNodeAmPlugin.

The following is an example of the plugin class for an authentication node:

public class MyCustomNodePlugin extends AbstractNodeAmPlugin {                                 1

  private static final String CURRENT_VERSION = "1.0.0";                                       2

  @Override
  protected Map<String, Iterable<? extends Class<? extends Node>>>
  getNodesByVersion() {
    return Collections.singletonMap("1.0.0", Collections.singletonList(MyCustomNode.class));   3
  }

  @Override
  public String getPluginVersion() {
    return MyCustomNodePlugin.currentVersion;
  }
}

1 Name the plugin class after the core class, and append Plugin. For example, MyCustomNodePlugin.

2 Provide a version number for the authentication node.

3 Ensure a call to the getNodesByVersion() function returns the core classes of the authentication nodes to register. In this example the version is 1.0.0, and there is just one node being registered as that version.

AM plugins are notified of the following events:

onInstall

The plugin has been found during AM startup, and is being installed for the first time. It should create all the services and objects it needs.

onStartup(StartupType startupType)

The plugin is installed and is being started. Any dependency plugins can be relied on as having been started.

The type of startup is provided:

  • FIRST_TIME_INSTALL. The AM instance has been installed for the first time.

  • NORMAL_STARTUP. The AM instance is starting from a previously installed state, or is joining an already installed cluster.

onShutdown

The AM instance is in the process of shutting down cleanly. Any resources the plugin is using should be released and cleaned up.

upgrade(String fromVersion)

An existing version of the plugin is installed, and a new version has been found during startup. The plugin should make any changes it needs to the services and objects used in the previous version, and create all the services and objects required by the new version.

The version of the plugin being upgraded is provided.

onAmUpgrade(String fromVersion, String toVersion)

An AM system upgrade is in progress. Any updates needed to accommodate the AM upgrade should be made.

Plugin-specific upgrade should not be made here, as upgrade will be called subsequently if the plugin version has also changed.

The AM version being upgraded from, and to, are provided.

The plugin is responsible for maintaining a version number for its content, which is used for triggering appropriate events for installation and upgrade.

Learn more in amPlugin in the AM Public API Javadoc.

Upgrade nodes and change node configuration

Over time, it may become necessary to change the schema of the configuration for your node.

When this happens, the changes must be propagated to the AM configuration system. To ensure an update of the AM configuration, use either of the following methods, depending on the stage of development:

  • In the development stage, give your nodes the special version number 0.0.0. Any AM configuration created by nodes that have this special version number is wiped on each restart of AM.

    If you are using custom nodes with version 0.0.0 in trees, you must remove them from the trees before restarting AM and reinsert them after the restart. If you do not do this, the entire tree cannot be viewed in the UI after the restart.
  • After moving to production and switching to semantic versioning, you must write upgrade functions into the node to locate existing configuration and convert it to the new schema.

    For information on upgrading schema in production mode, refer to Upgrade simple node configuration schema changes and Upgrade complex node configuration schema changes.

Upgrade simple node configuration schema changes

This section explains how to upgrade nodes with simple schema changes. For example, changing an attribute to a compatible type.

When configuration schema changes are simple, call the PluginTools#upgradeAuthNode(Class) method in the upgrade method of your plugin, as follows:

@Override
public void upgrade(String fromVersion) throws PluginException {
  pluginTools.upgradeAuthNode(MyCustomNode.class);
}

Examples of simple schema changes include:

  • Changing an attribute type to one that is backwards-compatible with any existing values.

    For example changing an integer to a string type, or T to Set<T>.

  • Adding a new attribute that has a default value defined.

    For example:

    public class MyCustomNode implements Node {
      public interface Config {
        @Attribute(order = 1)
        String existingAttribute();
        @Attribute(order = 2)
        default Integer newAttribute() {
          return 5;
        }
      }
      // ...
    }

Upgrade complex node configuration schema changes

This section explains how to upgrade nodes that are changing the configuration schema such that existing values would clash with the new schema. For example, changing an attribute to an incompatible type.

When configuration schema changes are complex, use the API provided in the com.sun.identity.sm package. In this example, version 1.0.0 of a node has the following configuration schema:

public interface Config {
    @Attribute(order = 1)
    String name();
}

Version 2.0.0 of the node requires the user’s given name and family name separately, rather than simply a name string. The config for version 2.0.0 is as follows:

public interface Config {
  @Attribute(order = 1)
  String givenName();

  @Attribute(order = 2)
  String familyName();
}

To upgrade this example node configuration, find all existing instances of configuration created by the version 1.0.0 node, find the current values for the name attribute, and split it on the first space character to use in the two new attributes.

The following code shows how to upgrade the schema of this example node:

@Override
public void upgrade(String fromVersion) throws PluginException {
  try {
    SSOToken token = AccessController.doPrivileged(AdminTokenAction.getInstance());
    String serviceName = MyCustomNode.class.getSimpleName();
    ServiceConfigManager configManager = new ServiceConfigManager(serviceName, token);

    // Read all the values from all node in all the realms that will need replacing
    OrganizationConfigManager realmManager = new OrganizationConfigManager(token, "/");
    Set<String> realms = ImmutableSet.<String>builder()
      .add("/")
      .addAll(realmManager.getSubOrganizationNames("*", true))
      .build();
    Map<Pair<Realm, String>, String> oldValues = new HashMap<>();
    for (String realm : realms) {
      ServiceConfig container = configManager.getOrganizationConfig(realm, null);
      for (String nodeId : container.getSubConfigNames()) {
        ServiceConfig nodeConfig = container.getSubConfig(nodeId);
        String name = nodeConfig.getAttributes().get("name").iterator().next();
        oldValues.put(Pair.of(Realms.of(realm), nodeId), name);
      }
    }

    // Do the upgrade of the schema
    pluginTools.upgradeAuthNode(MyCustomNode.class);

    // Remove the old value and set the new values
    for (Map.Entry<Pair<Realm, String>, String> nameForUpdate : oldValues.entrySet()) {
      String realm = nameForUpdate.getKey().getFirst().asPath();
      String nodeId = nameForUpdate.getKey().getSecond();
      String name = nameForUpdate.getValue();
      int spaceIndex = name.indexOf(" ");

      ServiceConfig container = configManager.getOrganizationConfig(realm, null);
      ServiceConfig nodeConfig = container.getSubConfig(nodeId);
      nodeConfig.removeAttribute("name");
      nodeConfig.setAttributes(ImmutableMap.of(
        "givenName", singleton(name.substring(0, spaceIndex)),
        "familyName", singleton(name.substring(spaceIndex + 1))));
    }
  } catch (SSOException | SMSException | RealmLookupException e) {
      throw new PluginException("Could not upgrade", e);
  }
  super.upgrade(fromVersion);
}

Internationalize nodes

Internationalization (i18n) of content targets both the end user and the node administrator. Messages sent to users and other UIs can be internationalized.

You can also internationalize error messages and administrator-facing UI using the same mechanism for better user and admin experience.

Internationalized nodes use the locale of the request to find the correct resource bundle, with a default fallback if none is found.

Localize node UI text

  1. Create a Java resource bundle under the resources folder in the Maven project for your node.

    The path and filename must match that of the core class that will use the translated text.

    For example, the resource bundle for the Username Collector node is located in the following path: src/main/resources/org/forgerock/openam/auth/nodes/UsernameCollectorNode.

    The resource bundle for the username collector node as displayed in the project window in the IntelliJ IDE.
    Figure 4. Example resource bundle
  2. Add the properties and strings that the node will display to the user.

    For example:

    callback.username=User Name
  3. Create a .properties file in the resource bundle for each language your node will display.

    The filename must include the language identifier, as per rfc5646 - Tags for Identifying Languages.

    For example, for French translations your .properties file could be called UsernameCollectorNode_fr.properties.

  4. Replicate the properties and translate the values in each .properties files.

    For example:

    callback.username=Nom d'utilisateur
  5. In the core class for your node, specify the path to the resource bundle from which the node will retrieve the translated strings:

    private static final String BUNDLE = "org/forgerock/openam/auth/nodes/UsernameCollectorNode";
  6. Define a reference to the bundle using the getBundleInPreferredLocale function to enable retrieval of translated strings:

    ResourceBundle bundle = context.request.locales.getBundleInPreferredLocale(
            BUNDLE, getClass().getClassLoader());
  7. Use the getString function whenever you need to retrieve a translation from the resource bundle:

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

Build and install Java nodes

This section explains how to build and install Java nodes for use in authentication trees.

Build and install a custom Java node

  1. Change to the root directory of the Maven project of the custom nodes.

    For example:

    $ cd /Users/Ping/Repositories/am-external/openam-auth-trees/auth-nodes
  2. Run the mvn clean package command.

    The project will generate a .jar file containing your custom nodes. For example, auth-nodes-version.jar.

  3. Include the custom .jar file in the AM .war file, as described in Customize the AM .war file.

    Delete or overwrite older versions of the nodes .jar file from the WEB-INF/lib/ folder, to avoid clashes.

    If you are using custom nodes with version 0.0.0 in trees, you must remove them from the trees before restarting AM and reinsert them after the restart. If you do not do this, the entire tree cannot be viewed in the UI after the restart.
  4. Restart AM for the new nodes to become available.

    The custom authentication node is now available in the tree designer to add to authentication trees:

    Custom nodes appear alongside built-in authentication nodes in the Administration console.
    Figure 5. Custom node in a tree

    Learn more about using the tree designer to manage authentication trees in Configure authentication trees.

For information on upgrading custom nodes, refer to Upgrade nodes and change node configuration.

Post-installation tasks

This page covers post-installation tasks relating to authentication nodes, such as testing, debugging, auditing, and performance monitoring.

Test nodes

You can test authentication nodes in multiple ways, including unit tests, functional tests, and performing exploratory or manual testing.

Authentication nodes are well suited to tests that have a high percentage of code coverage. The low number of static dependencies lets you unit test the node class as well as the business logic classes.

Unit tests

Your unit tests should aim to cover a high percentage of the code. Most of the business logic is defined by the tree layout instead of within the nodes themselves, which simplifies unit testing.

At a minimum, the process(TreeContext context) method must be tested to make sure all appropriate code paths are triggered, based on whether appropriate values in the shared state and callbacks exist.

The TreeContext class and contents have been designed to simplify unit tests without needing to mock.

Functional tests

Functional tests involve creating an authentication tree in an AM instance and testing it using the REST API. You should write them to cover all the possible authentication flows, including successful and unsuccessful outcomes.

Consider the following when writing functional tests:

  • All relevant code paths discovered through unit testing should be functionally tested to ensure helper, utility, and related mechanisms function as expected.

  • Functional tests must make sure the business logic is called correctly and processed as expected.

  • Mocking expected services can be useful when functionally testing nodes that make calls to third-party services.

To perform functional testing, write a series of REST requests to create the authentication tree and test all the identified flows through the authentication tree. The REST API returns a JSON payload, which lets you programmatically step through each node and collect responses at every stage.

You can find details about creating a tree using the REST API in Create an authentication tree over REST.

Test your tree in the UI first and use the Developer Tools in your browser to copy relevant web requests as a curl command. The resulting curl command includes all the headers, options, and data sent for the selected web request. You can use this information and the REST syntax as the basis of your REST calls.

You can use a third-party tool such as Postman for REST-based testing.

Manual testing

Manual testing should occur both during and after node development.

During development, it is expected a node developer will frequently load and reload nodes to ensure they operate as expected, including configuration and execution, as well as any expected error conditions.

After development, manual testing should continue in an exploratory fashion. Using a node multiple times can often highlight areas left unpolished, or particular usability issues that can be missed by automated testing.

Debug nodes

You might need to debug your nodes during development as well as prepare your nodes to allow debug logging after they are deployed.

Use the Debug node

During development, it can be useful to include one or more Debug nodes in your authentication tree to inspect the tree state.

  1. Insert the Debug node in the tree between the node you want to inspect and the next node in the flow. For example, if you wanted to inspect the Custom Node, where the next node was a Scripted Decision node, you would insert the Debug node as follows:

    Debug Node
  2. Save your changes.

  3. Test your tree in a new incognito browser window (or a separate browser).

    When the Debug node is reached, a pop-up window displays details about the tree state.

    If the browser blocks the pop-up window, unblock it:

    Refresh the browser window. The pop-up window should now appear.

  4. Remove the Debug node once you finish debugging.

Attach a debugger to Apache Tomcat

During development, you may want to attach a remote debugger to Tomcat to let you debug the node development with an IDE (for example, to set breakpoints).

  1. Set a CATALINA_OPTS environment variable to enable the debugger on Tomcat. For example, add the following in your setenv file:

    • Linux

    • Windows

    In $CATALINA_BASE/bin/setenv.sh:

    export CATALINA_OPTS="$CATALINA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,
    address=*:8000,server=y,suspend=n"

    In $CATALINA_BASE/bin/setenv.bat:

    set "CATALINA_OPTS=%CATALINA_OPTS% -Xdebug -Xrunjdwp:transport=dt_socket,
    address=*:8000,server=y,suspend=n"

    The *: in the address option lets you debug remote servers as well as developments running on localhost.

  2. Start the web container as normal.

  3. Connect your IDE to Tomcat as a remote JVM debug instance. The way you do this depends on your IDE, but make sure the following settings are correct:

    • Port: the port in the IDE debug configuration must match the port set in the setenv file (8000 in this example).

    • Address: the address should either be the IP address of the Tomcat instance if it’s running on a different machine or localhost.

  4. Remove the debugger options from your setenv file once you finish debugging.

Add debug logging

Add debug logging to your custom node to help administrators and support staff investigate any issues that arise in production.

To add debug logging to a node, include a reference to the amAuth SLF4J Logger instance.

For example, you can assign the logger to a private field as follows:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// ...

private final Logger logger = LoggerFactory.getLogger("amAuth");

Consider the logging level you use. Excessive use of the error or warning level can cause debug logs to fill, and can have a negative performance impact, if your node is used frequently.

You can also use the SLF4J varargs methods to defer string concatenation to SLF4J. This means you can skip string concatenation if the configured logging level means your message won’t be written.

The following example uses the debug level:

logger.debug("authLevelSufficient {}", authLevelSufficient);

Audit logging

Audit logging helps administrators to investigate user and system behavior.

AM records all incoming calls as access events. Additionally, to capture further details about the authentication flows, AM records an authentication audit event for each node, and the tree outcome.

A node can provide extra data to be included in the standard audit event which is logged when an authentication node completes.

AM logs an AM-NODE-LOGIN-COMPLETED audit event each time an authentication node completes. To add extra information to this audit event, override the node interface method getAuditEntryDetail.

For example, the Retry Limit Decision node overrides this method to record how many retries remain:

@Override
public JsonValue getAuditEntryDetail() {
  return json(object(field("remainingRetries", String.valueOf(retryLimitCount))));
}

When this node is processed, it results in an audit event similar to the following:

{
  "realm": "/",
  "transactionId": "45453155-cf94-4e23-8ee9-ecdfc9f97e12-1785617",
  "component": "Authentication",
  "eventName": "AM-NODE-LOGIN-COMPLETED",
  "entries": [
    {
      "info": {
        "nodeOutcome": "Retry",
        "treeName": "Example",
        "displayName": "Retry Limit Decision",
        "nodeType": "RetryLimitDecisionNode",
        "nodeId": "bf010b6b-61f8-457e-80f3-c3678e5606d2",
        "authLevel": "0",
        "nodeExtraLogging": {
          "remainingRetries": "2"
        }
      }
    }
  ],
  "timestamp": "2018-08-24T09:43:55.959Z",
  "trackingIds": [
    "45453155-cf94-4e23-8ee9-ecdfc9f97e12-1785618"
  ],
  "_id": "45453155-cf94-4e23-8ee9-ecdfc9f97e12-1785622"
}

The result of the getAuditEntryDetail method is stored in the nodeExtraLogging field.

Monitor nodes

You can track authentication flows that complete with success, failure, or timeout as an outcome by using the metrics functionality built-in to AM.

Learn more in Monitor AM instances.

You can also use the following nodes in a tree to create custom metrics:

Troubleshoot node development

This page offers solutions to issues that may occur when developing authentication nodes.

I installed my node in AM. Why doesn’t it appear in the authentication tree designer?

The authNodeName.properties file for your node must include a nodeDescription property for your node to appear in the authentication tree designer.

AM uses the nodeDescription property value as the name of your node.

How do I get new attributes to appear in the node after the service has been loaded once?

Learn more in Upgrade nodes and change node configuration.

What type of exception should I throw so that the framework handles it gracefully?

To display a custom message to the user, exceptions must be handled inside the node and an appropriate information callback returned.

Learn more in Handle errors.

Do I need multiple projects/jars for multiple nodes?

No — you can bundle multiple nodes into one plugin and deploy that plugin in a single .jar file.

What utilities exist for me to use to assist in the node building experience?

A number of utilities are available for use in your integrations and custom nodes.

Learn more in the AM Public API Javadoc.

Transient state vs shared state — when should I use one or the other?

Use transient state for secret values that shouldn’t persist.

If my service collects a username in a different way from the Username Collector node, where do I put the username from the framework to get the principal?

Learn more in Access an identity’s profile.

Where do I go for examples of authentication nodes?

There are many public examples of community nodes at https://github.com/ForgeRock.

Find sample community nodes written by third parties on the Marketplace website.

For source access to the authentication nodes included with AM, read How do I access and build the sample code provided for PingAM?.