Node development
These topics provide guidance and best practices for developing and maintaining authentication nodes in AM.
About authentication nodes
Learn how authentication nodes define actions taken during authentication.
Prepare your environment
Discover the prerequisites for building and customizing authentication nodes.
Translate nodes
Internationalize the text in your nodes.
Build and install nodes
Find out how to build and install authentication nodes for use in authentication trees.
For information on configuring and using authentication trees, see Authentication nodes and trees.
ForgeRock® Identity Platform serves as the basis for our simple and comprehensive Identity and Access Management solution. We help our customers deepen their relationships with their customers, and improve the productivity and connectivity of their employees and partners. For more information about ForgeRock and about the platform, see https://www.forgerock.com.
Authentication nodes
Authentication trees (also referred to as intelligent authentication) provide fine-grained authentication by allowing multiple paths and decision points throughout the authentication flow.
Authentication trees are made up of authentication nodes, which define actions taken during authentication, similar to authentication modules in chains.
You can create complex yet customer-friendly authentication experiences by linking nodes together, creating loops, and nesting nodes within a tree, as follows:
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.
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.
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 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.
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. |
Compare nodes and modules
Authentication modules contain multiple states, with the associated inputs and outputs of each state resulting in either callbacks returned to the user, or state changes inside either AM or at a third party service.
States within a module either collect input from the user or process it. For example, a module can collect the username and the password from the user, then authenticate the user against the data store. When finished, the module decides whether to return a boolean success or failure flag.
The outcome of an authentication module can only be success or failure. Any branching or looping must be handled within the module. An authentication mechanism is implemented in full within a single module, rather than across multiple modules. Therefore, authentication modules can become large - handling multiple steps within the flow of an authentication journey.
Authentication nodes, however, can have an arbitrary number of outcomes that do not need to represent success or failure. Branching and looping are handled by connecting nodes within the tree and are fully controlled by the tree administrator, rather than the node developer. Nodes are often considerably smaller in terms of code size, and are responsible for handling a single step within the authentication flow. For example, an individual node could capture user input, another could make a decision based on available state, and another could invoke an external API.
Nodes expose this approach to the tree administrator. Unlike modules, where the journey through the module’s states is defined by the module’s developer, a journey through a collection of nodes may be different for each user.
Node developers should be aware of the expectations for a node to deliver a limited amount of specific functionality, which tree administrators can connect together in a variety of ways.
-
Nodes are responsible for a single step of the authentication flow. Modules are responsible for an entire authentication mechanism.
-
Tree administrators control the branching, looping, and sequencing of steps by linking nodes together. Module developers set these state transitions within the module itself.
-
Nodes are stateless; instances of node objects are not retained between HTTP requests, and all state captured must be saved to the authentication session’s shared state. Modules store state within the module object.
-
Node configuration is handled with annotations. Modules use XML.
-
Nodes are easier to test due to their smaller code size and their specific functionality.
Convert modules to nodes
The ease of transitioning from a module to a node depends on the amount of functionality provided in the module. As nodes are more fine-grained than modules, split the functionality of the module into individual nodes which can then work together to provide functionality similar to the module, but in a more flexible manner.
An example of this approach was applied to the one-time password nodes, which were developed from the HOTP authentication module. The module performs the following duties:
-
Generates one-time passwords.
-
Sends one-time passwords by using SMS messaging.
-
Sends one-time passwords by using SMTP.
-
Collects and verifies the one-time password.
The four distinct functions are encapsulated into separate nodes, allowing greater control over the one-time password process.
For example, a tree administrator who is only interested in sending one-time passwords by using SMS messages can omit the SMTP node. Separating out the decision functionality means that it can be combined with another decision node, or simply routed to an alternative authentication process.
Some authentication modules delegate their functionality to utility classes in AM, which simplifies the process of creating a similarly functioning node.
For example, the LDAP authentication module and LDAP Decision node
share the LDAPAuthUtils
class for LDAP authentication.
In cases where such utility classes do not exist,
consider extracting the common functionality used by the module into such a class,
so that it can be more easily used by nodes.
For information on sharing configuration,
see Share configuration between nodes.
Restrict a node’s functionality
To determine the functionality of a node, reduce the node’s responsibility to its core purpose, while ensuring it performs sufficient tasks to be useful as a step in an authentication journey.
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 which, on their own, do not 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. (This is similar to the state mechanism used by modules.)
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.
For information about customizing post-authentication hooks for a tree, see Create post-authentication hooks for trees. |
Prepare an environment for building custom authentication nodes
-
Make sure that your BackStage account is part of a subscription:
-
In a browser, go to the ForgeRock BackStage website and log in or register for an account.
-
Confirm or request that your account is added to a subscription. For more details, see "Getting access to product support" in the ForgeRock Knowledge Base.
-
-
Install Apache Maven 3.2.5 or later, and Oracle JDK or OpenJDK version 11 or later.
To verify the installed versions, run the
mvn --version
command:$ mvn --version Maven home: /usr/local/Cellar/maven/3.6.0/libexec Java version: 11.0.4, vendor: AdoptOpenJDK, runtime: /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home Default locale: en_US, platform encoding: UTF-8 OS name: "mac os x", version: "10.16", arch: "x86_64", family: "mac"
-
Configure Maven to be able to access the ForgeRock repositories by adding your BackStage credentials to the Maven
settings.xml
file. For details, see "How do I access the ForgeRock protected Maven repositories?" in the ForgeRock Knowledge Base.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 yoursettings.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:443/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
ForgeRock 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:
-
To use the ForgeRock
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 that 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 that is registered through a plugin is stored with the name returned as a result of calling
Class.getSimpleName()
. AM does not 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 that the name does not collide with the names of existing nodes.Alternatively, use the
namespace
property in the@Node.Metadata
annotation to change the name that is saved for the node. For example, addingnamespace=Namespace.PRODUCT
to theWriteFederationInformationNode
results in that node being stored asproduct-WriteFederationInformationNode
.For example:
$ mvn archetype:generate \ -DgroupId=com.example \ -DartifactId=customAuthNode \ -Dversion=1.0.0-SNAPSHOT \ -Dpackage=com.example.customAuthNode \ -DauthNodeName=myCustomAuthNode \ -DarchetypeGroupId=org.forgerock.am \ -DarchetypeArtifactId=auth-tree-node-archetype \ -DarchetypeVersion=7.4.0 \ -DinteractiveMode=false [INFO] Project created from Archetype in dir: /Users/ForgeRock/Repositories/customAuthNode [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.397 s [INFO] Finished at: 2018-01-18T15:45:06+00:00 [INFO] Final Memory: 16M/491M [INFO] ------------------------------------------------------------------------
A new custom authentication node project is created; for example, in the
/Users/ForgeRock/Repositories/customAuthNode
folder.Example
Figure 2. Node project created by using the archetype
-
-
To download the project containing the default AM authentication nodes from the
am-external
repository:-
If you do not already have a ForgeRock BackStage account, get one from https://backstage.forgerock.com. You cannot clone the
am-external
repository without a BackStage account. -
Clone the
am-external
repository:URL-encode your BackStage Username if it contains special characters. For example, : becomes
%3A
and @ becomes%40
.Enter your BackStage password when prompted to do so.
-
Check out the
release/7.4.0
branch:$ cd am-external$ git checkout releases/7.4.0
The AM authentication nodes project is located in the
am-external/openam-auth-trees/auth-nodes/
folder.Example
Figure 3. Node Project Cloned from ForgeRock
-
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 that which you compiled against.
ForgeRock endeavours to make nodes from previous product versions binary compatible with subsequent product versions, so a node built against AM 6 APIs may be deployed in an AM 7.4.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 ForgeRock product dependencies as
provided
in your build system configuration. -
Repackage all non-internal, non-ForgeRock 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.
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.
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. See Node class.
authNodeNamePlugin.java
-
Plugin class for the custom authentication node. See Plugin class.
authNodeName.properties
-
Properties file containing the localized strings displayed by the custom authentication node. See Internationalization.
You must include a
nodeDescription
property in your node to ensure that it appears in the authentication tree designer. AM uses thenodeDescription
property value as the name of your node.
The authNodeName reflects the name of your authentication node.
For example, the ForgeRock auth-tree-node-archetype
for Maven uses myCustomAuthNode
as the authNodeName.
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 SetSessionProperties
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 user’s 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 |
The annotation specifies the outcome provider, configuration class, and optionally, the configuration validation class and tags. Use an existing outcome provider such as |
|
2 Implement the |
Extend one of the following abstract classes to implement the
Alternatively, write your own implementation of the |
Javadoc:
|
3 Implement the |
The |
|
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 |
|
6 Override the |
The It takes a The method returns an The choice of outcome in a simple decision node would be |
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.
Optionally, you can also specify a class as a configValidator
, and tags
to categorize the
node type for display in the UI.
outcomeProvider
-
The class that defines the possible outcomes.
The abstract implementations of the node interface,
org.forgerock.openam.auth.node.api.SingleOutcomeNode
andorg.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.For example, the following is the custom outcome provider from the LDAP Decision node, which has
True
,False
,Locked
,Cancelled
, andExpired
exit paths:/** * Defines the possible outcomes from this Ldap node. */ public static class LdapOutcomeProvider implements OutcomeProvider { @Override public List<Outcome> getOutcomes(PreferredLocales locales, JsonValue nodeAttributes) { 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"))); } }
configClass
-
The class name that contains the configuration of any attributes requested by the node when using it as part of a tree.
For more information, See Config interface.
configValidator
-
An optional class name used to validate the provided configuration.
tags
-
An optional 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"
MFA
"mfa"
Risk
"risk"
Behavioral
"behavioral"
Contextual
"contextual"
Federation
"federation"
Identity Management
"identity management"
Anonymous User Mapping node
Terms and Conditions Decision nodeUtilities
"utilities"
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"})
For more information on the @Node.Metadata
annotation,
see the Node.Metadata annotation type
in the AM Public API Javadoc.
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. For further information, refer to Internationalization.
For more information, refer to the Config annotation type in the AM Public API Javadoc.
The @Attribute
annotation
The @Attribute
annotation is required. You must specify an integer value for order
,
which determines the position of the attribute in the UI.
Optionally, specify one or more validators
to validate the attribute values provided.
To include a validator:
-
Use an existing validator class from the
org.forgerock.openam.auth.nodes.validators
package, such asDecimalValidator
orHMACKeyLengthValidator
. -
Create your own validator by implementing the ServiceAttributeValidator interface.
For example:
public interface Config {
@Attribute(order = 1)
String domain(); 1
@Attribute(order = 2, validators = {RequiredValueValidator.class, 2
GreaterThanZeroValidator.class})
int exampleNumber();
@Attribute(order = 3, requiredValue = true)
boolean isVerificationRequired(); 3
@Attribute(order = 4)
@Password 4
char[] clientSecret();
@Attribute(order = 5)
default YourCustomEnum action() {
return YourCustomEnum.LockScreen; 5
};
}
1 The domain
attribute defines a String-typed 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. Specifying requiredValue=true
is equivalent to including the parameter validators = RequiredValueValidator.class
.
4 Use the Password annotation to mask the input characters and encrypt the value of the attribute.
5 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.
For more information, refer to the Attribute annotation type in the AM Public API Javadoc.
Share configuration between nodes
You can share configuration between nodes that have common properties. For example, a number of nodes may call out to an external service that requires a username, password, IP address, and port setting.
Instead of repeating the same configuration in each of these nodes, create a shared, auxiliary service to hold the common properties in one node. You can then reference that service from other nodes.
The following sections explain how to create and reference this auxiliary service. The sections also describe how to run more than one instance of an auxiliary service if required, and how to obtain the configuration from services built-in to AM.
Create a shared auxiliary service
You can create a shared auxiliary service in the configuration interface defined as part of a node.
Annotate the service with the org.forgerock.openam.annotations.sm.Config
annotation
to describe how the service functions.
Specify the scope of the service, either GLOBAL
or REALM
, as shown below:
@Config(scope = Config.Scope.REALM)
public interface MyAuxService {
@Attribute(order = 1)
String serviceUrl();
}
You can also specify other features of the service, such as whether the service is a singleton in its scope, or if it can have multiple instances. For information about supporting multiple instances, refer to Allow multiple instances of an auxiliary service.
Reference a shared auxiliary service instance
To access the shared auxiliary service,
add org.forgerock.openam.sm.AnnotatedServiceRegistry
to the @Inject
-annotated constructor of the node.
Obtain the instance using the get instance methods on that class, for example:
serviceRegistry.getRealmSingleton(MyAuxService.class, realm)
Reinstall a shared auxiliary service instance
When developing a custom authentication node that references a shared auxiliary service, it can be useful for the node to be able to remove and reinstall the auxiliary service during upgrade, so that any existing configuration is cleared.
In the upgrade
function of your plugin class, use the following example code to remove and reinstall a service:
public void upgrade(String fromVersion) throws PluginException {
SSOToken adminToken = AccessController.doPrivileged(AdminTokenAction.getInstance());
if (fromVersion.equals(PluginTools.DEVELOPMENT_VERSION)) {
ServiceManager sm = new ServiceManager(adminToken);
if (sm.getServiceNames().contains("MyAuxService")) {
sm.removeService("MyAuxService", "1.0");
}
pluginTools.install(MyAuxService.class);
}
}
For more information on upgrading custom authentication nodes, refer to Upgrade nodes and change node configuration.
Allow multiple instances of an auxiliary service
To enable configuration of multiple instances of the auxiliary service in either the same realm or at a global level,
set the collection
attribute to true
in the Config
annotation.
You can present the names of the instances of the service as a drop-down menu to the tree administrator.
To be able to present the names, make sure the service instance exposes its id
, as follows:
@Config(scope = Config.Scope.REALM, collection = true)
public interface MyAuxService {
@Id
String id();
@Attribute(order = 1)
String serviceUrl();
}
Change the nodes that will be using a service instance to store the id
it uses,
and implement choiceValuesClass
as shown below:
public class MyCustomNode implements Node {
public interface Config {
@Attribute(order = 1, choiceValuesClass = ExternalServiceValues.class)
String serviceId();
}
public static class ExternalServiceValues extends ChoiceValues {
@Override
public Map<String, String> getChoiceValues() {
return getChoiceValues(null);
}
@Override
public Map<String, String> getChoiceValues(Map envParams) {
String realmName = "/";
if (envParams != null) {
realmName = (String) envParams.getOrDefault(Constants.ORGANIZATION_NAME, "/");
}
try {
return InjectorHolder.getInstance(AnnotatedServiceRegistry.class)
.getRealmInstances(MyAuxService.class, Realms.of(realmName))
.stream()
.collect(Collectors.toMap(MyAuxService::id, MyAuxService::id));
} catch (SSOException | SMSException | RealmLookupException e) {
LoggerFactory.getLogger("amAuth").error("Couldn't load realm {}", realmName, e);
throw new IllegalStateException("Couldn't load realm that was passed", e);
}
}
}
// ...
}
Get configuration of built-in services
You can obtain configuration from services built-in to AM. For example, access the Email Service configuration to obtain the SMTP settings for the realm.
AM services are defined by two methods:
-
An annotated interface (most services)
-
An XML file (legacy services)
The following sections describe how to obtain the configuration from services defined using these two methods.
Get configuration from an annotated service
To obtain the configuration from a service that uses an annotated interface,
add org.forgerock.openam.sm.AnnotatedServiceRegistry
to your Guice constructor.
If the configuration is realm-based, include the realm in the constructor, as follows:
public class MyCustomNode extends SingleOutcomeNode {
private final AnnotatedServiceRegistry serviceRegistry;
private final Realm realm;
@Inject
public MyCustomNode(@Assisted Realm realm, AnnotatedServiceRegistry serviceRegistry) {
this.realm = realm;
this.serviceRegistry = serviceRegistry;
}
// ...
}
Obtain an instance of the service using one of the get methods of AnnotatedServiceRegistry
in the constructor.
If the calls you make depend on input from elsewhere in the tree
you can add AnnotatedServiceRegistry
to the process
method.
Note that the following example assumes
that a previous node has stored the ID of the AM service to use in shared state:
public Action process(TreeContext context) throws NodeProcessException {
String serviceId = context.getState.get("myAuxServiceId");
MyAuxService instance = serviceRegistry.getRealmInstance(MyAuxService.class, realm, serviceId);
// ...
}
Get configuration from a legacy service
To obtain an instance of the configuration from a legacy service, use the APIs in the com.sun.identity.sm
package.
For example, to obtain the configuration values from a realm instance of a service,
use ServiceConfigManager
as follows:
ServiceConfigManager scm = new ServiceConfigManager("legacyServiceName", token);
ServiceConfig sc = scm.getOrganizationConfig(realm.asPath(), null);
final Map<String, Set<String>> configMap = sc.getAttributes();
However, to obtain the configuration values from a global instance of a service,
use ServiceSchemaManager
as follows:
ServiceSchemaManager ssm = new ServiceSchemaManager("legacyServiceName", getAdminToken());
Map<String, Set<String>> configMap = ssm.getGlobalSchema().getAttributeDefaults();
Inject objects into a node instance
A node instance is constructed every time that node is reached in a tree and is discarded as soon as it has been used to process the state once.
This model is different to authentication modules which are instantiated once for each end-user authentication process. All authentication interactions for the life of the authentication process address the same instance in the same JVM.
Modules can store state in the module instance; however, 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 node interface supports sending one-time password (OTP) scripts in plain text notification only. You can’t include HTML-rich notifications that use information from shared or transient state. If you need to support HTML notifications, you can use a Groovy script with a private HTTP client that makes the REST API calls and places the output in a scripted decision node. |
The following example is the configuration injection used by the Debug node:
@Inject
public DebugNode(@Assisted DebugNode.Config config) {
this.config = config;
...
}
For more information, refer to 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, 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
}
}
Custom Guice bindings
If just-in-time bindings are not 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
// Refer to 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();
}
For more information, refer to the Action class in the AM Public API Javadoc.
Action fields and methods
The Action
class uses the following fields:
Fields | Description |
---|---|
|
A list of the callbacks requested by the node.
This list may be |
|
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. |
|
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. |
|
The result of the node. |
|
A map of properties returned to the client. Use |
|
The list of classes implementing the TreeHook interface that run after a successful login. |
|
A map of properties added to the final session if the authentication tree completes successfully. Use |
|
State that AM shares between nodes through the tree context—the properties set so far by nodes in the tree. Refer to Store values in shared tree state. |
|
The list of webhooks that run after logout. |
The Action
class provides the following methods:
Methods | Description |
---|---|
|
Specify the exit path to take, and move on to the next node in the tree. For example:
|
|
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
|
|
Returns true if the action is a request for input from the user. |
|
Suspends the authentication tree, and lets the user resume it from the point it was suspended. For example, the following call is taken from the Email Suspend node:
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 |
---|---|
|
Add a node type to the session properties and shared state. Replace any existing shared state with the specified TreeContext’s shared state. |
|
Add one or more session hook classes for AM to run after a successful login. |
|
Add one or more webhook names to the list of webhooks. |
|
Creates and returns an Action instance providing the mandatory fields are set. |
|
Add a new session property. |
|
Remove the specified session property. |
|
Replace the current shared state with the specified shared state. |
|
Replace the current transient state with the specified transient state. |
|
Set a custom message for when the authentication tree reaches the failure node. |
|
Add an identity, authenticated or not, that is confirmed to exist in an identity store.
Specify the username and identity type or an Use this method to record the type of identified user. If the advanced server property,
This lets the authentication tree engine correctly resolve identities that have the same username. For more information, refer to advanced server properties. |
|
Set a custom message for when the user is locked out. |
|
Set a header for this action. |
|
Set a description for this action. |
|
Add a property to the list that is returned to the client. |
|
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. |
|
Deprecated. Use |
Store 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.
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 action()
method.
AM ensures that 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 ID. Downstream consumers, such as IDM user self-service nodes, must have the same key to decrypt and read it.To ensure that the authentication flow is not bloated with calls to encrypt/decrypt data, and to ensure the authentication session size stays small, limit what you store with
putTransient()
. This is especially true when the realm is configured for client-side authentication 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.
For details, see 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.ifDefined(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:
-
transient
-
secure
-
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.
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 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 store a verified identity in the authentication session, call |
Read an identity’s profile
Use the IdUtils
static class:
AMIdentity id = IdUtils.getIdentity(username, realm);
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) {
ServiceConfigManager mgr = new ServiceConfigManager(
ISAuthConstants.AUTH_SERVICE_NAME,
AccessController.doPrivileged(AdminTokenAction.getInstance());
ServiceConfig serviceConfig = mgr.getOrganizationConfig(realm, null);
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.
Include callbacks
Nodes use callbacks to enable interaction with the authenticating user.
AM doesn’t support creating your own custom 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.
Below is 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
);
Note that the order of callbacks defined in 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
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.
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:
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,
annotate your config property with @RequiredValueValidator
, as follows:
@Attribute(order = 300, validators = {RequiredValueValidator.class})
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.
For more information, see Internationalization.
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 String currentVersion = "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. -
FIRST_TIME_DEMO_INSTALL
. The AM deployment has been installed for the first time, using an embedded data store as the config and user stores. -
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.
For more information, see 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
toSet<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);
}
Internationalization
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
-
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
.Figure 4. Example resource bundle -
Add the properties and strings that the node will display to the user.
For example:
callback.username=User Name
-
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 calledUsernameCollectorNode_fr.properties
. -
Replicate the properties and translate the values in each
.properties
files.For example:
callback.username=Nom d'utilisateur
-
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";
-
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());
-
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 nodes
This section explains how to build and install authentication nodes for use in authentication trees.
Build and install a custom authentication node
-
Change to the root directory of the Maven project of the custom nodes.
For example:
$ cd /Users/Forgerock/Repositories/am-external/openam-auth-trees/auth-nodes
-
Run the
mvn clean package
command.The project will generate a
.jar
file containing your custom nodes. For example,auth-nodes-version.jar
. -
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 theWEB-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. -
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:
Figure 5. Custom node in a treeFor more information on using the tree designer to manage authentication trees, refer to Configure authentication trees.
For information on upgrading custom nodes, refer to Upgrade nodes and change node configuration.
Maintain authentication nodes
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 numerous ways. For example, you can use unit tests, functional tests, and perform 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 means that unit testing of the node class itself can occur, rather than simply the business logic classes, as was the case for modules. Furthermore, as nodes should be significantly smaller than modules, testing iterations should be much shorter.
Unit tests
Your unit tests should aim for an appropriately high-level percentage of coverage of the code. Unit testing becomes easier with nodes, as most of the business logic is defined by the tree layout, rather than in the nodes themselves.
At minimum, the process(TreeContext context)
method should be tested
to ensure that all appropriate code paths are triggered based on the existence, or lack of, appropriate values
in the shared state and callbacks.
The TreeContext
class and contents have been designed to make sure they are simple to use in unit tests,
without the need to resort to mocking.
Functional tests
Functional tests involve deploying the node into an AM instance and testing it using the authentication REST API. They should be written to cover all normal flows through the node.
All the appropriate code paths discovered through unit testing should be functionally tested to ensure that helper, utility, and related mechanisms function as expected.
Additionally, functional tests will ensure that the business logic is correctly called and processed as expected.
Mocking expected services may be useful when functionally testing nodes that call out to third-party services. |
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. Simply using a node numerous times can often highlight areas left unpolished, or particular usability issues that may be missed by automated testing.
Debug nodes
Add debug logging to your custom node to help administrators and support staff investigate any issues which may arise in production.
To add debug logging to a node, obtain 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 |
You can also use the SLF4J varargs
methods to defer string concatenation to SLF4J.
This means string concatenation can be skipped
if the configured logging level means that your message will not 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, in order to capture further details regarding 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 which complete with success, failure, or timeout as an outcome by using the metrics functionality built-in to AM.
For more information, see 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 anodeDescription
property to ensure that that your node appears 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?
-
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.
For more information, see Handle errors.
-
Do I need multiple projects/jars for multiple nodes?
No - you can bundle multiple nodes into one plugin, which should be deployed in one single
.jar
file. -
What ForgeRock 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.
See the AM Public API Javadoc.
-
Transient State vs Shared State - When should I use one or the other?
Transient state is used for secret values that should not 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?
-
Where do I go for examples of authentication nodes?
There are many public examples of ForgeRock community nodes at https://github.com/ForgeRock.
Examples of community nodes written by third parties may be found on the ForgeRock Marketplace.
For source access to the authentication nodes builtin to AM, see How do I access and build the sample code provided for AM/OpenAM (All versions)? in the ForgeRock Knowledge Base.