# Passkey (/docs/plugins/passkey)

Passkey



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

Passkey 插件的实现幕后由 [SimpleWebAuthn](https://simplewebauthn.dev/) 提供支持。

## 安装 [#安装]

<Steps>
  <Step>
    ### 安装插件 [#安装插件]

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install @better-auth/passkey
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @better-auth/passkey
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @better-auth/passkey
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @better-auth/passkey
        ```
      </CodeBlockTab>
    </CodeBlockTabs>
  </Step>

  <Step>
    ### 将插件添加到认证配置 [#将插件添加到认证配置]

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

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { passkey } from "@better-auth/passkey" // [!code highlight]

    export const auth = betterAuth({
        plugins: [ 
            passkey(), // [!code highlight]
        ],
    })
    ```
  </Step>

  <Step>
    ### 迁移数据库 [#迁移数据库]

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

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    参见 [架构](#schema) 部分以手动添加字段。
  </Step>

  <Step>
    ### 添加客户端插件 [#添加客户端插件]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client"
    import { passkeyClient } from "@better-auth/passkey/client" // [!code highlight]

    export const authClient = createAuthClient({
        plugins: [
            passkeyClient() // [!code highlight]
        ]
    })
    ```
  </Step>
</Steps>

## 配置（可选） [#配置可选]

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

```ts title="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 注册（预认证） [#passkey-first-注册预认证]

当 `registration.requireSession` 为 `false` 时，可以在没有会话的情况下发起 passkey 注册。您可以向注册选项端点传递一个不透明的 `context`；它会被转发给 `resolveUser`。

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

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

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

当 `createSession` 为 `true` 时，成功验证会设置会话 cookie，并返回与已注册 passkey 关联的 `session` 和 `user`。如果您想避免在 WebAuthn 流程成功之前创建用户，请在 `registration.afterVerification` 中创建或加载用户，并返回其 `userId`。

## 用法 [#用法]

### 添加/注册 passkey 密钥 [#添加注册-passkey-密钥]

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

**Endpoint:** `POST /passkey/add-passkey`

### Client Side

```ts
const { data, error } = await authClient.passkey.addPasskey({
    name: "example-passkey-name", // 可选的名称，用于标记正在注册的认证器账户。如果未提供，默认将使用用户的电子邮件地址或用户 ID
    authenticatorAttachment: "cross-platform", // 还可以指定要注册的认证器类型。默认行为允许注册平台密钥和跨平台密钥
    extensions, // 可选的 WebAuthn 扩展（例如 PRF、credProps、largeBlob）
    returnWebAuthnResponse, // 返回 WebAuthn 响应和扩展结果
    context, // 用于 passkey-first 注册流程的可选上下文。转发到 `registration.resolveUser`。
    createSession, // 成功注册 passkey 后创建会话。启用后，响应中会包含 session 和 user。
});
```

### Type Definition

```ts
type addPasskey = {
    /**
     * 可选的名称，用于标记正在注册的认证器账户。如果未提供，默认将使用用户的电子邮件地址或用户 ID
     */
    name?: string = "example-passkey-name"
    /**
     * 还可以指定要注册的认证器类型。默认行为允许注册平台密钥和跨平台密钥
     */
    authenticatorAttachment?: "platform" | "cross-platform" = "cross-platform"
    /**
     * 可选的 WebAuthn 扩展（例如 PRF、credProps、largeBlob）
     */
    extensions?: AuthenticationExtensionsClientInputs
    /**
     * 返回 WebAuthn 响应和扩展结果
     */
    returnWebAuthnResponse?: boolean
    /**
     * 用于 passkey-first 注册流程的可选上下文。转发到 `registration.resolveUser`。
     */
    context?: string
    /**
     * 成功注册 passkey 后创建会话。启用后，响应中会包含 session 和 user。
     */
    createSession?: boolean
}
```

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

### 使用 passkey 登录 [#使用-passkey-登录]

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

**Endpoint:** `POST /sign-in/passkey`

### Client Side

```ts
const { data, error } = await authClient.signIn.passkey({
    autoFill: true, // 浏览器自动填充，也称为条件 UI。阅读更多：https://simplewebauthn.dev/docs/packages/browser#浏览器自动填充-aka-条件-ui
    extensions, // 可选的 WebAuthn 扩展（例如 PRF、credProps、largeBlob）
    returnWebAuthnResponse, // 返回 WebAuthn 响应和扩展结果
});
```

### Type Definition

```ts
type signInPasskey = {
    /**
     * 浏览器自动填充，也称为条件 UI。阅读更多：https://simplewebauthn.dev/docs/packages/browser#浏览器自动填充-aka-条件-ui
     */
    autoFill?: boolean = true
    /**
     * 可选的 WebAuthn 扩展（例如 PRF、credProps、largeBlob）
     */
    extensions?: AuthenticationExtensionsClientInputs
    /**
     * 返回 WebAuthn 响应和扩展结果
     */
    returnWebAuthnResponse?: boolean
}
```

#### 示例用法 [#示例用法]

```ts
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`。

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

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

### 列出 passkey [#列出-passkey]

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

**Endpoint:** `GET /passkey/list-user-passkeys`

### Client Side

```ts
const { data: passkeys, error } = await authClient.passkey.listUserPasskeys();
```

### Server Side

```ts
const passkeys = await auth.api.listPasskeys({
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type listPasskeys = {
}
```

### 按认证器命名 passkey [#按认证器命名-passkey]

当用户注册 passkey 时如果未为其命名，存储的 `name` 会保持为空。您可以在 UI 中显示一个友好的默认名称，该名称可根据创建该凭证的认证器推导得出。

每一条 passkey 记录都带有一个 `aaguid`，即认证器*型号*的标识符（例如 Google Password Manager 或 1Password）。Better Auth 会在注册时存储它，并在 `listPasskeys` 中返回，因此您可以在渲染 passkey 时确定显示标签。由于解析发生在读取时，更新后的提供商列表也会应用于已存在的 passkey。

该插件为最常见的认证器附带了一个小型、尽力而为的查找表：

* [passkeydeveloper/passkey-authenticator-aaguids](https://github.com/passkeydeveloper/passkey-authenticator-aaguids)

```ts title="passkey-list.tsx"
import { getAuthenticatorName } from "@better-auth/passkey";

const passkeys = await authClient.passkey.listUserPasskeys();

for (const passkey of passkeys.data ?? []) {
    const label = passkey.name || getAuthenticatorName(passkey.aaguid) || "Passkey";
}
```

内置列表刻意保持较小，并不具有权威性。许多认证器都缺失，而且注重隐私的平台会报告一个全零的 AAGUID，从而无法解析出任何结果（Apple 设备在默认的 `attestation: "none"` 流程下就是这样）。您可以使用导出的 `commonAuthenticatorNames` 映射来扩展它，或者对照社区维护的数据源以获得完整覆盖：

* [passkeydeveloper/passkey-authenticator-aaguids](https://github.com/passkeydeveloper/passkey-authenticator-aaguids)

```ts title="passkey-list.tsx"
import { commonAuthenticatorNames } from "@better-auth/passkey";

const names = { ...commonAuthenticatorNames, "your-aaguid": "Your Provider" };
```

要在注册时于服务端设置默认标签，请在 `registration.afterVerification` 中返回一个 `name`。AAGUID 可通过 `verification.registrationInfo.aaguid` 获取，而且客户端提供的名称始终优先：

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { passkey, getAuthenticatorName } from "@better-auth/passkey";

export const auth = betterAuth({
    plugins: [
        passkey({
            registration: {
                afterVerification: async ({ verification }) => ({
                    name: getAuthenticatorName(verification.registrationInfo?.aaguid),
                }),
            },
        }),
    ],
});
```

### 删除 passkey [#删除-passkey]

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

**Endpoint:** `POST /passkey/delete-passkey`

### Client Side

```ts
const { data, error } = await authClient.passkey.deletePasskey({
    id: "some-passkey-id", // required, 要删除的 passkey 的 ID。
});
```

### Server Side

```ts
const data = await auth.api.deletePasskey({
    body: {
        id: "some-passkey-id", // required, 要删除的 passkey 的 ID。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type deletePasskey = {
    /**
     * 要删除的 passkey 的 ID。
     */
    id: string = "some-passkey-id"
}
```

### 更新 passkey 名称 [#更新-passkey-名称]

**Endpoint:** `POST /passkey/update-passkey`

### Client Side

```ts
const { data, error } = await authClient.passkey.updatePasskey({
    id: "id of passkey", // required, 要更新的 passkey 的 ID。
    name: "my-new-passkey-name", // required, 更新后的新名称。
});
```

### Server Side

```ts
const data = await auth.api.updatePasskey({
    body: {
        id: "id of passkey", // required, 要更新的 passkey 的 ID。
        name: "my-new-passkey-name", // required, 更新后的新名称。
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type updatePasskey = {
    /**
     * 要更新的 passkey 的 ID。
     */
    id: string = "id of passkey"
    /**
     * 更新后的新名称。
     */
    name: string = "my-new-passkey-name"
}
```

### 条件 UI [#条件-ui]

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

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

<Steps>
  <Step>
    #### 更新输入字段 [#更新输入字段]

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

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

    ```html
    <label for="name">用户名：</label>
    <input type="text" name="name" autocomplete="username webauthn">
    <label for="password">密码：</label>
    <input type="password" name="password" autocomplete="current-password webauthn">
    ```
  </Step>

  <Step>
    #### 预加载 passkey [#预加载-passkey]

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

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

    <Tabs items="[&#x22;React&#x22;]">
      <Tab value="React">
        ```ts
        useEffect(() => {
           if (!PublicKeyCredential.isConditionalMediationAvailable ||
               !PublicKeyCredential.isConditionalMediationAvailable()) {
             return;
           }

          void authClient.signIn.passkey({ autoFill: true })
        }, [])
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

表名：`passkey`。



<DatabaseTable name="passkey" fields="passkeyTableFields" />

## 选项 [#选项]

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

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

**origin**：您的 better-auth 服务器所在的源 URL，例如 `http://localhost` 或 `http://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 集成 [#expo-集成]

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

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

### 示例配置 [#示例配置]

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

```ts title="服务器端: 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 客户端配置中匹配该前缀：

```ts title="客户端: 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 名称，也可以传递前缀数组：

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

<Callout type="warn">
  如果 `cookiePrefix` 与您的 `webAuthnChallengeCookie` 的前缀不匹配，passkey 认证流程将会失败，因为挑战 Cookie 无法存储并在验证时发送回服务器。
</Callout>

欲了解更多 Expo 集成信息，请参见 [Expo 文档](/docs/integrations/expo)。

