Back to Skills

Two Factor Authentication Best Practices

Configure TOTP authenticator apps, send OTP codes via email/SMS, manage backup codes, handle trusted devices, and implement 2FA sign-in flows using Better Auth's twoFactor plugin. Use when users need MFA, multi-factor authentication, authenticator setup, or login security with Better Auth.

$ npx promptcreek add two-factor-authentication-best-practices

Auto-detects your installed agents and installs the skill to each one.

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.

Installation

Run in your project directory:
$ npx promptcreek add two-factor-authentication-best-practices

Auto-detects your installed agents (Claude Code, Cursor, Codex, etc.) and installs the skill to each one.

View Full Skill Content

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

}

};

twoFactorEnabled is not set to true until first TOTP verification succeeds. Override with skipVerificationOnEnable: true (not recommended).

TOTP (Authenticator App)

Displaying the QR Code

import QRCode from "react-qr-code";

const TotpSetup = ({ 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)

allowedAttempts: 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: {

allowedAttempts: 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,

allowedAttempts: 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

}),

],

});

0Installs
0Views

Supported Agents

Claude CodeCursorCodexGemini CLIAiderWindsurfOpenClaw

Details

License
MIT
Source
admin
Published
3/18/2026

Related Skills