ForgeRock Access Management (AM) offers a powerful and flexible authentication tree system, enabling enterprises to design secure and dynamic login experiences. One of its useful features, the EmailSuspendNode, traditionally relies on ForgeRock Identity Management (IDM) for full functionality. But what if you’re not using IDM? This post walks through how to build a custom ForgeRock AM node that replicates the core functionality of EmailSuspendNodeβ€”complete with email delivery, resume flow support, and secure suspend/resume logicβ€”all without needing IDM integration.

πŸ”§ Why Customize EmailSuspendNode in ForgeRock AM?

In environments where ForgeRock IDM is not deployed, the native EmailSuspendNode in ForgeRock AM becomes difficult to use out of the box. However, email verification flows are a critical component of modern identity systemsβ€”for passwordless authentication, user registration verification, and multi-factor flows.

A custom node helps bridge this gap, allowing full control over email dispatch, suspend-resume logic, and templated messages within ForgeRock AM alone. This approach is ideal for lightweight deployments or decoupled architectures where IDM is not in scope.


πŸ“¬ How the Custom Node Works

At a high level, this custom node checks if the current context has resumed from suspension. If not, it triggers the suspend flow, sends a verification email to the user, and halts execution until the user clicks the email link.

Here’s the main logic in the process() method:

public Action process(TreeContext context) throws NodeProcessException {
    if (context.hasResumedFromSuspend()) {
        return this.goToNext().build();
    } else {
        return Action.suspend((resumeURI) -> {
            try {
                return this.createSuspendOutcome(context, resumeURI);
            } catch (Exception ex) {
                logger.error(ex.getMessage());
                return SuspendedTextOutputCallback.error(ex.getMessage());
            }
        }).build();
    }
}

This logic ensures a seamless suspend/resume authentication experience. Once the email link is clicked, the flow picks up where it left off.


🧠 Creating the Resume Email with Custom URL

The most critical part of the node is generating a valid resume link and sending it via email. Here’s the method handling that logic:

private SuspendedTextOutputCallback createSuspendOutcome(TreeContext context, URI resumeURI) throws MessagingException {
    String mail = context.sharedState.get("mail").asString();
    String returnUrl = this.config.returnUrl();
    if (mail != null && returnUrl != null) {
        String suspendedId = Arrays.stream(resumeURI.getQuery().split("&"))
                .filter(p -> p.startsWith("suspendedId="))
                .map(p -> p.split("=")[1])
                .findFirst()
                .orElse(null);
        returnUrl = returnUrl + suspendedId;
        String messageBody = this.config.messageTemplate().replace("{{returnUrl}}", returnUrl);
        this.sendEmail(this.config.hostName(), String.valueOf(this.config.hostPort()), this.config.username(),
                this.config.password() == null ? null : String.valueOf(this.config.password()),
                this.config.emailSubject(), this.config.sslOption(), mail, messageBody);
    }
    return SuspendedTextOutputCallback.info(this.config.emailSuspendMessage());
}

This function parses the suspended ID from the resume URI, builds a return URL, and sends a templated email. A configuration interface allows full customization.


βœ‰οΈ Sending the Email Securely

Email delivery is handled via JavaMail API with configurable support for SSL, STARTTLS, or plain SMTP. Here’s a simplified version:

public void sendEmail(String smtpHost, String smtpPort, String username, String password,
                      String subject, Config.SslOption sslOption,
                      String recipient, String messageBody) throws MessagingException {
    Properties props = new Properties();
    props.put("mail.smtp.host", smtpHost);
    props.put("mail.smtp.port", smtpPort);
    props.put("mail.smtp.auth", String.valueOf(password != null));
    if (sslOption == Config.SslOption.SSL) {
        props.put("mail.smtp.ssl.enable", "true");
    } else if (sslOption == Config.SslOption.START_TLS) {
        props.put("mail.smtp.starttls.enable", "true");
    }
    Session session = Session.getInstance(props);
    MimeMessage message = new MimeMessage(session);
    message.setFrom(new InternetAddress(username));
    message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient));
    message.setSubject(subject);
    message.setContent(messageBody, "text/html; charset=UTF-8");

    executorService.execute(() -> {
        try {
            Transport.send(message, username, password);
        } catch (MessagingException ex) {
            logger.error("Failed to send email", ex);
        }
    });
}

This non-blocking approach ensures authentication flows remain performant and scalable.


🧩 Configuration Flexibility via Interface

The Config interface allows admins to customize everything from SMTP settings to email subject and templates:

@Attribute(order = 100)
default String hostName() { return "smtp.example.com"; }

@Attribute(order = 800)
default String returnUrl() { return "https://www.example.com/suspendId="; }

@Attribute(order = 900)
default String emailSuspendMessage() {
    return "An email has been sent to the address you entered. Click the link in that email to proceed.";
}

This makes it easy to adapt the node for different environments or branding requirements.


βœ… Real-World Use Case: Email Verification Without IDM

Imagine a SaaS platform using ForgeRock AM for authentication but handling user profiles in a separate microservice. Instead of spinning up IDM just to manage EmailSuspendNode, this custom node can independently trigger an email verification link, thereby completing login or registration securely.

This aligns perfectly with microservices-based identity architectures and zero-trust principles.


🧭 Diagram: Suspend-Resume Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  AM Tree     β”‚
β”‚ Starts Flow  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Custom Suspendβ”‚
β”‚   Node       │◄────────┐
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
       β”‚ Email Link      β”‚
       β–Ό                 β”‚
  User checks email      β”‚
       β”‚                 β”‚
       β–Ό                 β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚Resume with IDβ”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ Continues    β”‚
β”‚ Flow         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’‘ Questions to Consider

  • How could you adapt this approach to support SMS-based suspend/resume?
  • What are the trade-offs of managing email verification purely in AM vs. integrating with IDM or an external service?
  • Can this model support progressive profiling and attribute collection over time?

By implementing a self-contained email verification node in ForgeRock AM, organizations gain agility, reduce dependencies, and tailor their authentication experience. Whether you’re modernizing legacy apps or building zero-IDM identity platforms, this custom node can be a critical enabler.