Agent skill
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.
Install this agent skill to your Project
npx add-skill https://github.com/better-auth/skills/tree/main/better-auth/twoFactor
SKILL.md
Setup
- Add
twoFactor()plugin to server config withissuer - Add
twoFactorClient()plugin to client config - Run
npx @better-auth/cli migrate - Verify: check that
twoFactorSecretcolumn 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.twoFactorRedirectinonSuccess - If
true, redirect to/2faverification 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
}),
],
});
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
organization-best-practices
Configure multi-tenant organizations, manage members and invitations, define custom roles and permissions, set up teams, and implement RBAC using Better Auth's organization plugin. Use when users need org setup, team management, member roles, access control, or the Better Auth organization plugin.
email-and-password-best-practices
Configure email verification, implement password reset flows, set password policies, and customise hashing algorithms for Better Auth email/password authentication. Use when users need to set up login, sign-in, sign-up, credential authentication, or password security with Better Auth.
better-auth-best-practices
Configure Better Auth server and client, set up database adapters, manage sessions, add plugins, and handle environment variables. Use when users mention Better Auth, betterauth, auth.ts, or need to set up TypeScript authentication with email/password, OAuth, or plugin configuration.
create-auth-skill
Scaffold and implement authentication in TypeScript/JavaScript apps using Better Auth. Detect frameworks, configure database adapters, set up route handlers, add OAuth providers, and create auth UI pages. Use when users want to add login, sign-up, or authentication to a new or existing project with Better Auth.
better-icons
Use when working with icons in any project. Provides CLI for searching 200+ icon libraries (Iconify) and retrieving SVGs. Commands: `better-icons search <query>` to find icons, `better-icons get <id>` to get SVG. Also available as MCP server for AI agents.
handoff
Compact the current conversation into a handoff document for another agent to pick up.
Didn't find tool you were looking for?