Two-Factor Authentication
TOTP, OTP, and backup codes for two-factor authentication.
The TwoFactorPlugin adds two-factor authentication (2FA) to your application. It supports TOTP (authenticator apps), OTP (email-based one-time passwords), and backup codes for recovery.
Setup
These examples assume you've already defined AppAuthSchema. If you want a complete setup from entity definitions through Axum mounting, start with the Axum integration guide.
use better_auth::plugins::TwoFactorPlugin;
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(TwoFactorPlugin::new())
.build()
.await?;Configuration
use better_auth::plugins::two_factor::TwoFactorConfig;
use std::sync::Arc;
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(
TwoFactorPlugin::new().with_config(TwoFactorConfig {
issuer: Some("My App".to_string()),
skip_verification_on_enable: false,
two_factor_cookie_max_age: 600,
trust_device_max_age: 30 * 24 * 60 * 60,
totp_period: 30,
totp_digits: 6,
send_otp: None,
})
)
.build()
.await?;| Option | Type | Default | Description |
|---|---|---|---|
issuer | Option<String> | None | Issuer name shown in authenticator apps. Falls back to app_name. |
skip_verification_on_enable | bool | false | Marks the user as 2FA-enabled immediately during enrollment. |
two_factor_cookie_max_age | i64 | 600 | Lifetime of the pending 2FA cookie in seconds. |
trust_device_max_age | i64 | 2592000 | Lifetime of the trusted-device cookie in seconds. |
totp_period | u64 | 30 | TOTP time step in seconds |
totp_digits | usize | 6 | Number of digits in TOTP code |
send_otp | Option<Arc<dyn SendTwoFactorOtp>> | None | Callback used by /two-factor/send-otp. |
How It Works
Enrollment Flow
- User calls
/two-factor/enablewith their password - An encrypted TOTP secret is generated and stored
- Backup codes are generated and stored as encrypted JSON
- User scans the TOTP URI in their authenticator app
twoFactorEnabledremainsfalseuntil verification unlessskip_verification_on_enableis enabled
Sign-In Flow with 2FA
- User signs in with email/password as normal
- If 2FA is enabled, instead of creating a session, a pending verification is created
- User must verify with one of:
- TOTP: Code from authenticator app
- OTP: Code sent via email
- Backup code: One-time recovery code
- After successful verification, a session is created
API Endpoints
The 2FA plugin exposes the following endpoints. For full request/response details, see the OpenAPI Reference.
| Endpoint | Method | Description |
|---|---|---|
/two-factor/enable | POST | Enable 2FA (returns TOTP URI and backup codes) |
/two-factor/disable | POST | Disable 2FA (requires password) |
/two-factor/get-totp-uri | POST | Retrieve TOTP URI for enrolled user |
/two-factor/verify-totp | POST | Verify TOTP code during sign-in |
/two-factor/send-otp | POST | Send OTP via email |
/two-factor/verify-otp | POST | Verify email OTP |
/two-factor/generate-backup-codes | POST | Generate new backup codes |
/two-factor/verify-backup-code | POST | Verify a backup code during sign-in |
Backup codes are shown only once during enrollment or regeneration on the public HTTP surface. Store them securely.
For TS compatibility, Rust keeps backup-code retrieval as a server-only capability. Use TwoFactorPlugin::view_backup_codes(...) from trusted server code when you need to read the currently stored codes. There is no public HTTP route for this.
Security Details
- TOTP secrets are encrypted before being stored in the database
- Backup codes are stored as encrypted JSON — the plaintext is only returned once during generation
- OTP codes are stored as verification records and expire after 3 minutes
- Password verification is required to enable/disable 2FA and to regenerate backup codes
- Used backup codes are immediately removed from the stored set
Errors
| Status | Condition |
|---|---|
| 400 | Invalid password |
| 400 | TOTP not enabled |
| 400 | Two factor isn't enabled |
| 400 | OTP has expired |
| 400 | Too many attempts. Please request a new code. |
| 401 | Invalid code |
| 401 | Invalid backup code |
| 401 | Invalid two factor cookie |
| 401 | Unauthenticated (missing or invalid session) |