PromptShop

Two Factor Authentication Best Practices

This skill guides developers in implementing two-factor authentication (2FA) using Better Auth. It covers setting up the 2FA plugin, enabling 2FA for users,...

Install

npx promptshop add two-factor-authentication-best-practices

Details

What This Skill Does

This skill guides developers in implementing two-factor authentication (2FA) using Better Auth. It covers setting up the 2FA plugin, enabling 2FA for users, verifying TOTP codes from authenticator apps, and configuring OTP delivery via email/SMS. This is useful for developers looking to enhance the security of their applications with 2FA.

When to Use

  • Set up the two-factor authentication plugin.
  • Enable 2FA for users.
  • Verify TOTP codes from authenticator apps.
  • Configure OTP delivery via email/SMS.
  • Display QR codes for TOTP setup.
  • Handle 2FA redirects.

Key Features

  • Adds the twoFactor() plugin to the server config.
  • Adds the twoFactorClient() plugin to the client config.
  • Enables 2FA with password verification.
  • Returns TOTP URI for QR code generation.
  • Verifies TOTP codes with a time window.
  • Configures OTP delivery for email/SMS.

Manual Installation

Setup

  • Add twoFactor() plugin to server config with issuer.
  • Add twoFactorClient() plugin to client config.
  • Run npx @better-auth/cli migrate.
  • Verify: check that twoFactorSecret column exists on user table.
  • import { betterAuth } from "better-auth";.
  • import { twoFactor } from "better-auth/plugins";.
  • export const auth = betterAuth({.
  • appName: "My App",.
  • plugins: [.
  • twoFactor({.
  • issuer: "My App",.
  • }),.
  • ],.
  • });.

Client-Side Setup

  • import { createAuthClient } from "better-auth/client";.
  • import { twoFactorClient } from "better-auth/client/plugins";.
  • export const authClient = createAuthClient({.
  • plugins: [.
  • twoFactorClient({.
  • onTwoFactorRedirect() {.
  • window.location.href = "/2fa";.
  • },.
  • }),.
  • ],.
  • });.

Enabling 2FA for Users

Requires password verification. Returns TOTP URI (for QR code) and backup codes.

const enable2FA = async (password: string) => { const { data, error } = await authClient.twoFactor.enable({ password, });

if (data) { // data.totpURI — generate a QR code from this // data.backupCodes — display to user } };

twoFactor. Enabled is not set to true until first TOTP verification succeeds. Override with skipVerificationOn. Enable: true (not recommended).

TOTP (Authenticator App)

Displaying the QR Code

import QRCode from "react-qr-code";

const Totp. Setup = ({ totpURI }: { totpURI: string }) => { return <QRCode value={totpURI} />; };

Verifying TOTP Codes

Accepts codes from one period before/after current time:

const verifyTotp = async (code: string) => { const { data, error } = await authClient.twoFactor.verifyTotp({ code, trustDevice: true, }); };

TOTP Configuration Options

twoFactor({ totpOptions: { digits: 6, // 6 or 8 digits (default: 6) period: 30, // Code validity period in seconds (default: 30) }, });

OTP (Email/SMS)

Configuring OTP Delivery

import { betterAuth } from "better-auth"; import { twoFactor } from "better-auth/plugins"; import { sendEmail } from "./email";

export const auth = betterAuth({ plugins: [ twoFactor({ otpOptions: { sendOTP: async ({ user, otp }, ctx) => { await sendEmail({ to: user.email, subject: "Your verification code", text: Your code is: ${otp}, }); }, period: 5, // Code validity in minutes (default: 3) digits: 6, // Number of digits (default: 6) allowed. Attempts: 5, // Max verification attempts (default: 5) }, }), ], });

Sending and Verifying OTP

Send: authClient.twoFactor.sendOtp(). Verify: authClient.twoFactor.verifyOtp({ code, trustDevice: true }).

OTP Storage Security

Configure how OTP codes are stored in the database:

twoFactor({ otpOptions: { storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed" }, });

For custom encryption:

twoFactor({ otpOptions: { storeOTP: { encrypt: async (token) => myEncrypt(token), decrypt: async (token) => myDecrypt(token), }, }, });

Backup Codes

Generated automatically when 2FA is enabled. Each code is single-use.

Displaying Backup Codes

const BackupCodes = ({ codes }: { codes: string[] }) => { return ( <div> <p>Save these codes in a secure location:</p> <ul> {codes.map((code, i) => ( <li key={i}>{code}</li> ))} </ul> </div> ); };

Regenerating Backup Codes

Invalidates all previous codes:

const regenerateBackupCodes = async (password: string) => { const { data, error } = await authClient.twoFactor.generateBackupCodes({ password, }); // data.backupCodes contains the new codes };

Using Backup Codes for Recovery

const verifyBackupCode = async (code: string) => { const { data, error } = await authClient.twoFactor.verifyBackupCode({ code, trustDevice: true, }); };

Backup Code Configuration

twoFactor({ backupCodeOptions: { amount: 10, // Number of codes to generate (default: 10) length: 10, // Length of each code (default: 10) storeBackupCodes: "encrypted", // Options: "plain", "encrypted" }, });

Handling 2FA During Sign-In

Response includes twoFactorRedirect: true when 2FA is required:

Sign-In Flow

Call signIn.email({ email, password }) Check context.data.twoFactorRedirect in onSuccess If true, redirect to /2fa verification page Verify via TOTP, OTP, or backup code Session cookie is created on successful verification

const signIn = async (email: string, password: string) => { const { data, error } = await authClient.signIn.email( { email, password }, { onSuccess(context) { if (context.data.twoFactorRedirect) { window.location.href = "/2fa"; } }, } ); };

Server-side: check "twoFactorRedirect" in response when using auth.api.signInEmail.

Trusted Devices

Pass trustDevice: true when verifying. Default trust duration: 30 days (trustDeviceMaxAge). Refreshes on each sign-in.

Security Considerations

Session Management

Flow: credentials → session removed → temporary 2FA cookie (10 min default) → verify → session created.

twoFactor({ twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default) });

Rate Limiting

Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional attempt limiting:

twoFactor({ otpOptions: { allowed. Attempts: 5, // Max attempts per OTP code (default: 5) }, });

Encryption at Rest

TOTP secrets: encrypted with auth secret. Backup codes: encrypted by default. OTP: configurable ("plain", "encrypted", "hashed"). Uses constant-time comparison for verification.

2FA can only be enabled for credential (email/password) accounts.

Disabling 2FA

Requires password confirmation. Revokes trusted device records:

const disable2FA = async (password: string) => { const { data, error } = await authClient.twoFactor.disable({ password, }); };

Complete Configuration Example

import { betterAuth } from "better-auth"; import { twoFactor } from "better-auth/plugins"; import { sendEmail } from "./email";

export const auth = betterAuth({ appName: "My App", plugins: [ twoFactor({ // TOTP settings issuer: "My App", totpOptions: { digits: 6, period: 30, }, // OTP settings otpOptions: { sendOTP: async ({ user, otp }) => { await sendEmail({ to: user.email, subject: "Your verification code", text: Your code is: ${otp}, }); }, period: 5, allowed. Attempts: 5, storeOTP: "encrypted", }, // Backup code settings backupCodeOptions: { amount: 10, length: 10, storeBackupCodes: "encrypted", }, // Session settings twoFactorCookieMaxAge: 600, // 10 minutes trustDeviceMaxAge: 30 24 60 * 60, // 30 days }), ], });

Setup

  • Add twoFactor() plugin to server config with issuer.
  • Add twoFactorClient() pl.