Custom authentication nodes in ForgeRock Access Manager (AM) 7.5 can significantly enhance your identity and access management strategies by allowing tailored authentication processes. However, developing these nodes can be tricky if you’re not familiar with the underlying architecture and best practices. In this post, I’ll walk you through the process, share some hard-won insights, and provide code examples to help you build robust custom nodes.

The Problem

ForgeRock AM provides a rich set of built-in authentication nodes to cover most use cases, but sometimes you need something unique. Maybe you want to integrate with a specific third-party service or implement a custom authentication mechanism. That’s where custom authentication nodes come in. But getting them right can be challenging, especially if you hit roadblocks during development and testing.

Setting Up Your Development Environment

Before diving into coding, ensure your environment is set up correctly. You’ll need:

  • JDK 11 or later
  • Maven
  • ForgeRock AM 7.5 installed and running
  • IDE (IntelliJ IDEA or Eclipse recommended)

Common Pitfall: Incorrect JDK Version

Using the wrong JDK version can lead to compilation errors. Always check your pom.xml for the correct Java version and make sure your IDE is configured to use it.

<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>

Creating a Custom Authentication Node

Let’s create a simple custom node that checks if a user’s email domain matches a predefined list of allowed domains. This is useful for restricting access based on email addresses.

Step 1: Create a New Maven Project

Start by creating a new Maven project. You can use the following command:

mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=custom-auth-node \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false

Step 2: Add Dependencies

Edit your pom.xml to include dependencies for ForgeRock AM SDKs:

<dependencies>
    <dependency>
        <groupId>org.forgerock.openam</groupId>
        <artifactId>openam-core</artifactId>
        <version>7.5</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.forgerock.openam</groupId>
        <artifactId>openam-shared</artifactId>
        <version>7.5</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Step 3: Implement the Node Logic

Create a new Java class for your custom node. Let’s call it EmailDomainCheckNode.java.

package com.example;

import com.google.inject.Inject;
import org.forgerock.openam.auth.node.api.*;
import org.forgerock.openam.auth.node.api.Action.ActionBuilder;
import org.slf4j.Logger;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import java.util.List;
import java.util.Set;

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

    private static final String BUNDLE = "com/example/EmailDomainCheckNode";
    private final Logger logger;
    private final Config config;

    public interface Config {
        @Attribute(order = 100)
        default Set<String> allowedDomains() {
            return Set.of("example.com", "test.com");
        }
    }

    @Inject
    public EmailDomainCheckNode(@Assisted Config config, @Assisted Logger logger) {
        this.config = config;
        this.logger = logger;
    }

    @Override
    public Action process(TreeContext context) throws NodeProcessException {
        NameCallback nameCallback = context.getCallback(NameCallback.class).orElseThrow();
        String username = nameCallback.getDefaultName();

        // Simulate fetching user details from a data store
        String userEmail = getUserEmailFromStore(username);

        if (userEmail == null || !isDomainAllowed(userEmail)) {
            return Action.goTo(OUTCOME_FALSE).build();
        }

        return Action.goTo(OUTCOME_TRUE).build();
    }

    private String getUserEmailFromStore(String username) {
        // Replace with actual data store logic
        if ("[email protected]".equals(username)) {
            return "[email protected]";
        } else if ("[email protected]".equals(username)) {
            return "[email protected]";
        }
        return null;
    }

    private boolean isDomainAllowed(String email) {
        String domain = email.substring(email.indexOf('@') + 1);
        return config.allowedDomains().contains(domain);
    }
}

Step 4: Compile and Package

Compile your project using Maven:

mvn clean package

This will generate a JAR file in the target directory.

Step 5: Deploy the Node

Copy the JAR file to the AM server’s WEB-INF/lib directory and restart the server.

cp target/custom-auth-node-1.0-SNAPSHOT.jar /path/to/openam/WEB-INF/lib/

Step 6: Configure the Node

Log in to the AM admin console, navigate to Realms > Top Level Realm > Authentication > Trees, and create a new authentication tree or modify an existing one. Add your custom node to the tree and configure the allowed domains.

Troubleshooting Common Issues

Issue 1: Node Not Showing Up in the Console

Ensure your JAR file is correctly placed in the WEB-INF/lib directory and the server is restarted. Check the server logs for any deployment errors.

Issue 2: Compilation Errors

Double-check your pom.xml for correct dependencies and versions. Ensure your IDE is using the correct JDK version.

Issue 3: Configuration Not Saved

Verify that your configuration class is correctly annotated and that the fields have valid default values.

Best Practices for Securing Custom Nodes

Validate Inputs

Always validate and sanitize inputs to prevent injection attacks.

String username = nameCallback.getDefaultName();
if (username == null || username.isEmpty()) {
    throw new NodeProcessException("Invalid username");
}

Avoid Hardcoding Secrets

Never hardcode sensitive information like passwords or API keys in your code. Use configuration options or external vaults.

Log Sensitively

Avoid logging sensitive information. Use logging levels appropriately and mask sensitive data.

logger.debug("User email fetched: {}", userEmail.replaceAll("@.*", "@***"));

Test Thoroughly

Test your custom node thoroughly in a staging environment before deploying it to production. Use different scenarios to ensure it handles all cases correctly.

Conclusion

Building custom authentication nodes in ForgeRock AM 7.5 can be a powerful way to tailor your IAM solutions. By following the steps outlined above and adhering to best practices, you can create secure, efficient, and effective custom nodes. Remember to test thoroughly and keep security top of mind throughout the development process.

Deploy your custom node, monitor its performance, and refine it based on feedback and usage patterns. Happy coding!