Better Auth in Rust

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,
            account_lockout: Default::default(),
        })
    )
    .build()
    .await?;
OptionTypeDefaultDescription
issuerOption<String>NoneIssuer name shown in authenticator apps. Falls back to app_name.
skip_verification_on_enableboolfalseMarks the user as 2FA-enabled immediately during enrollment.
two_factor_cookie_max_agei64600Lifetime of the pending 2FA cookie in seconds.
trust_device_max_agei642592000Lifetime of the trusted-device cookie in seconds.
totp_periodu6430TOTP time step in seconds
totp_digitsusize6Number of digits in TOTP code
send_otpOption<Arc<dyn SendTwoFactorOtp>>NoneCallback used by /two-factor/send-otp.
account_lockoutAccountLockoutEnabled, 10 failures, 900 secondsShared failure budget across sign-in factors and challenges.

How It Works

Enrollment Flow

  1. User calls /two-factor/enable with their password
  2. An encrypted TOTP secret is generated and stored
  3. Backup codes are generated and stored as encrypted JSON
  4. User scans the TOTP URI in their authenticator app
  5. twoFactorEnabled remains false until verification unless skip_verification_on_enable is enabled

The enrollment response contains method: "totp", totpURI, and backupCodes.

Pass method: "otp" with the password to enable email/SMS codes when send_otp is configured. This method enables 2FA immediately, rotates the session, and returns only { "method": "otp" }. It does not create an authenticator secret or backup codes.

A pending TOTP enrollment can be restarted. Once verified, another enable request returns TOTP_ALREADY_ENABLED and preserves the existing secret and backup codes. Disable 2FA before replacing a verified authenticator.

Sign-In Flow with 2FA

  1. User signs in with email/password as normal
  2. If 2FA is enabled, instead of creating a session, a pending verification is created and the response is { "twoFactorRedirect": true, "twoFactorMethods": [...] }, where twoFactorMethods lists the factors this user can complete: "totp" once a secret is enrolled, "otp" when send_otp is configured
  3. User must verify with one of:
    • TOTP: Code from authenticator app
    • OTP: Code sent via email
    • Backup code: One-time recovery code
  4. After successful verification, a session is created

When sign-in sets rememberMe: false, the completed two-factor session expires after 24 hours and uses a session cookie. Remembered sessions use the configured session lifetime.

API Endpoints

The 2FA plugin exposes the following endpoints. For full request/response details, see the OpenAPI Reference.

EndpointMethodDescription
/two-factor/enablePOSTEnable 2FA (returns TOTP URI and backup codes)
/two-factor/disablePOSTDisable 2FA (requires password)
/two-factor/get-totp-uriPOSTRetrieve TOTP URI for enrolled user
/two-factor/verify-totpPOSTVerify TOTP code during sign-in
/two-factor/send-otpPOSTSend OTP via email
/two-factor/verify-otpPOSTVerify email OTP
/two-factor/generate-backup-codesPOSTGenerate new backup codes
/two-factor/verify-backup-codePOSTVerify 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
  • OTP comparison uses the original code bytes. Password NFKC normalization does not apply to OTP codes.
  • Password verification is required to enable/disable 2FA and to regenerate backup codes
  • Used backup codes are removed with a conditional database update, so concurrent requests cannot reuse a code
  • OTP codes and sign-in challenges are consumed atomically; an expired latest code also invalidates older codes for the same identifier
  • TOTP and backup-code challenges allow five failed attempts before the challenge is invalidated
  • Account lockout blocks sign-in factor verification for 15 minutes after 10 consecutive failures across challenges; successful verification clears the counter. As in Better Auth 1.7.6, the account must have an authenticator record. OTP-only enrollment uses the per-code attempt limit.

Errors

StatusCondition
400Invalid password
400TOTP not enabled
400TOTP is already enabled
400Two factor isn't enabled
400OTP has expired
400Too many attempts. Please request a new code.
401Invalid code
401Invalid backup code
401Invalid two factor cookie
401Unauthenticated (missing or invalid session)
409A concurrent request already replaced the backup-code set
429Account temporarily locked after repeated verification failures

On this page