Skip to main content
The two-factor authentication (2FA) plugin adds a second verification step after a user signs in with their password. It supports:
  • TOTP — time-based codes from an authenticator app (Google Authenticator, Authy, etc.)
  • OTP — one-time codes sent to the user’s email or phone
  • Backup codes — single-use recovery codes
  • Trusted devices — skip 2FA on recognized devices for 30 days

Installation

1

Add the server plugin

Import twoFactor and add it to your plugins list. Set appName to customize the issuer shown in authenticator apps:
auth.ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  appName: "My App", // shown as the issuer in authenticator apps
  plugins: [
    twoFactor(),
  ],
});
2

Run the database migration

The plugin adds a twoFactorEnabled column to the user table and creates a twoFactor table:
npx auth migrate
3

Add the client plugin

Import twoFactorClient and configure where to redirect users who need to complete 2FA:
auth-client.ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    twoFactorClient({
      // Option 1: redirect to a page
      twoFactorPage: "/two-factor",
      // Option 2: handle programmatically (avoids full page reload)
      // onTwoFactorRedirect() {
      //   router.push("/two-factor");
      // },
    }),
  ],
});
Using twoFactorPage causes a full page reload when redirecting. Use onTwoFactorRedirect to handle the redirect programmatically and avoid a reload.

Usage

Enable 2FA for a user

Call twoFactor.enable with the user’s current password. The server generates a TOTP secret and backup codes:
enable-2fa.ts
import { authClient } from "@/lib/auth-client";

const { data, error } = await authClient.twoFactor.enable({
  password: "user-current-password",
});

if (data) {
  // data.totpURI — use this to generate a QR code for the user to scan
  // data.backupCodes — show these to the user once; they cannot be retrieved again
  console.log(data.totpURI);
  console.log(data.backupCodes);
}
twoFactorEnabled is not set to true until the user verifies a TOTP code. This ensures the user has successfully scanned the QR code before 2FA is enforced. You can skip this requirement by setting skipVerificationOnEnable: true in the plugin config.

Display a QR code

Use the totpURI to render a scannable QR code:
two-factor-setup.tsx
import { authClient } from "@/lib/auth-client";
import QRCode from "react-qr-code";

export function TwoFactorSetup({ password }: { password: string }) {
  const { data: session } = authClient.useSession();

  const { data: qrData } = useQuery({
    queryKey: ["totp-uri"],
    queryFn: async () => {
      const res = await authClient.twoFactor.getTotpUri({ password });
      return res.data;
    },
    enabled: !!session?.user.twoFactorEnabled,
  });

  return <QRCode value={qrData?.totpURI ?? ""} />;
}

Verify a TOTP code

After the user scans the QR code, verify the first code to confirm setup:
verify-totp.ts
import { authClient } from "@/lib/auth-client";

const { data, error } = await authClient.twoFactor.verifyTotp({
  code: "123456",      // 6-digit code from the authenticator app
  trustDevice: true,   // remember this device for 30 days
});
Better Auth accepts TOTP codes from the current 30-second window, plus one window before and after, to account for clock drift.

Sign in with 2FA enabled

When a user with 2FA enabled signs in, the response includes twoFactorRedirect: true. Handle it in the onSuccess callback:
sign-in.ts
import { authClient } from "@/lib/auth-client";

await authClient.signIn.email(
  {
    email: "user@example.com",
    password: "password123",
  },
  {
    async onSuccess(context) {
      if (context.data.twoFactorRedirect) {
        // Redirect or show the 2FA verification UI
        router.push("/two-factor");
      }
    },
  }
);

Disable 2FA

disable-2fa.ts
await authClient.twoFactor.disable({
  password: "user-current-password",
});

OTP (email or SMS)

Instead of TOTP, you can send a one-time code via email or phone. Configure otpOptions.sendOTP on the server:
auth.ts
twoFactor({
  otpOptions: {
    async sendOTP({ user, otp }, ctx) {
      await sendEmail({
        to: user.email,
        subject: "Your verification code",
        text: `Your code is: ${otp}`,
      });
    },
  },
})

Send an OTP

send-otp.ts
await authClient.twoFactor.sendOtp();

Verify an OTP

verify-otp.ts
const { data, error } = await authClient.twoFactor.verifyOtp({
  code: "123456",
  trustDevice: true,
});

Backup codes

Backup codes are generated when 2FA is enabled. Each code can only be used once.

Generate new backup codes

generate-backup-codes.ts
const { data } = await authClient.twoFactor.generateBackupCodes({
  password: "user-current-password",
});
// data.backupCodes — array of new codes; old codes are deleted
Generating new backup codes permanently deletes the old ones.

Verify a backup code

verify-backup-code.ts
const { data, error } = await authClient.twoFactor.verifyBackupCode({
  code: "backup-code-here",
  trustDevice: false,
});

Trusted devices

Pass trustDevice: true when verifying any 2FA method to skip 2FA for 30 days on that device:
verify-with-trust.ts
await authClient.twoFactor.verifyTotp({
  code: "123456",
  trustDevice: true, // user won't be prompted for 2FA again on this device for 30 days
});

Configuration options

Server options

OptionTypeDefaultDescription
issuerstringapp nameIssuer name shown in authenticator apps. Overrides appName.
skipVerificationOnEnablebooleanfalseSet true to mark 2FA enabled without requiring a TOTP verification.
twoFactorTablestring"twoFactor"Custom table name for storing 2FA data.
otpOptions.sendOTPfunctionSend an OTP code to the user.
otpOptions.periodnumber3OTP validity period in minutes.
totpOptions.digitsnumber6Number of digits in the TOTP code.
totpOptions.periodnumber30TOTP window in seconds.
backupCodeOptions.amountnumber10Number of backup codes to generate.
backupCodeOptions.lengthnumber10Length of each backup code.

Client options

OptionTypeDescription
twoFactorPagestringURL to redirect users who need to complete 2FA. Causes a full page reload.
onTwoFactorRedirectfunctionCallback for handling the 2FA redirect programmatically.