Skip to content

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

  1. User clicks "Sign in with a one-time code" on the login page.
  2. User enters their email address and clicks Send code.
  3. JustIAM looks up a matching active internal account. If found, it creates an email_otp_challenges record and sends the email. If the email is unknown, the response is the same (prevents enumeration).
  4. The frontend shows a 6-digit code input field.
  5. User enters the code and clicks Sign in.
  6. JustIAM validates the code (bcrypt compare), marks the challenge as used, and issues a portal JWT + SSO session — identical to a successful password login.

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

POST /api/v1/auth/email-otp/request
Content-Type: application/json

{ "email": "alice@example.com" }

Response 200:

{ "otp_token": "<opaque-token>" }

Always returns 200. The otp_token is needed by the verify step.


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:

{
  "token": "<jwt>",
  "user": { ... },
  "permissions": [...],
  "proxy_mode": false
}

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.