Passkey 密钥

Passkey 密钥

Passkey 密钥是一种安全的无密码认证方法,使用加密密钥对,由 WebAuthn 和 FIDO2 标准支持,适用于网页浏览器。它用独特的密钥对替代密码:私钥存储在用户设备上,公钥共享给网站。用户可以通过生物识别、PIN 码或安全密钥登录,提供强大且抗钓鱼的认证,免除了传统密码的使用。

Passkey 插件的实现幕后由 SimpleWebAuthn 提供支持。

安装

安装插件

npm install @better-auth/passkey

将插件添加到认证配置

要将 passkey 插件添加到您的认证配置中,需要导入插件并将其传递给认证实例的 plugins 选项。

auth.ts
import { betterAuth } from "better-auth"
import { passkey } from "@better-auth/passkey"

export const auth = betterAuth({
    plugins: [ 
        passkey(), 
    ],
})

迁移数据库

运行迁移或生成架构以将所需的字段和表添加到数据库。

npx auth migrate
npx auth generate

参见 架构 部分以手动添加字段。

添加客户端插件

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { passkeyClient } from "@better-auth/passkey/client"

export const authClient = createAuthClient({
    plugins: [
        passkeyClient() 
    ]
})

配置(可选)

您可以自定义 passkey 插件,以支持 passkey-first 引导流程或 WebAuthn 扩展。

auth.ts
import { betterAuth } from "better-auth"
import { passkey } from "@better-auth/passkey"

export const auth = betterAuth({
  plugins: [
    passkey({
      registration: {
        // 默认值:true。设置为 false 可用于 passkey-first 引导。
        requireSession: false,
        // 当 requireSession 为 false 且不存在会话时必需。
        resolveUser: async ({ ctx, context }) => {
          // 验证 context(例如签名令牌),然后创建或加载用户。
          return { id: "user-id", name: "user@example.com" }
        },
        // 可选的服务端定义扩展
        extensions: { credProps: true },
      },
      authentication: {
        // 可选的服务端定义扩展
        extensions: { credProps: true },
      },
    }),
  ],
})

Passkey-first 注册(预认证)

registration.requireSessionfalse 时,可以在没有会话的情况下发起 passkey 注册。您可以向注册选项端点传递一个不透明的 context;它会被转发给 resolveUser

await auth.api.generatePasskeyRegistrationOptions({
  context: "signed-registration-token",
})

当使用 passkey-first 流程(registration.requireSession: false)时,在注册 passkey 时,请从客户端传递相同的 context,以便服务器在验证期间解析用户:

await authClient.passkey.addPasskey({
  name: "Primary passkey",
  context: "signed-registration-token",
})

用法

添加/注册 passkey 密钥

要添加或注册 passkey 密钥,请确保用户已认证,然后调用客户端提供的 passkey.addPasskey 函数。

POST/passkey/add-passkey
const { data, error } = await authClient.passkey.addPasskey({    name: "example-passkey-name",    authenticatorAttachment: "cross-platform",    extensions,    returnWebAuthnResponse,    context,});
Parameters
namestring

可选的名称,用于标记正在注册的认证器账户。如果未提供,默认将使用用户的电子邮件地址或用户 ID

authenticatorAttachment"platform" | "cross-platform"

还可以指定要注册的认证器类型。默认行为允许注册平台密钥和跨平台密钥

extensionsAuthenticationExtensionsClientInputs

可选的 WebAuthn 扩展(例如 PRF、credProps、largeBlob)

returnWebAuthnResponseboolean

返回 WebAuthn 响应和扩展结果

contextstring

用于 passkey-first 注册流程的可选上下文。转发到 registration.resolveUser

在获取选项中设置 throw: true 对注册和登录的 passkey 响应没有影响——它们始终返回包含错误对象的数据对象。

使用 passkey 登录

要使用 passkey 登录,可以使用 signIn.passkey 方法。这会提示用户使用他们的 passkey 登录。

POST/sign-in/passkey
const { data, error } = await authClient.signIn.passkey({    autoFill: true,    extensions,    returnWebAuthnResponse,});
Parameters
autoFillboolean

浏览器自动填充,也称为条件 UI。阅读更多:https://simplewebauthn.dev/docs/packages/browser#浏览器自动填充-aka-条件-ui

extensionsAuthenticationExtensionsClientInputs

可选的 WebAuthn 扩展(例如 PRF、credProps、largeBlob)

returnWebAuthnResponseboolean

返回 WebAuthn 响应和扩展结果

示例用法

import { authClient } from "@/lib/auth-client";

// 登录后跳转
await authClient.signIn.passkey({
    autoFill: true,
    // 可选扩展
    extensions: { credProps: true },
    fetchOptions: {
        onSuccess(context) {
            // 身份验证成功后跳转到仪表盘
            window.location.href = "/dashboard";
        },
        onError(context) {
            // 处理身份验证错误
            console.error("身份验证失败:", context.error.message);
        }
    }
});

扩展

您可以通过客户端 API 传递 extensions 来使用 WebAuthn 扩展。设置 returnWebAuthnResponse 为 true 时,客户端会返回 webauthn.clientExtensionResults

const result = await authClient.passkey.addPasskey({
  name: "My Passkey",
  extensions: {
    // 示例扩展输入(通用)
    credProps: true,
  },
  returnWebAuthnResponse: true,
});

console.log(result.webauthn?.clientExtensionResults);

列出 passkey

您可以通过调用 passkey.listUserPasskeys 列出所有已认证用户的 passkey:

GET/passkey/list-user-passkeys
const { data: passkeys, error } = await authClient.passkey.listUserPasskeys();

删除 passkey

您可以通过调用 passkey.delete 并提供 passkey ID 来删除 passkey。

POST/passkey/delete-passkey
const { data, error } = await authClient.passkey.deletePasskey({    id: "some-passkey-id", // required});
Parameters
idstringrequired

要删除的 passkey 的 ID。

更新 passkey 名称

POST/passkey/update-passkey
const { data, error } = await authClient.passkey.updatePasskey({    id: "id of passkey", // required    name: "my-new-passkey-name", // required});
Parameters
idstringrequired

要更新的 passkey 的 ID。

namestringrequired

更新后的新名称。

条件 UI

插件支持条件 UI,允许浏览器在用户已注册 passkey 时自动填充。

条件 UI 要正常工作需要两个条件:

更新输入字段

在输入字段中添加 autocomplete 属性,并将其值设置为 webauthn。您可以将此属性添加到多个输入字段,但至少需要一个才能使条件 UI 正常工作。

webauthn 值也应该是 autocomplete 属性中的最后一个条目。

<label for="name">Username:</label>
<input type="text" name="name" autocomplete="username webauthn">
<label for="password">Password:</label>
<input type="password" name="password" autocomplete="current-password webauthn">

预加载 passkey

当组件挂载时,您可以通过将 autoFill 选项设置为 true 来预加载用户的 passkey,调用 authClient.signIn.passkey 方法。

为了防止不必要的调用,我们还将添加检查以确认浏览器是否支持条件 UI。

useEffect(() => {
   if (!PublicKeyCredential.isConditionalMediationAvailable ||
       !PublicKeyCredential.isConditionalMediationAvailable()) {
     return;
   }

  void authClient.signIn.passkey({ autoFill: true })
}, [])

表名:passkey

Table
字段
类型
描述
id
string
PK
Unique identifier for each passkey
name ?
string
-
The name of the passkey
publicKey
string
-
The public key of the passkey
userId
string
FK
The ID of the user
credentialID
string
-
The unique identifier of the registered credential
counter
number
-
The counter of the passkey
deviceType
string
-
The type of device used to register the passkey
backedUp
boolean
-
Whether the passkey is backed up
transports ?
string
-
The transports used to register the passkey
createdAt ?
Date
-
The time when the passkey was created
aaguid ?
string
-
Authenticator's Attestation GUID indicating the type of the authenticator

选项

rpID:基于您的身份验证服务器源的唯一网站标识符。开发环境可用 'localhost'。RP ID 可通过舍弃有效顶级域名左侧的零个或多个标签而形成,例如 www.example.com 可使用 www.example.comexample.com 作为 RP ID,但不能使用顶级域名 com

rpName:您网站的人类可读名称。

origin:您的 better-auth 服务器所在的源 URL,例如 http://localhosthttp://localhost:PORT。请勿包含尾部斜杠。

authenticatorSelection:允许自定义 WebAuthn 认证器选择标准。若不指定则为默认设置。

  • authenticatorAttachment:指定认证器类型
    • platform:认证器附着于平台(例如指纹读取器)
    • cross-platform:认证器未附着于平台(例如安全密钥)
    • 默认:未设置(允许平台和跨平台,优先使用平台)
  • residentKey:决定凭证存储行为。
    • required:用户必须在认证器上存储凭证(最高安全性)
    • preferred:鼓励存储凭证但非强制
    • discouraged:不要求存储凭证(最快体验)
    • 默认:preferred
  • userVerification:控制生物识别/PIN 验证:
    • required:用户必须验证身份(最高安全性)
    • preferred:鼓励验证但非强制
    • discouraged:无需验证(最快体验)
    • 默认:preferred

advanced:高级选项

  • webAuthnChallengeCookie:在认证流程中存储 WebAuthn 挑战 ID 的 Cookie 名称(默认:better-auth-passkey

Expo 集成

使用 passkey 插件和 Expo 时,需要在 Expo 客户端配置 cookiePrefix 选项,以确保正确检测和存储 passkey cookies。

默认情况下,passkey 插件使用 "better-auth-passkey" 作为挑战 Cookie 名称。该名称以 "better-auth" 开头,因此适用于 Expo 客户端默认配置。但如果自定义了 webAuthnChallengeCookie,则必须同步更新 Expo 客户端配置中的 cookiePrefix

示例配置

如果您使用了自定义 Cookie 名称:

服务器端: auth.ts
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";

export const auth = betterAuth({
    plugins: [
        passkey({
            advanced: {
                webAuthnChallengeCookie: "my-app-passkey" // 自定义 Cookie 名称
            }
        })
    ]
});

确保在 Expo 客户端配置中匹配该前缀:

客户端: auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import { passkeyClient } from "@better-auth/passkey/client";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStore,
            cookiePrefix: "my-app" // 必须与自定义 Cookie 名称的前缀匹配
        }),
        passkeyClient()
    ]
});

如果使用多种认证系统或自定义多个 Cookie 名称,也可以传递前缀数组:

客户端: auth-client.ts
expoClient({
    storage: SecureStore,
    cookiePrefix: ["better-auth", "my-app", "custom-auth"]
})

如果 cookiePrefix 与您的 webAuthnChallengeCookie 的前缀不匹配,passkey 认证流程将会失败,因为挑战 Cookie 无法存储并在验证时发送回服务器。

欲了解更多 Expo 集成信息,请参见 Expo 文档