TOTP-based 2FA with authenticator app support, backup codes, and trusted devices.
Two-factor authentication adds a second verification step after password sign-in. launch.now implements TOTP (Time-based One-Time Passwords) — the standard used by Google Authenticator, Authy, and 1Password.
The flow splits into two paths depending on whether the user has 2FA enabled:
- Without 2FA — sign in with email + password → session created immediately
- With 2FA — sign in with email + password → redirected to OTP verification → session created after code entry
Configuration
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"
export const auth = betterAuth({
// ...
plugins: [
twoFactor({
issuer: "launch.now", // shown in the authenticator app
otpOptions: {
period: 30, // code valid for 30 seconds
digits: 6,
},
backupCodes: {
enabled: true,
count: 10, // number of backup codes generated
length: 10, // length of each code
},
}),
],
})Database migration
The 2FA plugin adds columns to the user table. Run the migration after updating auth.ts:
npx better-auth migrate
This adds: twoFactorEnabled, twoFactorSecret, twoFactorBackupCodes.
File structure
src/components/features/auth/two-factor/
enable-2fa-dialog.tsx ← QR code + TOTP setup flow
verify-otp-form.tsx ← OTP input shown during sign-in
backup-codes-dialog.tsx ← display + download backup codes
disable-2fa-dialog.tsx ← confirm + disable 2FA
trusted-device-badge.tsx ← "trust this device" checkbox
Enabling 2FA (user settings)
The enable flow has three steps: password confirmation → QR code scan → TOTP verification.
Initiate setup
Call enable2fa with the user's current password. Better Auth generates a TOTP secret and returns a URI you can render as a QR code.
const { data, error } = await authClient.twoFactor.enable({
password, // current password — required to prevent unauthorized setup
})
// data.totpURI → pass to a QR code library (e.g. qrcode.react)
// data.backupCodes → show once, let user download/copyDisplay the QR code
Render the totpURI as a QR code. The user scans it with their authenticator app.
import QRCode from "react-qr-code"
<QRCode value={data.totpURI} size={200} />Also show the manual entry key for users who can't scan:
// Extract the secret from the URI
const secret = new URL(data.totpURI).searchParams.get("secret")Verify and activate
Ask the user to enter the 6-digit code their app is now showing. This confirms the setup worked before locking it in.
const { error } = await authClient.twoFactor.verifyTotp({
code, // 6-digit code from authenticator app
})
// On success — 2FA is now active for this accountSigning in with 2FA
When a user with 2FA enabled submits their email + password, Better Auth intercepts the session creation and calls the onTwoFactorRedirect callback you defined in auth-client.ts. The user is sent to /verify-otp.
// Standard TOTP code
const { error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: rememberDevice, // skip 2FA on this device for 30 days
})
// Backup code (when user lost their authenticator)
const { error } = await authClient.twoFactor.verifyBackupCode({
code, // one of the 10-character backup codes
})
Backup codes are single-use. Once a code is used it's invalidated. When all backup codes are exhausted the user needs to generate a new set from their account settings.
Trusted devices
When trustDevice: true is passed during verification, Better Auth sets a long-lived cookie that skips the OTP step on that device for 30 days.
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={rememberDevice}
onChange={(e) => setRememberDevice(e.target.checked)}
/>
Trust this device for 30 days
</label>
Backup codes
Backup codes are generated when 2FA is first enabled. Show them once, let the user copy or download them, and never display them again.
// Regenerate backup codes (invalidates the old ones)
const { data } = await authClient.twoFactor.generateBackupCodes({
password, // confirm identity before regenerating
})
// data.backupCodes → string[]
Always let users download backup codes as a .txt file. A user locked out of their authenticator app with no backup codes is locked out forever unless you have an account recovery flow.
Disabling 2FA
const { error } = await authClient.twoFactor.disable({
password, // required — prevents someone with a stolen session from disabling 2FA
})
Checking 2FA status
const { data: session } = useSession()
const has2FA = session?.user.twoFactorEnabled
Use this to conditionally show the enable/disable button in account settings.
Auth, billing, orgs, and emails — all wired up. Clone and deploy in minutes.
Get launch.now