two-factor-authentication-best-practices by better-auth/skills
npx skills add https://github.com/better-auth/skills --skill two-factor-authentication-best-practicesissuer 的 twoFactor() 插件twoFactorClient() 插件npx @better-auth/cli migratetwoFactorSecret 列import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
appName: "My App",
plugins: [
twoFactor({
issuer: "My App",
}),
],
});
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = "/2fa";
},
}),
],
});
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
需要密码验证。返回 TOTP URI(用于生成二维码)和备用代码。
const enable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.enable({
password,
});
if (data) {
// data.totpURI — 用于生成二维码
// data.backupCodes — 展示给用户
}
};
在首次 TOTP 验证成功之前,twoFactorEnabled 不会被设置为 true。可通过 skipVerificationOnEnable: true 覆盖此行为(不推荐)。
import QRCode from "react-qr-code";
const TotpSetup = ({ totpURI }: { totpURI: string }) => {
return <QRCode value={totpURI} />;
};
接受当前时间前后一个周期内的验证码:
const verifyTotp = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true,
});
};
twoFactor({
totpOptions: {
digits: 6, // 6 或 8 位数字(默认:6)
period: 30, // 验证码有效期,单位秒(默认:30)
},
});
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: "您的验证码",
text: `您的验证码是:${otp}`,
});
},
period: 5, // 验证码有效期,单位分钟(默认:3)
digits: 6, // 数字位数(默认:6)
allowedAttempts: 5, // 最大验证尝试次数(默认:5)
},
}),
],
});
发送:authClient.twoFactor.sendOtp()。验证:authClient.twoFactor.verifyOtp({ code, trustDevice: true })。
配置 OTP 验证码在数据库中的存储方式:
twoFactor({
otpOptions: {
storeOTP: "encrypted", // 选项:"plain"、"encrypted"、"hashed"
},
});
自定义加密:
twoFactor({
otpOptions: {
storeOTP: {
encrypt: async (token) => myEncrypt(token),
decrypt: async (token) => myDecrypt(token),
},
},
});
启用双因素认证时自动生成。每个代码仅限使用一次。
const BackupCodes = ({ codes }: { codes: string[] }) => {
return (
<div>
<p>请将这些代码保存在安全的地方:</p>
<ul>
{codes.map((code, i) => (
<li key={i}>{code}</li>
))}
</ul>
</div>
);
};
使之前的所有代码失效:
const regenerateBackupCodes = async (password: string) => {
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password,
});
// data.backupCodes 包含新代码
};
const verifyBackupCode = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code,
trustDevice: true,
});
};
twoFactor({
backupCodeOptions: {
amount: 10, // 生成的代码数量(默认:10)
length: 10, // 每个代码的长度(默认:10)
storeBackupCodes: "encrypted", // 选项:"plain"、"encrypted"
},
});
当需要双因素认证时,响应会包含 twoFactorRedirect: true:
signIn.email({ email, password })onSuccess 中检查 context.data.twoFactorRedirecttrue,重定向到 /2fa 验证页面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";
}
},
}
);
};
服务器端:使用 auth.api.signInEmail 时检查响应中是否包含 "twoFactorRedirect"。
验证时传递 trustDevice: true。默认信任时长:30 天(trustDeviceMaxAge)。每次登录时刷新。
流程:凭据 → 移除会话 → 临时双因素认证 Cookie(默认 10 分钟)→ 验证 → 创建会话。
twoFactor({
twoFactorCookieMaxAge: 600, // 10 分钟,单位秒(默认值)
});
内置限制:所有双因素认证端点每 10 秒 3 次请求。OTP 有额外的尝试限制:
twoFactor({
otpOptions: {
allowedAttempts: 5, // 每个 OTP 验证码的最大尝试次数(默认:5)
},
});
TOTP 密钥:使用身份验证密钥加密。备用代码:默认加密。OTP:可配置("plain"、"encrypted"、"hashed")。验证时使用恒定时间比较。
双因素认证只能为凭据(电子邮件/密码)账户启用。
需要密码确认。撤销受信任设备记录:
const disable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.disable({
password,
});
};
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 设置
issuer: "My App",
totpOptions: {
digits: 6,
period: 30,
},
// OTP 设置
otpOptions: {
sendOTP: async ({ user, otp }) => {
await sendEmail({
to: user.email,
subject: "您的验证码",
text: `您的验证码是:${otp}`,
});
},
period: 5,
allowedAttempts: 5,
storeOTP: "encrypted",
},
// 备用代码设置
backupCodeOptions: {
amount: 10,
length: 10,
storeBackupCodes: "encrypted",
},
// 会话设置
twoFactorCookieMaxAge: 600, // 10 分钟
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 天
}),
],
});
每周安装量
3.5K
代码仓库
GitHub 星标数
151
首次出现
2026年2月10日
安全审计
已安装于
opencode3.3K
codex3.3K
gemini-cli3.3K
github-copilot3.2K
amp3.2K
kimi-cli3.1K
twoFactor() plugin to server config with issuertwoFactorClient() plugin to client confignpx @better-auth/cli migratetwoFactorSecret column exists on user tableimport { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
appName: "My App",
plugins: [
twoFactor({
issuer: "My App",
}),
],
});
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = "/2fa";
},
}),
],
});
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).
import QRCode from "react-qr-code";
const TotpSetup = ({ totpURI }: { totpURI: string }) => {
return <QRCode value={totpURI} />;
};
Accepts codes from one period before/after current time:
const verifyTotp = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyTotp({
code,
trustDevice: true,
});
};
twoFactor({
totpOptions: {
digits: 6, // 6 or 8 digits (default: 6)
period: 30, // Code validity period in seconds (default: 30)
},
});
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)
},
}),
],
});
Send: authClient.twoFactor.sendOtp(). Verify: authClient.twoFactor.verifyOtp({ code, trustDevice: true }).
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),
},
},
});
Generated automatically when 2FA is enabled. Each code is single-use.
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>
);
};
Invalidates all previous codes:
const regenerateBackupCodes = async (password: string) => {
const { data, error } = await authClient.twoFactor.generateBackupCodes({
password,
});
// data.backupCodes contains the new codes
};
const verifyBackupCode = async (code: string) => {
const { data, error } = await authClient.twoFactor.verifyBackupCode({
code,
trustDevice: true,
});
};
twoFactor({
backupCodeOptions: {
amount: 10, // Number of codes to generate (default: 10)
length: 10, // Length of each code (default: 10)
storeBackupCodes: "encrypted", // Options: "plain", "encrypted"
},
});
Response includes twoFactorRedirect: true when 2FA is required:
signIn.email({ email, password })context.data.twoFactorRedirect in onSuccesstrue, redirect to /2fa verification pageconst 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.
Pass trustDevice: true when verifying. Default trust duration: 30 days (trustDeviceMaxAge). Refreshes on each sign-in.
Flow: credentials → session removed → temporary 2FA cookie (10 min default) → verify → session created.
twoFactor({
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
});
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)
},
});
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.
Requires password confirmation. Revokes trusted device records:
const disable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.disable({
password,
});
};
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
}),
],
});
Weekly Installs
3.5K
Repository
GitHub Stars
151
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode3.3K
codex3.3K
gemini-cli3.3K
github-copilot3.2K
amp3.2K
kimi-cli3.1K
97,600 周安装