Email Verification Codes
A 6-digit, single-use, hashed, rate-limited email code that clears a sensitive-action step-up for any account type — the universal verification fallback.
Email verification codes are the universal fallback among the three sensitive-action verification methods. Because they only require a verified email, they work for every account type, including OAuth-only and magic-link users who have no password to confirm.
How it works
- The user requests a code for a specific action. The backend
(
security/challenges.ts) creates a challenge and emails a 6-digit code. - The code is single-use and short-lived. Only a salted, peppered SHA-256
hash is stored — never the plaintext code (
security/crypto.ts, peppered withBETTER_AUTH_SECRET). - The user enters the code; the backend hashes the input and compares. On a match it consumes the challenge and mints the action-scoped grant.
The email template lives at
email/templates/sensitiveActionVerificationEmail.tsx.
Limits and cooldowns
- Creation is limited by
sensitiveEmailChallengeCreateinrateLimit.ts(3 codes per 10 minutes, keyed per user and action). - A 60-second resend cooldown is enforced inline when creating a challenge.
- Codes are single-use; consuming or expiring one invalidates it.
See Rate Limiting for how these limits are configured.
Why hashing
Storing only a hash means a database read never reveals a usable code, and the audit log never contains one. The same approach protects session scopes used by fresh-session checks.
Password Confirmation
Password confirmation re-verifies a user's current password through Better Auth to clear a sensitive-action step-up — no new login and no password change.
Rate Limiting
Server-side rate limiting on abuse-prone surfaces using the Convex rate-limiter component, configured in one place in rateLimit.ts.