two-factor-authentication-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Setting Up Two-Factor Authentication

设置双因素认证

When adding 2FA to your application, configure the
twoFactor
plugin with your app name as the issuer. This name appears in authenticator apps when users scan the QR code.
ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  appName: "My App", // Used as the default issuer for TOTP
  plugins: [
    twoFactor({
      issuer: "My App", // Optional: override the app name for 2FA specifically
    }),
  ],
});
Note: After adding the plugin, run
npx @better-auth/cli migrate
to add the required database fields and tables.
在你的应用中添加2FA时,请将
twoFactor
插件的发行方(issuer)配置为你的应用名称。用户扫描二维码时,该名称会显示在认证器应用中。
ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  appName: "My App", // Used as the default issuer for TOTP
  plugins: [
    twoFactor({
      issuer: "My App", // Optional: override the app name for 2FA specifically
    }),
  ],
});
注意:添加插件后,请运行
npx @better-auth/cli migrate
来添加所需的数据库字段和表。

Client-Side Setup

客户端设置

Add the client plugin and configure the redirect behavior for 2FA verification:
ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    twoFactorClient({
      onTwoFactorRedirect() {
        window.location.href = "/2fa"; // Redirect to your 2FA verification page
      },
    }),
  ],
});
添加客户端插件并配置2FA验证的重定向行为:
ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    twoFactorClient({
      onTwoFactorRedirect() {
        window.location.href = "/2fa"; // Redirect to your 2FA verification page
      },
    }),
  ],
});

Enabling 2FA for Users

为用户启用2FA

When a user enables 2FA, require their password for verification. The enable endpoint returns a TOTP URI for QR code generation and backup codes for account recovery.
ts
const enable2FA = async (password: string) => {
  const { data, error } = await authClient.twoFactor.enable({
    password,
  });

  if (data) {
    // data.totpURI - Use this to generate a QR code
    // data.backupCodes - Display these to the user for safekeeping
  }
};
Important: The
twoFactorEnabled
flag on the user is not set to
true
until the user successfully verifies their first TOTP code. This ensures users have properly configured their authenticator app before 2FA is fully active.
当用户启用2FA时,需要验证他们的密码。启用接口会返回一个用于生成二维码的TOTP URI,以及用于账户恢复的备份码。
ts
const enable2FA = async (password: string) => {
  const { data, error } = await authClient.twoFactor.enable({
    password,
  });

  if (data) {
    // data.totpURI - Use this to generate a QR code
    // data.backupCodes - Display these to the user for safekeeping
  }
};
重要提示:只有当用户成功验证其第一个TOTP代码后,用户的
twoFactorEnabled
标志才会设置为
true
。这确保用户在2FA完全激活前已正确配置好他们的认证器应用。

Skipping Initial Verification

跳过初始验证

If you want to enable 2FA immediately without requiring verification, set
skipVerificationOnEnable
:
ts
twoFactor({
  skipVerificationOnEnable: true, // Not recommended for most use cases
});
Note: This is generally not recommended as it doesn't confirm the user has successfully set up their authenticator app.
如果你想立即启用2FA而无需验证,可以设置
skipVerificationOnEnable
ts
twoFactor({
  skipVerificationOnEnable: true, // Not recommended for most use cases
});
注意:通常不建议这样做,因为它无法确认用户已成功设置好认证器应用。

TOTP (Authenticator App)

TOTP(认证器应用)

TOTP generates time-based codes using an authenticator app (Google Authenticator, Authy, etc.). Codes are valid for 30 seconds by default.
TOTP通过认证器应用(如Google Authenticator、Authy等)生成基于时间的代码。默认情况下,代码有效期为30秒。

Displaying the QR Code

显示二维码

Use the TOTP URI to generate a QR code for users to scan:
tsx
import QRCode from "react-qr-code";

const TotpSetup = ({ totpURI }: { totpURI: string }) => {
  return <QRCode value={totpURI} />;
};
使用TOTP URI生成供用户扫描的二维码:
tsx
import QRCode from "react-qr-code";

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

Verifying TOTP Codes

验证TOTP代码

Better Auth accepts codes from one period before and one after the current time, accommodating minor clock differences between devices:
ts
const verifyTotp = async (code: string) => {
  const { data, error } = await authClient.twoFactor.verifyTotp({
    code,
    trustDevice: true, // Optional: remember this device for 30 days
  });
};
Better Auth接受当前时间段前后各一个周期内的代码,以适应设备间的微小时钟差异:
ts
const verifyTotp = async (code: string) => {
  const { data, error } = await authClient.twoFactor.verifyTotp({
    code,
    trustDevice: true, // Optional: remember this device for 30 days
  });
};

TOTP Configuration Options

TOTP配置选项

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

OTP (Email/SMS)

OTP(邮件/短信)

OTP sends a one-time code to the user's email or phone. You must implement the
sendOTP
function to deliver codes.
OTP会将一次性代码发送到用户的邮箱或手机。你必须实现
sendOTP
函数来发送代码。

Configuring OTP Delivery

配置OTP发送

ts
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)
      },
    }),
  ],
});
ts
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

发送并验证OTP

ts
// Request an OTP to be sent
const sendOtp = async () => {
  const { data, error } = await authClient.twoFactor.sendOtp();
};

// Verify the OTP code
const verifyOtp = async (code: string) => {
  const { data, error } = await authClient.twoFactor.verifyOtp({
    code,
    trustDevice: true,
  });
};
ts
// Request an OTP to be sent
const sendOtp = async () => {
  const { data, error } = await authClient.twoFactor.sendOtp();
};

// Verify the OTP code
const verifyOtp = async (code: string) => {
  const { data, error } = await authClient.twoFactor.verifyOtp({
    code,
    trustDevice: true,
  });
};

OTP Storage Security

OTP存储安全性

Configure how OTP codes are stored in the database:
ts
twoFactor({
  otpOptions: {
    storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed"
  },
});
For custom encryption:
ts
twoFactor({
  otpOptions: {
    storeOTP: {
      encrypt: async (token) => myEncrypt(token),
      decrypt: async (token) => myDecrypt(token),
    },
  },
});
配置OTP代码在数据库中的存储方式:
ts
twoFactor({
  otpOptions: {
    storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed"
  },
});
自定义加密方式:
ts
twoFactor({
  otpOptions: {
    storeOTP: {
      encrypt: async (token) => myEncrypt(token),
      decrypt: async (token) => myDecrypt(token),
    },
  },
});

Backup Codes

备份码

Backup codes provide account recovery when users lose access to their authenticator app or phone. They are generated automatically when 2FA is enabled.
当用户无法访问认证器应用或手机时,备份码可用于账户恢复。启用2FA时会自动生成备份码。

Displaying Backup Codes

显示备份码

Always show backup codes to users when they enable 2FA:
tsx
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>
  );
};
启用2FA时,务必向用户显示备份码:
tsx
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

重新生成备份码

When users need new codes, regenerate them (this invalidates all previous codes):
ts
const regenerateBackupCodes = async (password: string) => {
  const { data, error } = await authClient.twoFactor.generateBackupCodes({
    password,
  });
  // data.backupCodes contains the new codes
};
当用户需要新的备份码时,可重新生成(这会使所有旧代码失效):
ts
const regenerateBackupCodes = async (password: string) => {
  const { data, error } = await authClient.twoFactor.generateBackupCodes({
    password,
  });
  // data.backupCodes contains the new codes
};

Using Backup Codes for Recovery

使用备份码恢复账户

ts
const verifyBackupCode = async (code: string) => {
  const { data, error } = await authClient.twoFactor.verifyBackupCode({
    code,
    trustDevice: true,
  });
};
Note: Each backup code can only be used once and is removed from the database after successful verification.
ts
const verifyBackupCode = async (code: string) => {
  const { data, error } = await authClient.twoFactor.verifyBackupCode({
    code,
    trustDevice: true,
  });
};
注意:每个备份码只能使用一次,验证成功后会从数据库中删除。

Backup Code Configuration

备份码配置

ts
twoFactor({
  backupCodeOptions: {
    amount: 10, // Number of codes to generate (default: 10)
    length: 10, // Length of each code (default: 10)
    storeBackupCodes: "encrypted", // Options: "plain", "encrypted"
  },
});
ts
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

登录时处理2FA

When a user with 2FA enabled signs in, the response includes
twoFactorRedirect: true
:
ts
const signIn = async (email: string, password: string) => {
  const { data, error } = await authClient.signIn.email(
    {
      email,
      password,
    },
    {
      onSuccess(context) {
        if (context.data.twoFactorRedirect) {
          // Redirect to 2FA verification page
          window.location.href = "/2fa";
        }
      },
    }
  );
};
当已启用2FA的用户登录时,响应会包含
twoFactorRedirect: true
ts
const signIn = async (email: string, password: string) => {
  const { data, error } = await authClient.signIn.email(
    {
      email,
      password,
    },
    {
      onSuccess(context) {
        if (context.data.twoFactorRedirect) {
          // Redirect to 2FA verification page
          window.location.href = "/2fa";
        }
      },
    }
  );
};

Server-Side 2FA Detection

服务端2FA检测

When using
auth.api.signInEmail
on the server, check for 2FA redirect:
ts
const response = await auth.api.signInEmail({
  body: {
    email: "user@example.com",
    password: "password",
  },
});

if ("twoFactorRedirect" in response) {
  // Handle 2FA verification
}
在服务端使用
auth.api.signInEmail
时,检查是否需要重定向到2FA验证:
ts
const response = await auth.api.signInEmail({
  body: {
    email: "user@example.com",
    password: "password",
  },
});

if ("twoFactorRedirect" in response) {
  // Handle 2FA verification
}

Trusted Devices

可信设备

Trusted devices allow users to skip 2FA verification on subsequent sign-ins for a configurable period.
可信设备允许用户在后续登录时跳过2FA验证,有效期可配置。

Enabling Trust on Verification

验证时启用可信设备

Pass
trustDevice: true
when verifying 2FA:
ts
await authClient.twoFactor.verifyTotp({
  code: "123456",
  trustDevice: true,
});
验证2FA时传入
trustDevice: true
ts
await authClient.twoFactor.verifyTotp({
  code: "123456",
  trustDevice: true,
});

Configuring Trust Duration

配置信任有效期

ts
twoFactor({
  trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days in seconds (default)
});
Note: The trust period refreshes on each successful sign-in within the trust window.
ts
twoFactor({
  trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days in seconds (default)
});
注意:在信任窗口内每次成功登录都会刷新信任周期。

Security Considerations

安全注意事项

Session Management

会话管理

During the 2FA flow:
  1. User signs in with credentials
  2. Session cookie is removed (not yet authenticated)
  3. A temporary two-factor cookie is set (default: 10-minute expiration)
  4. User verifies via TOTP, OTP, or backup code
  5. Session cookie is created upon successful verification
Configure the two-factor cookie expiration:
ts
twoFactor({
  twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
});
在2FA流程中:
  1. 用户使用凭据登录
  2. 会话Cookie被移除(尚未完成认证)
  3. 设置一个临时的双因素Cookie(默认有效期10分钟)
  4. 用户通过TOTP、OTP或备份码完成验证
  5. 验证成功后创建会话Cookie
配置双因素Cookie的有效期:
ts
twoFactor({
  twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
});

Rate Limiting

速率限制

Better Auth applies built-in rate limiting to all 2FA endpoints (3 requests per 10 seconds). For OTP verification, additional attempt limiting is applied:
ts
twoFactor({
  otpOptions: {
    allowedAttempts: 5, // Max attempts per OTP code (default: 5)
  },
});
Better Auth对所有2FA接口应用内置的速率限制(每10秒3次请求)。对于OTP验证,还会应用额外的尝试次数限制:
ts
twoFactor({
  otpOptions: {
    allowedAttempts: 5, // Max attempts per OTP code (default: 5)
  },
});

Encryption at Rest

静态加密

  • TOTP secrets are encrypted using symmetric encryption with your auth secret
  • Backup codes are stored encrypted by default
  • OTP codes can be configured for plain, encrypted, or hashed storage
  • TOTP密钥使用你的认证密钥通过对称加密存储
  • 备份码默认加密存储
  • OTP代码可配置为明文、加密或哈希存储

Constant-Time Comparison

恒定时间比较

Better Auth uses constant-time comparison for OTP verification to prevent timing attacks.
Better Auth使用恒定时间比较来验证OTP代码,以防止计时攻击。

Credential Account Requirement

凭据账户要求

Two-factor authentication can only be enabled for credential (email/password) accounts. For social accounts, it's assumed the provider already handles 2FA.
双因素认证仅能为凭据(邮箱/密码)账户启用。对于社交账户,假设提供商已处理2FA。

Disabling 2FA

禁用2FA

Allow users to disable 2FA with password confirmation:
ts
const disable2FA = async (password: string) => {
  const { data, error } = await authClient.twoFactor.disable({
    password,
  });
};
Note: When 2FA is disabled, trusted device records are revoked.
允许用户通过密码确认来禁用2FA:
ts
const disable2FA = async (password: string) => {
  const { data, error } = await authClient.twoFactor.disable({
    password,
  });
};
注意:禁用2FA后,可信设备记录会被撤销。

Complete Configuration Example

完整配置示例

ts
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
    }),
  ],
});
ts
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
    }),
  ],
});