Email OTP / Magic Link¶
Overview¶
Email OTP lets users sign in without a password. When enabled, the login page shows a "Sign in with a one-time code" link. The user enters their email and receives a message containing:
- A 6-digit one-time code (valid for 10 minutes)
- A magic-link button that signs them in with a single click (same token, no code entry needed)
Both paths are handled by the same verify endpoint — a magic link simply omits the code.
This feature is disabled by default and requires SMTP to be configured. Enable it in Settings → Password & Auth.
How it works¶
OTP code path¶
- User clicks "Sign in with a one-time code" on the login page.
- User enters their email address and clicks Send code.
- JustIAM looks up a matching active internal account. If found, it creates an
email_otp_challengesrecord and sends the email. If the email is unknown, the response is the same (prevents enumeration). - The frontend shows a 6-digit code input field.
- User enters the code and clicks Sign in.
- JustIAM validates the code (bcrypt compare), marks the challenge as used, and issues a portal JWT + SSO session — identical to a successful password login.
Magic-link path¶
Same as above, but the user clicks the Sign in with one click button in the email instead of typing the code. The frontend detects the ?magic_token= query parameter on page load and automatically calls the verify endpoint.
Security properties¶
| Property | Detail |
|---|---|
| Token storage | Raw token never stored — SHA-256 hash in token_hash column |
| Code storage | 6-digit code never stored — bcrypt hash in code_hash column |
| TTL | 10 minutes from creation |
| Single-use | used_at is stamped on first successful consume; subsequent attempts with same token are rejected |
| Enumeration resistance | POST /auth/email-otp/request always returns 200 with a token regardless of whether the email exists |
| Rate limiting | Both endpoints are protected by the tenant-level auth rate limiter (configured via controlplane Security limits) |
| Local accounts only | Federated (SSO, SAML, LDAP) accounts are skipped silently |
Configuration¶
| Setting | Default | Description |
|---|---|---|
allow_email_otp |
false |
Enable / disable the feature for this tenant |
allow_password_login |
true |
When set to false, password login is hidden and OTP/magic-link becomes the only local auth method |
Enable in Settings → Password & Auth → Email OTP / Magic Link or via the API / Terraform provider.
Note
SMTP must be configured for the email to be delivered. Configure SMTP in Settings → Email.
API reference¶
Request a code / magic link¶
Response 200:
Always returns 200. The otp_token is needed by the verify step.
Verify a code or magic link¶
POST /api/v1/auth/email-otp/verify
Content-Type: application/json
{ "otp_token": "<token>", "code": "123456" }
Omit code (or send an empty string) for the magic-link path.
Response 200 — same structure as POST /auth/login:
Response 401 — invalid or expired token/code.
Response 403 — feature not enabled for this tenant.
Terraform¶
resource "justiam_settings" "main" {
allow_email_otp = true
allow_password_login = false # OTP-only login
}
Cleanup¶
Expired unused challenges are cleaned up by the background worker that runs periodic maintenance tasks. The DeleteExpired() method purges rows where expires_at < NOW() and used_at IS NULL.