PingAM

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 UsernameCollectorNodeV2 class shows the steps to implement the Node interface:

package org.forgerock.openam.auth.nodes;

import static java.util.Objects.requireNonNull;
import static org.forgerock.openam.auth.node.api.Action.send;
import static org.forgerock.openam.auth.node.api.SharedStateConstants.USERNAME;

import java.util.ResourceBundle;

import javax.inject.Inject;
import javax.security.auth.callback.NameCallback;

import org.forgerock.openam.annotations.sm.Attribute;
import org.forgerock.openam.auth.node.api.Action;
import org.forgerock.openam.auth.node.api.InputState;
import org.forgerock.openam.auth.node.api.Node;
import org.forgerock.openam.auth.node.api.OutputState;
import org.forgerock.openam.auth.node.api.SingleOutcomeNode;
import org.forgerock.openam.auth.node.api.TreeContext;
import org.forgerock.openam.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.assistedinject.Assisted;

/
 * A node that collects a username from the user using a name callback.
 *
 * <p>Allows setting of a customizable, localizable prompt and a default value retrieved from shared state</p>
 *
 * <p>Places the result in the shared state as 'username'.</p>
 /
@Node.Metadata(outcomeProvider = SingleOutcomeNode.OutcomeProvider.class,              1
        configClass = UsernameCollectorNodeV2.Config.class,
        tags = {"basic authn", "basic authentication"})
@Node.VersionMetadata(name = "UsernameCollectorNode", version = 2)                     2
public class UsernameCollectorNodeV2 extends SingleOutcomeNode {                       3

    private final Config config;                                                       4

    @Inject                                                                            5
    UsernameCollectorNodeV2(@Assisted Config config) {
        this.config = config;
    }
    /
     * Configuration for the username collector node V2.
     */
    public interface Config {                                                          6
        /*
         * If true, will prepopulate the username field with a value from shared state.
         *
         * @return Whether to prepopulate the username input field with a value from shared state
         */
        @Attribute(order = 100)
        default boolean prepopulate() {
            return false;
        }
    }

    private static final String BUNDLE = UsernameCollectorNodeV2.class.getName();
    private final Logger logger = LoggerFactory.getLogger(UsernameCollectorNodeV2.class);

    @Override                                                                          7
    public Action process(TreeContext context) {
        logger.debug("UsernameCollectorNode started");
        var nameCallback = context.getCallback(NameCallback.class);
        if (nameCallback.isPresent() && StringUtils.isNotEmpty(nameCallback.get().getName())) {
            context.getStateFor(this).putShared(USERNAME, nameCallback.get().getName());
            return goToNext().build();
        }

        return collectUsername(context);
    }

    private Action collectUsername(TreeContext context) {
        logger.debug("collecting username");
        ResourceBundle bundle = context.request.locales.getBundleInPreferredLocale(BUNDLE, getClass().getClassLoader());
        var nameCallback = new NameCallback(bundle.getString("callback.username"));
        var nodeState = context.getStateFor(this);
        if (config.prepopulate() && nodeState.isDefined(USERNAME)) {
            nameCallback.setName(requireNonNull(nodeState.get(USERNAME)).asString());
        }
        return send(nameCallback).build();
    }

    @Override
    public OutputState[] getOutputs() {
        return new OutputState[]{
                new OutputState(USERNAME)
        };
    }

    @Override
    public InputState[] getInputs() {
        return new InputState[]{
                new InputState(USERNAME)
        };
    }
}
Step Description Further information

1 Apply the @Node.Metadata annotation

The Metadata annotation specifies the outcome provider, configuration class, and optionally, the configuration validator, extensions 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 Apply the @Node.VersionMetadata annotation

Create a versioned node.

The VersionMetadata annotation specifies the name, version, and optionally, the upgrader class.

3 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:

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 Implement the Config interface

The Config interface defines the configuration data for a node.

7 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 or 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 is true or false, resulting in the authentication tree flow moving from the current node to a node at the relevant connection.