两因素认证 (2FA)

什么是两因素认证 (2FA)?

两因素认证(2FA)是一种安全机制,它要求用户在登录时提供两种不同类型的身份验证因素。这通常包括"你知道的东西"(如密码)和"你拥有的东西"(如手机或安全密钥)。通过要求多重验证步骤,2FA显著提高了账户安全性,即使密码被泄露,未授权用户也难以访问账户。

Better Auth的2FA插件支持多种验证方法,包括基于时间的一次性密码(TOTP)和一次性密码(OTP),让你能够为用户提供强大而灵活的安全选项。

此插件提供两种主要方法来进行第二因素验证:

  1. OTP(一次性密码):发送到用户电子邮件或手机的临时代码。
  2. TOTP(基于时间的一次性密码):由用户设备上的应用程序生成的代码。

附加功能包括:

  • 生成账户恢复的备份码
  • 启用/禁用 2FA
  • 管理可信设备

安装

将插件添加到你的认证配置中

将两因素插件添加到你的认证配置中,并指定你的应用名称作为发行者。

auth.ts
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"
 
export const auth = betterAuth({
    // ... 其他配置选项
    appName: "My App", // 提供你的应用名称。它将被用作发行者。
    plugins: [
        twoFactor() 
    ]
})

迁移数据库

运行迁移或生成schema,以向数据库添加必要的字段和表。

npx @better-auth/cli migrate

查看schema部分以手动添加字段。

添加客户端插件

添加客户端插件并指定如果用户需要验证第二因素时应该重定向到哪里

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
 
const authClient = createAuthClient({
    plugins: [
        twoFactorClient()
    ]
})

使用方法

启用 2FA

要启用两因素认证,请调用 twoFactor.enable 并提供用户密码和发行者(可选):

two-factor.ts
const { data } = await authClient.twoFactor.enable({
  password: "password", // 需要用户密码
  issuer: "my-app-name", // 可选,默认为应用名称
});

启用 2FA 时:

  • 会生成加密的 secretbackupCodes
  • enable 返回 totpURIbackupCodes

注意:在用户验证其 TOTP 代码之前,twoFactorEnabled 不会设置为 true

要验证,请显示二维码供用户使用认证器应用扫描。用户输入代码后,调用 verifyTotp

await authClient.twoFactor.verifyTotp({
    code: "" // 用户输入
})

你可以通过在插件配置中设置 skipVerificationOnEnable 为 true 来跳过验证。

使用 2FA 登录

当启用了 2FA 的用户尝试通过电子邮件登录时,响应将包含 twoFactorRedirect 设置为 true。这表示用户需要验证其 2FA 代码。

sign-in.ts
await authClient.signIn.email({
    email: "[email protected]",
    password: "password123",
})

你可以在 onSuccess 回调中处理此情况,或者通过在插件配置中提供 onTwoFactorRedirect 回调来处理。

sign-in.ts
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
 
const authClient = createAuthClient({
    plugins: [twoFactorClient({
        onTwoFactorRedirect(){
            // 全局处理 2FA 验证
        }
    })]
})

或者你可以在当前位置处理:

await authClient.signIn.email({
        email: "[email protected]",
        password: "password123",
    }, {
        async onSuccess(context) {
            if (context.data.twoFactorRedirect) {
                // 在当前位置处理 2FA 验证
            }
        }
    }
})

使用 auth.api

当你在服务器上调用 auth.api.signInEmail,并且用户启用了 2FA,它默认会返回一个 twoFactorRedirect 设置为 true 的对象。这种行为在 TypeScript 中没有被推断出来,这可能会产生误导。我们建议传递 asResponse: true 以获取 Response 对象。

const response = await auth.api.signInEmail({
    email: "[email protected]",
    password: "secure-password",
    asResponse: true
})

禁用 2FA

要禁用两因素认证,请调用 twoFactor.disable 并提供用户密码:

two-factor.ts
const { data } = await authClient.twoFactor.disable({
    password: "password" // 需要用户密码
})

TOTP

TOTP(基于时间的一次性密码)是一种算法,它使用时间作为计数器为每次登录尝试生成唯一密码。每个固定间隔(Better Auth 默认为 30 秒),都会生成一个新密码。这解决了传统密码的几个问题:它们可能被忘记、被盗或被猜测。OTP 解决了其中一些问题,但它们通过短信或电子邮件的传递可能不可靠(或者甚至有风险,考虑到它打开了新的攻击向量)。

然而,TOTP 离线生成代码,使其既安全又方便。你只需要在手机上安装一个认证器应用,就可以了——不需要互联网。

获取 TOTP URI

启用 2FA 后,你可以获取 TOTP URI 显示给用户。此 URI 由服务器使用 secretissuer 生成,可用于生成二维码供用户使用认证器应用扫描。

const { data, error } = await authClient.twoFactor.getTotpUri({
    password: "password" // 需要用户密码
})

示例:使用 React

user-card.tsx
import QRCode from "react-qr-code";
 
export default function UserCard(){
    const { data: session } = client.useSession();
	const { data: qr } = useQuery({
		queryKey: ["two-factor-qr"],
		queryFn: async () => {
			const res = await authClient.twoFactor.getTotpUri();
			return res.data;
		},
		enabled: !!session?.user.twoFactorEnabled,
	});
    return (
        <QRCode value={qr?.totpURI || ""} />
   )
}

默认情况下,TOTP 的发行者设置为认证配置中提供的应用名称,如果未提供,则设置为 Better Auth。你可以通过在插件配置中传递 issuer 来覆盖此设置。

验证 TOTP

用户输入其 2FA 代码后,你可以使用 twoFactor.verifyTotp 方法进行验证。

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

OTP

OTP(一次性密码)类似于 TOTP,但是会生成随机代码并发送到用户的电子邮件或手机。

在使用 OTP 验证第二因素之前,你需要在 Better Auth 实例中配置 sendOTP。此函数负责将 OTP 发送到用户的电子邮件、手机或应用程序支持的任何其他方法。

auth.ts
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins"
 
export const auth = betterAuth({
    plugins: [
        twoFactor({
          	otpOptions: {
				async sendOTP({ user, otp }, request) {
                    // 向用户发送 otp
				},
			},
        })
    ]
})

发送 OTP

通过调用 twoFactor.sendOtp 函数发送 OTP。此函数将触发你在 Better Auth 配置中提供的 sendOTP 实现。

const { data, error } = await authClient.twoFactor.sendOtp()
if (data) {
    // 重定向或显示用户输入代码
}

验证 OTP

用户输入 OTP 代码后,你可以验证它

const verifyOtp = async (code: string) => {
    await authClient.twoFactor.verifyOtp({ code }, {
        onSuccess(){
            //成功后重定向用户
        },
        onError(ctx){
            alert(ctx.error.message)
        }
    })
}

备份码

备份码在数据库中生成和存储。如果用户失去对手机或电子邮件的访问权限,这可以用于恢复对账户的访问权限。

生成备份码

为账户恢复生成备份码:

const { data, error } = await authClient.twoFactor.generateBackupCodes({
    password: "password" // 需要用户密码
})
if (data) {
    // 向用户显示备份码
}

使用备份码

现在你可以允许用户提供备份码作为账户恢复方法。

await authClient.twoFactor.verifyBackupCode({code: ""}, {
    onSuccess(){
        //成功后重定向用户
    },
    onError(ctx){
        alert(ctx.error.message)
    }
})

一旦使用了备份码,它将从数据库中删除,不能再次使用。

查看备份码

你可以随时通过调用 viewBackupCodes 查看备份码。此操作只能在服务器上使用 auth.api 执行。

await auth.api.viewBackupCodes({
    body: {
        userId: "user-id"
    }
})

可信设备

你可以通过在 verifyTotpverifyOtp 中传递 trustDevice 将设备标记为可信。

const verify2FA = async (code: string) => {
    const { data, error } = await authClient.twoFactor.verifyTotp({
        code,
        callbackURL: "/dashboard",
        trustDevice: true // 将此设备标记为可信
    })
    if (data) {
        // 2FA 已验证,设备已信任
    }
}

trustDevice 设置为 true 时,当前设备将被记住 60 天。在此期间,用户在后续从此设备登录时不会被要求提供 2FA。每次用户成功登录时,信任期都会刷新。

发行者

通过添加 issuer,你可以为 2fa 应用程序设置你的应用程序名称。

例如,如果你的用户使用 Google Auth,默认的 appName 将显示为 Better Auth。然而,通过使用以下代码,它将显示为 my-app-name

twoFactor({
    issuer: "my-app-name"
})

schema

该插件需要在 user 表中添加 1 个附加字段,并添加 1 个额外的表来存储两因素认证数据。

Field NameTypeKeyDescription
twoFactorEnabledboolean用户是否启用了两因素认证。

表:twoFactor

Field NameTypeKeyDescription
idstring两因素认证的 id。
userIdstring用户的 id
secretstring用于生成 TOTP 代码的密钥。
backupCodesstring如果用户失去对手机或电子邮件的访问权限,用于恢复账户访问的备份码。

选项

服务器

twoFactorTable:存储两因素认证数据的表名。默认:twoFactor

skipVerificationOnEnable:在为用户启用两因素之前跳过验证过程。

Issuer:发行者是你的应用程序的名称。它用于生成 TOTP 代码。它将显示在认证器应用程序中。

TOTP 选项

这些是 TOTP 的选项。

PropTypeDefault
digits?
number
6
period?
number
30

OTP 选项

这些是 OTP 的选项。

PropTypeDefault
sendOTP?
function
-
period?
number
30

备份码选项

当用户启用两因素认证时,备份码会在数据库中生成和存储。如果用户失去对手机或电子邮件的访问权限,可以用于恢复对账户的访问权限。

PropTypeDefault
amount?
number
10
length?
number
10
customBackupCodesGenerate?
function
-

客户端

要在客户端使用两因素插件,你需要将其添加到你的插件列表中。

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { twoFactorClient } from "better-auth/client/plugins"
 
const authClient =  createAuthClient({
    plugins: [
        twoFactorClient({ 
            onTwoFactorRedirect(){
                window.location.href = "/2fa" // 处理 2FA 验证重定向
            }
        }) 
    ]
})

选项

onTwoFactorRedirect:当用户需要验证其 2FA 代码时将调用的回调。这可用于将用户重定向到 2FA 页面。