Multi-Factor Authentication
TOTP-based two-factor authentication with authenticator apps. Users can enable MFA from their security settings.
MFA Setup and Usage
How users enable and use MFA.
Multi-Factor Authentication (MFA) adds an extra layer of security by requiring users to enter a time-based one-time password (TOTP) from an authenticator app after signing in with their email and password.
How MFA Works
MFA in the kit uses TOTP (Time-based One-Time Password), compatible with authenticator apps like:
- Google Authenticator
- Authy
- 1Password
- Microsoft Authenticator
- Any TOTP-compatible app
When MFA is enabled:
- User signs in with email/password
- Better Auth detects MFA is enabled for this user
- User is redirected to
/auth/verify - User enters the 6-digit code from their authenticator app
- If valid, session is created and user proceeds to dashboard
Enabling MFA
Users enable MFA from their security settings page.
Route: /settings/security
Click to expandSetup Process
- User clicks "Enable MFA" button
- A QR code is displayed
- User scans the QR code with their authenticator app
- User enters the initial verification code to confirm setup
- MFA is now active on their account
The QR code contains the TOTP secret and account information in a standard otpauth:// URI format that authenticator apps recognize.
Backup Codes
When MFA is enabled, users should be prompted to save backup codes. These one-time codes can be used if the user loses access to their authenticator app.
Verification Flow
Click to expandRoute: /auth/verify
The verification page shows a simple form for entering the 6-digit code. The code changes every 30 seconds in the authenticator app.
Client-Side Redirect
The auth client handles the redirect to verification automatically:
packages/better-auth/src/auth-client.ts
twoFactorClient({ onTwoFactorRedirect() { const redirect = new URLSearchParams(location.search).get('redirect'); window.location.href = '/auth/verify' + (redirect ? `?redirect=${redirect}` : ''); },}),Verification Component
The MFA challenge is handled by MultiFactorChallengeContainer:
packages/auth/src/components/multi-factor-challenge-container.tsx
<MultiFactorChallengeContainer paths={{ signIn: '/auth/sign-in', }}/>Configuration
MFA is enabled through the Better Auth two-factor plugin:
packages/better-auth/src/plugins/two-factor.ts
import { twoFactor } from 'better-auth/plugins/two-factor';export function createTwoFactorPlugin() { return twoFactor({ issuer: env('NEXT_PUBLIC_PRODUCT_NAME'), // Shows in authenticator app });}The issuer appears in the authenticator app alongside the account, helping users identify which service the code is for.
Programmatic MFA
Enable MFA
'use client';import { authClient } from '@kit/better-auth/client';async function enableMFA() { const result = await authClient.twoFactor.enable(); if (result.error) { console.error(result.error.message); return; } // result.data contains the TOTP secret and QR code URL return result.data;}Verify MFA Code
'use client';import { authClient } from '@kit/better-auth/client';async function verifyMFACode(code: string) { const result = await authClient.twoFactor.verifyTotp({ code, }); if (result.error) { console.error(result.error.message); return; } // MFA verified, session created window.location.href = '/dashboard';}Disable MFA
'use client';import { authClient } from '@kit/better-auth/client';async function disableMFA(code: string) { const result = await authClient.twoFactor.disable({ code, // Requires current TOTP code to disable }); if (result.error) { console.error(result.error.message); return; } // MFA disabled}MFA and Other Auth Methods
Magic Links
Magic link authentication bypasses MFA. If you require MFA for all users, consider disabling magic links:
apps/web/.env.local
NEXT_PUBLIC_AUTH_MAGIC_LINK=falseThis is by design since the email itself serves as a second factor (possession of the email account).
Social Providers
Review MFA behavior carefully for every enabled sign-in method. If you need stricter MFA enforcement, test the flows you expose and limit sign-in methods accordingly.
Password-Only MFA
Only email/password authentication enforces MFA. This provides a balance between security and convenience:
| Auth Method | MFA Required? |
|---|---|
| Email/Password | Yes (if enabled) |
| Magic Link | No |
| Google OAuth | No |
| GitHub OAuth | No |
Security Considerations
Rate Limiting
MFA verification attempts are rate-limited to prevent brute-force attacks. After several failed attempts, the user may need to wait before trying again.
Session Security
When MFA verification succeeds, a new session is created. The session inherits the same security properties (HTTP-only cookies, secure flag in production).
Recovery
If users lose access to their authenticator app:
- They can use backup codes (if saved)
- An admin can disable MFA for their account
- They can contact support for manual verification
Common Issues
"Invalid code" Error
- Code has expired (they change every 30 seconds)
- Device time is out of sync with server time
- Wrong account selected in authenticator app
QR Code Won't Scan
- Ensure good lighting and camera focus
- Try entering the secret manually in the authenticator app
- Check that the QR code is fully visible
MFA Locked Out
If a user is locked out:
- Check for backup codes
- Admin can query the database and update the user's MFA status
- Consider implementing an account recovery flow
Frequently Asked Questions
Is MFA required for all users?
What authenticator apps are supported?
Does MFA work with magic links?
Can I require MFA for certain roles?
How do I reset MFA for a user?
Previous: Session Handling ← | Next: Personal Accounts →