This version is in beta. Some features may change before release.

Email verification

The verify-email and resend-verification endpoints, the 6-digit code rules, and the require_verified_email opt-in that blocks login until the address is confirmed.

umbral-auth ships opt-in email verification. When enabled, a 6-digit code is sent to the user on registration and login is blocked with a 403 until the address is confirmed. When disabled (the default), the email_verified_at column is present on auth_user but no enforcement runs - useful for phased rollout.

Enabling enforcement

Code
rust
use umbral_auth::{AuthPlugin, AuthUser};
 
AuthPlugin::new()
.with_default_routes()
.require_verified_email() // auto-sends code on register; 403 on login until confirmed
.mailer(/* see Auth mailer */);

Without .require_verified_email(), the two endpoints below are still mounted and functional; the login gate is simply not installed.

Info

Your mailer receives this email as

MailKind::EmailVerification { code }
  • match on it to build a fully branded verification email with the raw code, or override
templates/auth/email/verify_code.{html,txt}

to change just the wording. See

Auth mailer

.

Endpoints

Both endpoints are mounted by with_default_routes() under the configured prefix (default /api/auth).

POST /api/auth/verify-email

Code
json
{ "email": "alice@example.com", "code": "482017" }

Verifies the 6-digit code. On success, writes email_verified_at = now() on the user row and returns 204 No Content. On failure:

SituationResponse
Code correct, not expired, not used204 No Content
Code wrong400 Bad Request
Attempt cap reached (5 attempts)400 Bad Request
Code expired (15-minute TTL)400 Bad Request
Code already used400 Bad Request

The response body does not distinguish between wrong code, expired code, or used code - same JSON shape to prevent oracle attacks.

POST /api/auth/resend-verification

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

Issues a fresh code and emails it. Always returns 202 Accepted regardless of whether the email belongs to a registered account (no enumeration). The previous active code for that user is invalidated.

Code rules

6 digits

Numeric only. Generated with a CSPRNG - not Math.random.

15-minute TTL

Codes expire 15 minutes after issue. Resend to get a fresh one.

5-attempt cap

Five wrong guesses burn the code. The user must resend.

Single-use

A code is marked used immediately on first successful verify.

The email_verified_at column

The column is added to auth_user by the auth plugin's own migration. Run makemigrations and migrate after adding .require_verified_email() to an existing app - the migration adds the nullable column and the framework handles existing unverified rows gracefully (they keep email_verified_at = NULL and are blocked from login until they verify).

See the design note docs/decisions/2026-06-28-auth-full-surface.md for the challenge model, TTL choices, and how verification interacts with the password-reset flow.

authemailverification