Multi-Factor Authentication Configuration

Configure TOTP-based multi-factor authentication for enhanced account security.

Add an extra layer of security with multi-factor authentication. Users enable MFA in their security settings and verify with a 6-digit code from their authenticator app on each sign-in.

This page is part of the Authentication documentation.

Overview

MFA (Multi-Factor Authentication) requires users to provide a second factor - a time-based one-time password (TOTP) - in addition to their password. This protects accounts even if passwords are compromised.

Supported authenticator apps:

  • Google Authenticator
  • Authy
  • 1Password
  • Microsoft Authenticator
  • Any TOTP-compatible app

How TOTP Works

TOTP generates 6-digit codes that change every 30 seconds. The code is derived from:

  1. A shared secret (stored in the authenticator app)
  2. The current time

Both your server and the authenticator app generate the same code at the same time, allowing verification without network communication.

Configuration

The MFA plugin is configured in packages/better-auth/src/plugins/two-factor.ts:

import { twoFactor } from 'better-auth/plugins/two-factor';
import { env } from '@kit/shared/env';
export function createTwoFactorPlugin() {
return twoFactor({
issuer: env('NEXT_PUBLIC_PRODUCT_NAME'),
});
}

The issuer appears in authenticator apps to identify which app the code is for (e.g., "MakerKit" or your app name).

MFA Setup Flow

When a user enables MFA:

  1. User navigates to Security Settings (/home/settings/security)
  2. User clicks "Enable MFA"
  3. Server generates a secret and QR code
  4. User scans QR code with authenticator app
  5. User enters the current 6-digit code to verify
  6. Server generates recovery codes
  7. User saves recovery codes securely
  8. MFA is enabled for the account

QR Code Display

The QR code contains a TOTP URL in this format:

otpauth://totp/AppName:user@example.com?secret=BASE32SECRET&issuer=AppName

MFA Login Flow

When a user with MFA enabled signs in:

  1. User enters email and password
  2. Server verifies credentials
  3. Server detects MFA is enabled
  4. User is redirected to /auth/verify
  5. User enters 6-digit code from authenticator app
  6. Server verifies the code
  7. Session is created
  8. User is signed in

Client-Side Redirect

The client plugin handles MFA redirects automatically:

twoFactorClient({
onTwoFactorRedirect() {
const redirect = new URLSearchParams(location.search).get('redirect');
window.location.href =
'/auth/verify' + (redirect ? `?redirect=${redirect}` : '');
},
}),

Recovery Codes

Recovery codes allow users to sign in if they lose access to their authenticator app. Each code can only be used once.

How recovery codes work:

  1. 8-10 codes are generated when MFA is enabled
  2. Codes are shown once and must be saved by the user
  3. Each code can be used instead of a TOTP code
  4. Used codes are invalidated immediately
  5. Users can regenerate codes (invalidates all previous codes)

Best practices for users:

  • Store codes in a password manager
  • Print and store in a secure location
  • Never share codes with anyone
  • Regenerate if codes may be compromised

Client Usage

Enable MFA

import { authClient } from '@kit/better-auth/client';
// Generate secret and QR code
const { data } = await authClient.twoFactor.enable();
// data.totpURI - QR code URI for authenticator apps
// data.backupCodes - Recovery codes to save
// After user scans QR code, verify with their first code
await authClient.twoFactor.verifyTOTP({
code: '123456',
});

Verify During Sign-In

import { authClient } from '@kit/better-auth/client';
// Called on /auth/verify page after sign-in
await authClient.twoFactor.verifyTOTP({
code: '123456',
});
// Or use a recovery code
await authClient.twoFactor.verifyBackupCode({
code: 'ABCD-1234-EFGH',
});

Disable MFA

import { authClient } from '@kit/better-auth/client';
await authClient.twoFactor.disable({
password: 'userPassword', // Requires password confirmation
});

Security Settings UI

The security settings page at /home/settings/security provides:

  • Enable/Disable MFA - Toggle two-factor authentication
  • View Recovery Codes - Show existing recovery codes (requires password)
  • Regenerate Recovery Codes - Generate new codes (invalidates old ones)

Common Pitfalls

  • Using production authenticator entries for development: Create separate entries for dev and prod to avoid confusion.
  • Not saving recovery codes: Users must save recovery codes. Without them, lost authenticator access means account lockout.
  • Time sync issues: TOTP requires accurate time. If codes don't work, check server time sync.
  • Forgetting to add client plugin: The twoFactorClient plugin must be in auth-client.ts for redirects to work.
  • MFA only works for email/password sign-ins: MFA is not supported for OAuth sign-ins - as it is assumed the user activates MFA in the oAuth provider.

Frequently Asked Questions

Can I require MFA for all users?
MFA is user-optional by default. To require it, you'd need to add middleware that checks MFA status and redirects users to enable it.
What if a user loses their phone?
Users can use their saved recovery codes. If they didn't save codes, you may need to manually disable MFA via the admin panel or database.
Can I use SMS-based MFA instead of TOTP?
Better Auth supports TOTP by default.
How many recovery codes are generated?
The default is 10 recovery codes. Each can be used once to sign in without the authenticator app.

Next: Adding Plugins →