Production-ready HOTP-based Email OTP authentication node for ForgeRock Access Management (AM) 7.x.
| File | Purpose |
|---|---|
HOTPGenerator.java |
RFC 4226–compliant HMAC-SHA1 OTP generator with test vectors |
EmailOTPNode.java |
Full two-phase AM auth node (send → verify) with rate-limit protection |
OTPEmailService.java |
SMTP delivery service with TLS, auth, and structured logging |
EmailOTPNodePlugin.java |
AM plugin registration + Guice SMTP config bindings |
HOTPGeneratorTest.java |
JUnit 5 tests validating all RFC 4226 Appendix D test vectors |
pom.xml |
Maven build producing a deployable shaded JAR |
📖 Full tutorial: Building an Email OTP Node in ForgeRock AM
The node implements a two-phase authentication flow inside a ForgeRock AM tree:
[ Identify User ] → [ Profile Attribute Node ] → [ Email OTP Node ] → [ Success/Failure ]
│
┌────────────────────────────┘
▼
1. SEND: generateHOTP(secret, counter) → email OTP
2. VERIFY: compare submitted OTP with stored value
3. LOCK: after N failed attempts → go to Failure outcome
HOTP generates time-independent OTPs based on:
- A shared secret (stored as a Base64-encoded LDAP attribute
hotpSecret) - An incrementing counter (stored in AM shared state, incremented after each send)
- ForgeRock AM 7.x (tested on 7.3, 7.4)
- Java 11+ (JDK)
- Maven 3.8+
- ForgeRock Backstage account (for Maven dependencies)
- An SMTP server (Gmail, SendGrid, SES, or corporate relay)
git clone https://github.com/IAMDevBox/forgerock-am-email-otp-node.git
cd forgerock-am-email-otp-node
# Configure ForgeRock Maven credentials (one-time setup)
# Add to ~/.m2/settings.xml:
# <server><id>forgerock-releases</id><username>YOUR_EMAIL</username><password>YOUR_TOKEN</password></server>
mvn clean package -DskipTestsThe deployable JAR is at target/forgerock-am-email-otp-node-1.0.0-deploy.jar.
mvn test
# All RFC 4226 Appendix D test vectors must pass# Copy the shaded JAR to AM's custom node directory
cp target/forgerock-am-email-otp-node-1.0.0-deploy.jar \
/path/to/am/WEB-INF/lib/
# Set SMTP environment variables (or use AM Secret Store)
export SMTP_HOST=smtp.example.com
export SMTP_PORT=587
export SMTP_USERNAME=noreply@example.com
export SMTP_PASSWORD=changeme
export SMTP_TLS=true
export SMTP_FROM=noreply@example.com
# Restart AM (Tomcat or container)- Navigate to Realms → Authentication → Trees
- Create a new tree or edit an existing one
- Drag Email OTP Node from the node panel
- Connect it after a Profile Attribute Node (to populate
emailin shared state) - Configure node settings:
- Max Attempts: 3 (recommended)
- Look-Ahead Window: 1 (allow one counter step of tolerance)
- Email Subject/Body: Customize for your branding
Before the Email OTP Node runs, ensure these values are present in AM shared state:
| Key | Source | Description |
|---|---|---|
username |
Set by Identify User node | The user's AM username |
email |
Set by Profile Attribute Node (attribute: mail) |
Delivery address |
hotpSecret |
Set by Profile Attribute Node (attribute: hotpSecret) |
Base64-encoded HOTP secret |
Each user needs a unique Base64-encoded HOTP secret stored in their LDAP profile:
# Generate a 20-byte random secret (per RFC 4226 recommendation)
SECRET=$(openssl rand -base64 20)
# Store it in the user's LDAP entry (example: cn=jdoe,ou=people,dc=example,dc=com)
ldapmodify -h ldap.example.com -p 389 -D "cn=Directory Manager" -w changeme <<EOF
dn: uid=jdoe,ou=people,dc=example,dc=com
changetype: modify
replace: hotpSecret
hotpSecret: $SECRET
EOF| Config Field | Default | Description |
|---|---|---|
maxAttempts |
3 | Failed attempts before the node returns False outcome |
lookAheadWindow |
1 | Counter window tolerance for clock skew |
emailSubject |
"Your one-time password for {realm}" |
Supports {realm} placeholder |
emailBodyTemplate |
"Your one-time password is: {otp}…" |
Supports {otp} placeholder |
SMTP config via environment variables:
| Env Var | Default | Description |
|---|---|---|
SMTP_HOST |
smtp.example.com |
SMTP server hostname |
SMTP_PORT |
587 |
SMTP port (587 for STARTTLS, 465 for SSL) |
SMTP_USERNAME |
— | SMTP authentication username |
SMTP_PASSWORD |
— | SMTP authentication password |
SMTP_TLS |
true |
Enable STARTTLS |
SMTP_FROM |
noreply@example.com |
From address |
- Secret storage: Never log or expose
hotpSecret. Mask emails in logs (seemaskEmail()inEmailOTPNode.java). - Counter replay protection: Each OTP is valid for exactly one use. The counter increments on every send.
- Rate limiting: The
maxAttemptsconfig prevents brute-force OTP enumeration. - TLS: Always enable
SMTP_TLS=truein production. Never send OTPs over plaintext SMTP. - Secret rotation: Rotate
hotpSecretperiodically or on suspected compromise.
- 📖 Building an Email OTP Node in ForgeRock AM (Full Tutorial)
- 📖 Custom Authentication Nodes in ForgeRock AM 7.5
- 📖 Custom Callback Usage and Extension Techniques in ForgeRock AM
- 📖 ForgeRock AM Scripted Decision Nodes
- 🛠️ IAMDevBox Developer Tools — JWT Builder, SAML Decoder, PKCE Generator
MIT — see LICENSE.
Created and maintained by IAMDevBox.com — practical IAM tutorials for ForgeRock, Keycloak, and beyond.