# 升级到 Better Auth 1.7 (/docs/guides/1-7-upgrade-guide)

将 Better Auth 从 1.6 升级到 1.7，包括 Expo、OAuth、OpenID Connect、MCP、SAML、SCIM、代理和自定义适配器变更。



Better Auth 1.7 的大多数变更都是新增性的。大多数项目只需先执行一条命令：

<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 upgrade
    ```
  </CodeBlockTab>

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

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

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

将 `better-auth` 和每个 `@better-auth/*` 软件包一起升级，以便 CLI 和库都保持在 1.7。

某些领域需要更多注意：OAuth、OpenID Connect、SAML、SCIM、双因素认证、MCP、自定义存储和代理设置。请使用下表选择适用于你项目的章节。

| 如果你的项目                              | 阅读                                          |
| ----------------------------------- | ------------------------------------------- |
| 使用 Better Auth                      | [升级之前](#升级之前) 和 [代理之后](#代理之后)               |
| 使用邮箱和密码、社交登录、通用 OAuth、One Tap 或 SSO | [作为登录客户端](#作为登录客户端)                         |
| 使用 Expo 或 React Native              | [Expo 和 React Native](#expo-和-react-native) |
| 运行自己的 OAuth 或 OpenID 提供方            | [作为身份提供方](#作为身份提供方)                         |
| 运行 MCP                              | [MCP](#mcp)                                 |
| 使用 SAML 或 SSO 域验证                   | [企业级 SSO](#企业级-sso)                         |
| 使用 SCIM                             | [SCIM](#scim)                               |
| 使用 Stripe 计费                        | [Stripe](#stripe)                           |
| 使用双因素认证、魔法链接或邮箱 OTP                 | [双因素和无密码安全](#双因素和无密码安全)                     |
| 使用设备授权插件                            | [设备授权](#设备授权)                               |
| 使用自定义数据库适配器、辅助存储或速率限制存储             | [自定义适配器和存储](#自定义适配器和存储)                     |

## 升级之前 [#升级之前]

升级命令会处理软件包更新。数据库需要更加谨慎，因为某些 1.7 功能会更改表或要求手动执行数据步骤。`auth` CLI 要求 Node.js 22.12 或更高版本。

这些功能会更改 schema：

| 功能        | 添加内容                                                                           | 需要手动准备？                           |
| --------- | ------------------------------------------------------------------------------ | --------------------------------- |
| 受保护资源     | 新的资源表和密钥列                                                                      | 否                                 |
| 资源绑定令牌    | 令牌表中的资源列                                                                       | 否                                 |
| DPoP      | 令牌绑定列                                                                          | 否                                 |
| 刷新令牌重用窗口  | 刷新令牌上的缓存重放响应列                                                                  | 否                                 |
| 授权码重放     | 两个令牌表上带索引的 `authorizationCodeId` 列                                             | 否                                 |
| 后端通道注销    | 注销 URL 和已撤销列                                                                   | 否                                 |
| 请求的用户信息声明 | 令牌表和同意表上的请求声明列                                                                 | 否                                 |
| SCIM      | 七个预配模型，以及三个可选的托管目录模型；其布局会取代旧版 SCIM 模型                                          | **是，重新预配**                        |
| 组织团队计数器   | `team.memberCount` 和 `teamMember.membershipKey` 列                              | 否                                 |
| 提供方客户端存储  | `oauthApplication` 变为 `oauthClient`，并新增令牌表                                     | **是，迁移客户端数据**                     |
| 设备授权      | `deviceCode` 和 `userCode` 上的唯一索引；可选 OAuth 授权还会添加 `oauthClientId` 和 `resources` | **是，去重；MySQL/SQL Server 还需绑定字符串** |

<Callout type="warn">
  应用生成的 1.7 schema 之前，请先准备好 OAuth 客户端和 SCIM 数据。CLI 会添加表、列和索引，但不会复制 OAuth 客户端，也不会转换旧版 SCIM 数据。
</Callout>

请按以下顺序操作：

1. 检查是否存在[重复的账号键](#check-for-duplicate-account-keys)，并在适用时完成[OAuth 客户端记录](#migrate-oauth-client-records)、[SCIM](#scim-requires-full-reprovisioning) 和[设备授权](#device-authorization)中的手动准备工作。
2. 应用 1.7 schema。使用内置 Kysely 适配器时，运行 `npx auth migrate`。使用 Drizzle、Prisma 或自定义 schema 工作流时，运行 `npx auth generate`，检查生成结果，然后使用你的迁移工具应用。
3. 一起部署 1.7 软件包和配置变更。
4. 完成所有部署后工作，包括 SCIM 重新预配。

### 检查是否存在重复的账号键 [#检查是否存在重复的账号键]

升级之前，请检查是否存在重复的 `(providerId, accountId)` 对，包括从 1.6 升级的情况。在 PostgreSQL 中运行：

```sql
SELECT "providerId", "accountId", count(*) FROM account GROUP BY 1, 2 HAVING count(*) > 1;
```

请使用你配置的表名和列名。升级前解决查询返回的所有记录。如果不同发行者共享一个提供方 ID，并使用相同的账号 ID，请为每个发行者指定独立的提供方 ID，并更新对应的账号行。不同用户应保持独立。如果多个行与该键匹配，Better Auth 会拒绝账号查询。

***

## 作为登录客户端 [#作为登录客户端]

本节涵盖邮箱和密码、社交登录、通用 OAuth 插件、One Tap 以及使用 SSO。

### 账号身份保留提供方键 [#账号身份保留提供方键]

Better Auth 1.7.0 到 1.7.2 在 `account` 表中添加了必需的 `issuer` 列，以及 `issuer` 和 `accountId` 上的唯一索引。Better Auth 1.7.3 移除了此要求。与 1.6 一样，账号通过 `providerId` 和 `accountId` 识别。

**账号 schema 与 1.6 相同。** 无需添加任何列。

<Callout type="warn">
  只有运行过 1.7.0 到 1.7.2 的数据库才需要执行以下步骤。新行不再写入 `issuer`，因此在放宽约束之前，`NOT NULL` 列会拒绝所有注册和账号关联操作。
</Callout>

<Steps>
  <Step>
    #### 检查该列是否存在 [#检查该列是否存在]

    ```sql
    SELECT * FROM account LIMIT 0;
    ```

    如果结果中没有 `issuer` 列，则无需操作。`account` 表中已有数据的 1.6 数据库无法添加此列，因为没有默认值的 `NOT NULL` 列无法添加到已填充数据的表中。
  </Step>

  <Step>
    #### 放宽约束 [#放宽约束]

    删除约束即可，并且可以恢复。删除列是独立的清理操作，可在任何时候执行。

    <Callout type="warn">
      先删除索引，再删除列。MySQL 会保留列消失后仍引用该列的索引，并在其余列上重建索引，从而将复合唯一索引变成仅针对 `accountId` 的唯一约束。这会拒绝同一用户在两个提供方持有相同账号 ID 的情况。
    </Callout>

    <Tabs items="[&#x22;Postgres&#x22;, &#x22;MySQL and SQL Server&#x22;, &#x22;SQLite&#x22;, &#x22;Prisma and Drizzle&#x22;, &#x22;MongoDB&#x22;]">
      <Tab value="Postgres">
        ```sql
        ALTER TABLE account ALTER COLUMN issuer DROP NOT NULL;
        DROP INDEX account_issuer_accountId_uidx;
        ```
      </Tab>

      <Tab value="MySQL and SQL Server">
        ```sql
        ALTER TABLE account MODIFY issuer VARCHAR(255) NULL;
        DROP INDEX account_issuer_accountId_uidx ON account;
        ```

        在严格模式之外，MySQL 从不会拒绝此插入操作。它会将 `issuer` 填为空字符串，因此注册仍会正常进行，而唯一索引会悄悄变成仅针对 `accountId` 的约束。这里也需要放宽该列。
      </Tab>

      <Tab value="SQLite">
        SQLite 不支持 `ALTER COLUMN`，因此请改为删除该列。

        ```sql
        DROP INDEX account_issuer_accountId_uidx;
        ALTER TABLE account DROP COLUMN issuer;
        ```
      </Tab>

      <Tab value="Prisma and Drizzle">
        请重新生成，而不是编写 SQL。生成的 `account` 模型会同时删除该字段和复合唯一索引，并由你自己的工具生成迁移。

        ```bash
        npx auth@latest generate
        ```
      </Tab>

      <Tab value="MongoDB">
        MongoDB 没有 `NOT NULL`，因此插入操作仍会正常进行。如果存在唯一索引，请将其删除：由于不再写入 `issuer`，该索引会将所有新账号视为共享同一个发行者，并拒绝使用相同账号 ID 的两个提供方。此索引会在首次使用时延迟创建，因此已包含重复账号 ID 的集合从未创建过该索引。

        ```js
        db.account.dropIndex("account_issuer_accountId_uidx");
        ```
      </Tab>
    </Tabs>

    生成的索引名称为 `account_issuer_accountId_uidx`。如果你自己的迁移使用了其他名称，请相应调整；如果你在 1.7 中通过 `account.fields.issuer` 重命名了列，请使用自己的列名。
  </Step>

  <Step>
    #### 确认 [#确认]

    注册一个新用户，并使用现有账号登录。
  </Step>
</Steps>

在 1.7.0 到 1.7.2 中共享同一个账号发行者的提供方配置，不再会合并到同一个账号行中。与 1.6 一样，每个提供方 ID 都会重新保留自己的行。

账号专用 API 现在使用明确的强类型选择器。从 `listAccounts` 读取 `id`，然后将其作为 `accountId` 传递给 `unlinkAccount`。对于 `getAccessToken`、`refreshToken` 和 `accountInfo`，请从以下请求形状中选择一个：

* `{ accountId: account.id, userId? }` 选择本地账号行。
* `{ useAccountCookie: true, userId? }` 从签名 Cookie 中选择账号。

从每个账号选择器中删除 `providerId`。选择器的 `accountId` 值是本地的 `account.id`，而不是提供方一侧的 `account.accountId`。以前省略选择器以使用账号 Cookie 的令牌或提供方配置文件请求，现在必须发送 `useAccountCookie: true`；同时省略两个受支持的选择器是无效的。

检查每个自定义 OAuth 提供方的账号主体。OpenID Connect 发现型提供方现在使用经过验证的 `sub`；普通 OAuth 提供方使用 `id`；此前在运行时于这些字段之间进行的回退逻辑已移除。如果这两种默认值都不是提供方的不可变标识符，请设置 `accountSubject`。`getUserInfo().user` 现在只包含可变的本地用户字段；请将提供方标识符保留在 `getUserInfo().data` 中。`mapProfileToUser` 现在不能再返回 `id`。`accountInfo` 响应会通过 `account.accountId` 而不是 `user.id` 暴露所选身份。

#### 迁移 Microsoft 账号标识符 [#迁移-microsoft-账号标识符]

Microsoft 账号现在使用稳定的目录 `oid` 声明，而不是成对使用、应用专属的 `sub` 声明。在生产环境启用 1.7 流量之前，请完成此数据迁移：

1. 检查 `providerId: "microsoft"`（适用于内置提供方）和 `providerId: "microsoft-entra-id"`（适用于 Generic OAuth 辅助工具）的行。
2. 将每行旧的、基于 `sub` 的 `accountId` 替换为该目录用户经过验证的 `oid`。
3. 如果有存储的 Microsoft ID 令牌，请验证每个令牌并复制其 `oid` 声明。`mapProfileToUser` 无法覆盖此提供方管理的账号标识符。
4. 如果没有存储的 ID 令牌，请在切换期间暂停 Microsoft 登录和账号关联，并从可信的 Microsoft Entra 导出中获取映射。Better Auth 无法仅凭旧账号行推导出 `oid`；在迁移前接受流量可能会创建重复账号。

如果你使用带有自定义作用域或 `getToken` 的 Generic OAuth `microsoftEntraId` 辅助工具，请确保令牌交换仍然返回 Microsoft 的 `id_token`，并且发现机制提供验证它所需的发行者和 JWKS 元数据。该辅助工具还要求提供具体的租户 GUID。请将 `common`、`organizations` 或 `consumers` 配置替换为内置的 Microsoft 社交提供方，该提供方会验证租户声明并推导令牌的实际发行者。

### 通用 OAuth 已重建到社交提供方路径上 [#通用-oauth-已重建到社交提供方路径上]

通用 OAuth 插件现在的工作方式与内置社交提供方一致。

**你需要做的：**

* 将 `signIn.oauth2({ providerId })` 替换为 `signIn.social({ provider })`。
* 将 `oauth2.link()` 替换为 `linkSocial()`。
* 将提供方回调 URL 从 `/api/auth/oauth2/callback/:id` 更新为 `/api/auth/callback/:id`。
* 从客户端插件中删除 `genericOAuthClient()`，并使用标准社交客户端 API。
* PKCE 现在默认启用。仅对于拒绝 PKCE 的提供方设置 `pkce: false`。
* 删除 `issuer` 和 `requireIssuerValidation`；发行者验证现在会自动进行。
* `authorizationUrlParams` 和 `tokenUrlParams` 现在只接受普通字符串映射。

### 身份令牌现在通过一个验证器处理 [#身份令牌现在通过一个验证器处理]

以前每个提供方都会自行验证它自己的身份令牌。现在只有一个验证器，每个提供方都声明一个 `idToken` 配置：密钥、issuer 和 audience。

**你需要做的：** 自定义提供方应将其 `verifyIdToken` 方法替换为 `idToken` 配置。PayPal 不再接受身份令牌登录；它改用自己的访问令牌。请将 PayPal 的身份令牌登录切换为重定向流程。内置提供方选项保持不变。

### Electron 需要现代 PKCE [#electron-需要现代-pkce]

Electron 登录流程现在需要 S256 PKCE，不再信任自定义 origin 头，并且对自定义 URL scheme 的匹配更加安全。

**操作方法：** 同时升级 `@better-auth/electron` 客户端和服务器。确保你的应用 URL scheme 位于 `trustedOrigins` 中。删除旧的 `disableOriginOverride` 选项。检查带有主机的可信条目，例如 `myapp://callback`，因为它们不再匹配外观相似的主机。

### `generateState()` 签名已更改 [#generatestate-签名已更改]

公开的 `generateState()` 辅助函数现在接收一个选项对象，而不是位置参数。

**你需要做的：** 如果你直接调用 `generateState()`，请将 `generateState(c, link, additionalData)` 替换为选项形式 `generateState(c, options)`。

### OAuth 回调错误码已重命名 [#oauth-回调错误码已重命名]

OAuth 回调重定向错误值 `email_doesn't_match` 已重命名为 `email_does_not_match`。

**你需要做的：** 如果你从回调重定向中读取此错误码，请将字符串更新为 `email_does_not_match`。

### Google One Tap 需要客户端 ID [#google-one-tap-需要客户端-id]

在 `oneTap()` 上设置 `clientId`，或使用客户端 ID 配置 Google 社交提供方。One Tap 现在会根据经过验证的 Google subject 绑定账号，而不是通过邮箱匹配。

### 作用域会在登录之间保留 [#作用域会在登录之间保留]

已授予的作用域以前会彼此覆盖。先前授予的权限可能会在后续请求更少权限的登录之后消失。

Better Auth 现在会在重新登录和令牌刷新期间保留账号上已有的作用域，使用现有的 `account.scope` 字段。这是一个行为修复：没有架构变更、没有回填，大多数项目也无需操作。

### SIWE 从已签名消息推导身份 [#siwe-从已签名消息推导身份]

从 `authClient.siwe.nonce()` 和 `authClient.siwe.getNonce()` 调用中删除钱包地址和链字段。如果你配置了服务端 `getNonce`，请返回一个包含 8 至 250 个字母数字字符的 ERC-4361 nonce。Better Auth 会从已签名的 SIWE 消息中读取地址和链。

### 发现型提供方会验证它们的身份令牌 [#发现型提供方会验证它们的身份令牌]

现在，配置了发现 URL 的通用 OAuth 提供方会验证提供方身份令牌。任何验证失败的令牌都会被拒绝。

**你需要做的：** 如果某个发现型提供方返回了一个无法验证但你的应用仍然信任的令牌，那么现在该登录会被拒绝。请确认提供方发布的密钥、issuer 和 audience。

### 签名断言登录会在启动时验证 [#签名断言登录会在启动时验证]

签名断言登录设置，也称为 private-key JWT，现在会在创建时进行检查。不支持的算法、没有材料的密钥，或者声明的算法与密钥不匹配，都会立即失败，而不是悄悄产生错误结果。

**操作方法：** 修复任何声明算法与密钥不一致的签名设置。将 `createAuthorizationCodeRequest`、`createRefreshAccessTokenRequest` 和 `createClientCredentialsTokenRequest` 替换为异步的 `authorizationCodeRequest`、`refreshAccessTokenRequest` 和 `clientCredentialsTokenRequest`。

### 匿名账号链接可在移动端和应用内浏览器中使用 [#匿名账号链接可在移动端和应用内浏览器中使用]

在社交登录后链接匿名账号现在可在 Expo 和其他应用内浏览器中正常工作，因为回调返回时没有通常的 cookie。新的 `addOAuthServerContext` API 可在登录过程中传递客户端无法伪造的受信任数据。

**你需要做的：** 对于大多数应用无需操作。如果你以前是自己在 OAuth 重定向之间传递匿名链接状态，请将其迁移到 `addOAuthServerContext` 上。

***

## Expo 和 React Native [#expo-和-react-native]

### 安全存储访问是异步的 [#安全存储访问是异步的]

Expo 客户端现在使用异步 SecureStore API 访问 Cookie 和会话缓存。`authClient.getCookie()` 返回 promise，因此请直接 await，并将读取 Cookie 的回调设为异步。

```ts
const cookie = await authClient.getCookie();
```

直接传递 `expo-secure-store` 仍然可以正常工作，无需包装器。自定义存储实现必须提供 `getItem`、`getItemAsync`、`setItem` 和 `setItemAsync`。导出的 `storageAdapter.setItem()` 方法是同步的；当调用方必须等待持久化完成时，请使用 `setItemAsync()`。

***

## 作为身份提供方 [#作为身份提供方]

本节涵盖 `@better-auth/oauth-provider`。

### 迁移 OAuth 客户端记录 [#迁移-oauth-客户端记录]

如果你使用 1.6 内置的 `oidcProvider` 或 MCP 插件，请将每个 `oauthApplication` 复制或重新注册为 `oauthClient`。将 `redirectUrls` 映射为 `redirectUris`，将 `metadata` 转换为 JSON，并设置客户端的授权类型和令牌端点认证方法。让旧访问令牌过期，然后在创建 1.7 令牌表之前删除或重命名旧的 `oauthAccessToken` 表。CLI 不会复制这些记录，也不会将 `oauthAccessToken.accessToken` 重命名为 `token`。

如果你已经使用 `@better-auth/oauth-provider`，请按以下方式迁移现有的 `oauthClient` 行：

1. 增加可空的 `applicationType`、`clientDiscoveryId` 和 `clientCredentialsScopes` 列。除非你拥有客户端的可信发现来源，否则将 `clientDiscoveryId` 保留为 null。
2. 将现有的 `web` 和 `native` 客户端类型映射到 `applicationType`。单独检查 `user-agent-based` 客户端。仅对公共客户端将 `tokenEndpointAuthMethod` 设置为 `none`；其他所有方法都是机密的。
3. 将 `clientCredentialsScopes` 设置为空数组，然后为使用 `client_credentials` 授权的每个客户端分配获准的机器作用域。从提供方配置中删除 `clientCredentialGrantDefaultScopes`。
4. 在添加复合唯一索引之前，删除相同 `clientId` 和 `resourceId` 的重复 `oauthClientResource` 行。
5. 回填后删除已移除的 `type` 和 `public` 列。

将客户端配置和注册负载中的裸 JWK 数组替换为 JWK Set 对象：`jwks: [key]` 变为 `jwks: { keys: [key] }`。删除 `oauthProvider.silenceWarnings` 选项。

### 受保护资源替代受众列表 [#受保护资源替代受众列表]

受众现在是资源。每个资源都可以有自己的令牌生命周期、作用域、声明和签名密钥。旧的 `validAudiences` 列表已被移除。

**需要做什么：**

* 将 `validAudiences` 中的每个条目移动到 `resources`。
* 通过 `oauthClientResource` 或注册流程将客户端链接到特定资源。
* 检查刷新令牌生命周期：现在适用的最短生命周期优先，因此每资源设置的长于提供方默认值的生命周期会被限制为默认值。

**如果你接受动态注册的客户端，** 默认情况下资源模型会强制按客户端进行资源访问控制。动态客户端的令牌请求可能会被拒绝，并返回 `invalid_target`。在这种情况下，将 `enforcePerClientResources: false`，并显式注册每个客户端的 `grant_types`，否则令牌端点会以 `unauthorized_client` 拒绝它们。

### 注册资源和作用域使用提供方策略 [#注册资源和作用域使用提供方策略]

在启用动态客户端注册之前，配置 `clientRegistrationDefaultResources` 和 `clientRegistrationAllowedResources`。发送 `resources` 参数的现有客户端必须只请求这些选项允许的标识符。将 `clientRegistrationDefaultScopes` 和 `clientRegistrationAllowedScopes` 视为客户端可以请求的能力，而不是用户同意。

### 令牌目标会锁定到登录 [#令牌目标会锁定到登录]

该 API 令牌所针对的目标现在会在登录时被捕获，并锁定到该授权中。后续请求可以缩小目标 API 的范围，但不能扩大它。如果请求一个登录时未涵盖的 API，则会被拒绝。自定义声明回调现在接收的是资源列表，而不是单个值。

**操作方法：** 更新自定义声明回调以读取资源列表。确保客户端只请求其登录所涵盖的资源。

### DPoP 重命名令牌验证器 [#dpop-重命名令牌验证器]

普通的令牌检查辅助函数 `verifyAccessToken` 已重命名为 `verifyBearerToken`，现在会拒绝 DPoP 令牌。请在可能接收 DPoP 请求的端点上使用新的 `verifyAccessTokenRequest`。

**操作方法：** 将 `verifyAccessToken` 重命名为 `verifyBearerToken`，并将支持 DPoP 的端点切换为 `verifyAccessTokenRequest`。要支持 DPoP，请配置基于数据库的验证存储。

**注意代理情况。** 原生 DPoP 会将证明中的 `htu` claim 与令牌端点为自身计算的 URL 进行比对。在 TLS 终止代理或自定义服务器后面，这个计算出的 URL 可能是内部绑定地址（`http://0.0.0.0:3000`）或代理的内部协议和端口。客户端是根据你的公共发现 URL 签名的 `htu`，因此有效的证明也可能被拒绝。在提供者读取之前，请在路由边界处将传入请求的协议和主机规范化为你配置的 `baseURL`。

### 受保护资源作用域选项已重命名 [#受保护资源作用域选项已重命名]

将受保护操作的 `scopes` 重命名为 `requiredScopes`，并使用 `challengeScopes` 作为 `WWW-Authenticate` 提示。如果应用程序代码直接创建作用域失败，请使用 `createInsufficientScopeError`，并将已识别的令牌或作用域失败传递给 `createResourceServerChallenge`。

### 注销会撤销会话令牌 [#注销会撤销会话令牌]

会话结束时，与其关联的访问令牌现在会被撤销。在内省和 UserInfo 中，这些令牌会显示为非活动状态。以前，它们会一直存在到过期。你的服务器还会向注册了注销 URL 的每个应用发送注销消息。

**操作方法：** 预期会话绑定令牌在注销时停止工作。在无服务器平台上，设置 `advanced.backgroundTasks.handler`，这样发送注销消息就不会拖慢注销。

如果你导入或保留带有 `backchannel_logout_uri` 的客户端，请在切换前审查这些注册。后端注销需要 JWT 插件，并且每个 URI 都必须是绝对的公共 HTTPS URL，不能包含凭据或片段。私有、保留、隧道和云元数据目标都会被拒绝。

### RP 发起的注销可能需要浏览器确认 [#rp-发起的注销可能需要浏览器确认]

在 1.6 中，`/oauth2/end-session` 只接受 `GET` 请求，并拒绝没有 `id_token_hint` 的请求。在 1.7 中，该端点还接受表单编码的 `POST` 请求。对于没有有效提示的浏览器导航，Better Auth 会要求用户确认，然后才结束当前会话。当提示指向与浏览器会话不同的会话时，也需要进行相同的浏览器确认。需要确认时，API 调用会收到协议错误。

确认流程保留现有的重定向规则：`post_logout_redirect_uri` 必须与已注册 URI 完全匹配。Better Auth 只会将 `state` 添加到经过验证的重定向中。

**操作方法：** 如果你的浏览器流程预期缺失或无效的提示会立即失败，请更新流程和测试，以处理确认页面。发送有效提示的标准客户端无需更改。仅对可信客户端启用 `enable_end_session`，并准确注册每个允许的注销后重定向 URI。

### 自定义 ID 令牌声明不能覆盖协议声明 [#自定义-id-令牌声明不能覆盖协议声明]

你的自定义 ID 令牌声明现在不能再设置标准为服务器保留的协议声明：发行者、主题、受众、过期时间、nonce、会话绑定、`auth_time`、`acr`、`amr` 和 `azp`。你自己的命名空间声明仍然会出现。ID 令牌也会报告 `acr: "0"`，而不是某个供应商特定的值。

**操作方法：** 如果 `customIdTokenClaims` 回调、扩展声明贡献器或每次签发的 `idTokenClaims` 设置了这些保留声明之一，该值现在会被忽略。请将数据移到你自己的命名空间声明中，或依赖服务器的值。

### ID 令牌不再包含 profile 和 email scope 声明 [#id-令牌不再包含-profile-和-email-scope-声明]

通过授权码流程签发的 ID 令牌不再携带 profile 和 email scope 声明。这些声明可从 UserInfo 端点获取。

**该怎么做：** 请改为从 UserInfo 读取 profile 和 email 声明，而不是从 ID 令牌中读取。

### `jwt.sign` 回调必须与配置的 alg 匹配 [#jwtsign-回调必须与配置的-alg-匹配]

当自定义 `jwt.sign` 回调的算法与 ID 令牌签发期间的 `keyPairConfig.alg` 不同时，该回调会被拒绝。

**要做什么：** 将你的自定义签名算法与 `keyPairConfig.alg` 保持一致。

### `/oauth2/revoke` 拒绝有效的 JWT 访问令牌 [#oauth2revoke-拒绝有效的-jwt-访问令牌]

撤销一个仍然有效的 JWT 访问令牌现在会返回 `400 unsupported_token_type`。

**该怎么做：** 撤销 refresh token 或不透明访问令牌；不要对 JWT 访问令牌调用 revoke。

### `max_age` 已强制执行 [#max_age-已强制执行]

当客户端请求 `max_age`，而用户的登录时间早于该值时，提供方会将他们重定向回登录。之前，这个请求会被忽略。

**需要做什么：** 无需配置任何内容。如果某个客户端发送了 `max_age` 并期望它被忽略，现在则应预期会出现重新登录提示。

**注意你的日期列。** `max_age` 检查会将会话的创建时间按日期读回。如果你的自定义模式将会话时间戳存储为文本，而不是实际的日期或整数时间戳类型，该检查可能会误读该值，并将用户送入登录循环。请将 `user`、`session`、`account` 和 `verification` 的时间戳存储为日期或时间戳类型。Better Auth CLI 会生成正确的类型。

### 客户端创建返回 201 [#客户端创建返回-201]

现在创建客户端时返回的是 `201 Created`，而不是 `200 OK`，并且注册端点会强制执行与手动创建端点相同的权限检查。

**要做什么：** 更新任何期望从客户端创建返回 `200` 的客户端，使其接受 `201`。若要允许机器客户端注册，请配置 `validateInitialAccessToken`。

### 未认证注册会保留客户端的认证方式 [#未认证注册会保留客户端的认证方式]

在没有登录用户的情况下进行的动态客户端注册，不再会强制将客户端设为公共客户端。现在，省略 `token_endpoint_auth_method` 的客户端将被视为机密客户端，使用 RFC 7591 默认的 `client_secret_basic` 和生成的密钥；只有当它注册 `token_endpoint_auth_method: "none"` 时，才会成为公共客户端。

**该怎么做：** 如果你依赖于未认证注册会降级为公共客户端，那么对于必须保持为公共客户端的客户端，请显式注册 `token_endpoint_auth_method: "none"`。

### 令牌请求使用已注册的客户端认证方法 [#令牌请求使用已注册的客户端认证方法]

OAuth Provider 现在会拒绝通过不同于客户端 `token_endpoint_auth_method` 的方法发送的机密客户端凭据。请求正文认证失败时返回 `400 invalid_client`；Basic 认证失败时返回带有 Basic challenge 的 `401 invalid_client`。

**操作方法：** 使每个客户端的令牌请求与其注册的方法匹配。Better Auth 的通用 OAuth 请求辅助工具在提供客户端密钥时默认使用正文认证，因此对于使用 RFC 7591 默认方法注册的客户端，请在调用时传递 `authentication: "basic"` 或 `tokenEndpointAuth: { method: "client_secret_basic" }`。只有确实需要正文认证时，才显式注册 `client_secret_post`。

### 注册要求响应类型和授权类型互相对应 [#注册要求响应类型和授权类型互相对应]

已注册客户端的 `response_types` 和 `grant_types` 现在必须互为对应：`code` 响应类型需要 `authorization_code` 授权类型，而 token 授权类型需要其匹配的响应类型。不匹配的注册将被拒绝。

**要做什么：** 为每个客户端注册相匹配的 `response_types` 和 `grant_types`。

### OAuth 端点返回标准错误信封 [#oauth-端点返回标准错误信封]

OAuth 端点（token、authorize、revoke、introspect、register、end-session）上的验证失败和格式错误请求失败现在返回 RFC 6749 的 `{ error, error_description }` 信封，而不是之前通用的验证错误形状。

**要做什么：** 如果客户端或工具解析了旧的错误形状，请更新为读取 `error` 和 `error_description`。

### 内省返回一致的声明 [#内省返回一致的声明]

`/oauth2/introspect` 现在对不透明令牌返回的声明与对 JWT 返回的声明相同，并且资源服务器可以内省发给不同客户端的令牌。

**要做什么：** 无需操作。对于不透明令牌，预期会获得更丰富且一致的内省响应。

### UserInfo 接受表单正文中的 bearer token [#userinfo-接受表单正文中的-bearer-token]

userinfo 端点现在接受在表单编码正文中的访问令牌，并会拒绝同时在请求头和正文中发送令牌的请求。

**该怎么做：** 将访问令牌只放在一个位置发送，`Authorization` 请求头或表单正文，二者选其一，不要同时发送。

### 客户端认证与授权类型绑定 [#客户端认证与授权类型绑定]

通过扩展接口注册的自定义客户端认证方法，现在只能证明是哪个客户端在调用。由服务器决定该客户端被允许执行什么操作。

**要做什么：** 添加了客户端认证方法的配套插件应依赖服务器解析出的客户端，而不是返回它们自己的客户端判断。

### 服务器端 OAuth 请求拒绝重定向 [#服务器端-oauth-请求拒绝重定向]

Better Auth 现在会拒绝其发出的服务器端 OAuth 请求中的 HTTP 重定向：令牌交换、令牌刷新、客户端凭证、令牌自省以及 JWKS 请求。符合规范的 OAuth 提供商会直接响应这些端点，而不会进行重定向。

**需要做什么：** 对于标准提供商，无需任何操作。如果自定义提供商端点会重定向，请让它直接返回最终响应。

### PKCE 对机密客户端和 OIDC 客户端的要求 [#pkce-对机密客户端和-oidc-客户端的要求]

公共客户端始终需要 PKCE。通过动态客户端注册注册的机密客户端可以使用 `clientRegistrationRequirePKCE: false` 选择退出。携带 `offline_access` 作用域的请求仍然需要 PKCE，除非该请求是带有 `nonce` 的 OIDC 请求，机密客户端可以改用这种方式。

**操作方法：** 仅对无法使用 PKCE 的机密客户端设置 `clientRegistrationRequirePKCE: false`。如果机密 OIDC 客户端需要在没有 PKCE 的情况下使用 `offline_access`，请发送 `nonce`。

### 授权接受表单编码请求并拒绝请求对象 [#授权接受表单编码请求并拒绝请求对象]

授权和用户信息端点现在接受表单编码（POST）请求，而授权端点会明确拒绝其不支持的 OIDC `request` 和 `request_uri` 参数。

**要做什么：** 标准客户端无需采取任何操作。

### 刷新令牌重试可以被容忍 [#刷新令牌重试可以被容忍]

在 `refreshTokenReuseInterval` 期间，OAuth 提供方可以对重复的刷新请求重放相同的刷新响应。严格的刷新令牌重放处理仍然是默认行为。

**操作方法：** 仅当客户端可能在另一个本地会话已经轮换旧令牌后，使用旧令牌重试刷新请求时，才设置 `refreshTokenReuseInterval`。OAuth Provider 将严格重放处理保持在 `0`；`mcp()` 对每个客户端默认将该间隔设置为 30 秒。

### 如果你曾手动扩展过 OAuth provider [#如果你曾手动扩展过-oauth-provider]

如果你通过打补丁或 fork OAuth provider 来添加自定义 grant、claim 或客户端认证方法，请改用受支持的扩展接口，而不是重新应用补丁。请在插件的 `init(ctx)` 钩子中使用 `extendOAuthProvider(ctx, ...)` 注册你的贡献。使用 `provider.issueTokens(...)` 签发 token，使用 `provider.authenticateClient(...)` 验证客户端，使用 `provider.hashToken(...)` 对 token 进行哈希。通过向 `issueTokens` 传递 `resources` 来绑定 token 的受众；受众由服务器拥有。

以较旧或手工拼装的形式编写的贡献在这里可能会静默失败。它可能通过类型检查并正常运行，但 grant 或 claim 永远不会进入 token。将每一项迁移到 `init()` 之后，请确认该 grant 或 claim 能真正进入一个真实的 token。

### 旧的 `oidcProvider` 插件已被移除 [#旧的-oidcprovider-插件已被移除]

**操作方法：** 将 `better-auth/plugins` 中的 `oidcProvider` 替换为 `@better-auth/oauth-provider` 中的 `oauthProvider`，并迁移配置。

***

## MCP [#mcp]

### MCP 迁移到独立包 [#mcp-迁移到独立包]

MCP 插件从 `better-auth` 移至 `@better-auth/mcp`，并使用 `@better-auth/oauth-provider`。

**你需要做什么：**

1. 安装 `@better-auth/mcp`、`@better-auth/cimd` 以及应用所需的官方版本 2 MCP 客户端或服务器软件包。从 `@better-auth/mcp` 导入 MCP 授权和受保护请求辅助工具。将已移除的内置客户端和适配器替换为 `@modelcontextprotocol/client` 或 `@modelcontextprotocol/server`。
2. 添加必需的 `jwt()` 插件。使用 `cimd({ fetchClientMetadataResource, metadataProfile: "mcp-2026-07-28" })` 组合 `mcp()` 以进行客户端注册。
3. 将选项从 `oidcConfig` 移动到 `mcp({ ... })` 的顶层。设置一个规范的 HTTPS `resource`，例如 `https://api.example.com/mcp`，并用该值替换 `resourceMetadataMappings`。
4. 将 `withMcpAuth` 重命名为 `requireMcpAuth`，将 `mcpHandler` 重命名为 `createMcpProtectedRequestHandler`。应用[受保护资源作用域选项变更](#受保护资源作用域选项已重命名)。
5. 使用带有 `legacy: "reject"` 的版本 2 `createMcpHandler`，用 `requireMcpAuth` 包装它，并且只公开 `POST`。删除 MCP 路由的 `GET` 和 `DELETE` 导出，以及 `redisUrl` 等会话存储选项。
6. 通过[迁移 OAuth 客户端记录](#迁移-oauth-客户端记录)迁移已注册的客户端。

OAuth 端点从 `/mcp/*` 移至 `/oauth2/*`。基于发现机制的客户端会自动找到新端点。MCP 刷新令牌重用间隔现在对每个客户端默认为 30 秒；在 `mcp()` 上设置 `refreshTokenReuseInterval: 0` 以要求严格的重放处理。

`mcp()` 不再启用未经身份验证的动态客户端注册。如果你确实支持它，请同时设置 `allowDynamicClientRegistration` 和 `allowUnauthenticatedClientRegistration`。否则，请使用上面的 CIMD 配置。

***

## 企业级 SSO [#企业级-sso]

### SSO 账号 subject 由协议定义 [#sso-账号-subject-由协议定义]

OIDC SSO 现在使用经过验证的 `sub` 声明作为账号 subject，SAML 使用已签名的 `NameID`。配置文件映射仍然可以选择邮箱、姓名、图片和其他字段，但 `oidcConfig.mapping.id` 和 `samlConfig.mapping.id` 已被移除。不提供 `idpMetadata.metadata` 的手动 SAML 配置必须设置 `idpMetadata.entityID`；`samlConfig.issuer` 标识服务提供方，不再作为 IdP 回退值。

**要做什么：** 从每个 OIDC 和 SAML 映射中移除 `id`，然后确认每个 OIDC 提供方都返回稳定的 `sub`，每个 SAML 提供方都返回稳定且已签名的 `NameID`。

如果之前的映射使用了 `mapping.id`，请在维护窗口前准备替代方案。为每个旧账号 subject 与协议定义的身份建立可信映射，然后重写这些账号行：OIDC 需要经过验证的 `sub`；SAML 需要已签名的 `NameID`。请从身份提供方获取此映射，而不是依据邮箱或可变的配置文件属性。不要为旧映射部署运行时回退方案。

### IdP 发起的 SAML 默认关闭 [#idp-发起的-saml-默认关闭]

身份提供方发起的未请求登录现在默认被禁用。登录响应会针对 `InResponseTo` 进行校验，因此不能重放到它未发起请求的登录上，而单点注销请求会通过 `SessionIndex` 匹配到对应会话。

**该怎么做：** 如果你依赖旧行为，请设置 `saml.allowIdpInitiated: true` 来恢复它。

### SAML 证书可以是一个列表 [#saml-证书可以是一个列表]

签名证书现在既可以接受单个值，也可以接受列表，这使你能够在不停机的情况下轮换证书。管理端点会将证书作为列表返回，或者当证书位于 `idpMetadata` 文档中时省略它。

**该怎么做：** 更新任何从这些端点读取证书的代码，使其预期接收列表或不存在的情况。确保每个 SAML 配置都提供一个签名证书来源，即明确的证书或 `idpMetadata` 文档，否则注册会失败。

### SAML 配置已简化 [#saml-配置已简化]

回调 URL 会自动派生，服务提供商元数据会自动为你生成，并且移除了若干字段。一个端点路径发生了变化，重定向中的错误码也从简短别名改为全小写的内部完整代码（例如 `saml_multiple_assertions`）。

**操作方法：** 从配置中删除空的 `spMetadata` 和已移除的字段。将 ACS URL 作为基础 URL 加上 `/sso/saml2/sp/acs/:providerId` 注册到 IdP；使用默认基础路径时，该 URL 为 `https://yourapp.com/api/auth/sso/saml2/sp/acs/:providerId`。对于 SP 发起的登录，请在 `signIn.sso()` 中使用 `callbackURL` 设置登录后的重定向。`callbackUrl` 配置字段不再是 ACS URL，现在是可选的，但对于 IdP 发起的登录，它仍然是登录后的重定向。如果启用了 `allowIdpInitiated` 且需要指定落地页面，请保留它。如果你从重定向 URL 中读取 SAML 错误码，请切换为小写代码。

### SAML 签名强制规则与其配置一致 [#saml-签名强制规则与其配置一致]

在 1.6 中，`wantAssertionsSigned` 强制要求 SAML 响应消息签名，而不是断言签名。1.7 会验证断言元素本身，并对其应用配置的要求，因此当服务提供方要求断言签名时，仅签署响应消息的 IdP 登录会失败。携带 RelayState 的回调现在会无条件验证它，并且提供方注册会拒绝弱化已签名断言策略、超过元数据大小限制或声明包含 URL 片段的 ACS 位置的服务提供方元数据。

**操作方法：** 确认每个 IdP 都会签署断言，或者对于无法签署断言的 IdP，明确设置 `wantAssertionsSigned: false`。修复或重新注册不再通过验证的已存储 SP 元数据。如果自定义集成在 SAML 回调中提供自己的 RelayState，请确保它能够往返传递 Better Auth 签发的值。

### `/sso/update-provider` 会拒绝部分映射 [#ssoupdate-provider-会拒绝部分映射]

更新 SSO provider 现在会拒绝不完整的 OIDC 或 SAML 映射对象。

**该怎么做：** 在调用 `/sso/update-provider` 时发送完整的映射对象。

### OIDC SSO 可在 Cloudflare Workers 上运行 [#oidc-sso-可在-cloudflare-workers-上运行]

带发现机制的 OIDC SSO 现在可在 Cloudflare Workers 上运行。会重定向的 discovery 或 token 端点会被拒绝，并给出清晰的配置错误，而不是以运行时特定方式失败。

**该怎么做：** 不需要做任何事。如果某个 provider 端点会重定向，请将配置指向最终 URL。

***

## SCIM [#scim]

### SCIM 支持三种连接模式 [#scim-支持三种连接模式]

每个 SCIM 请求都会解析不可变的连接 ID、凭据身份、作用域和配置域。该插件不再依赖 Organization 或 SSO 插件，应用程序可以选择三种连接模式之一：

* **静态代码定义：** 在 `scim({ connections })` 中声明连接和 bearer 凭据。
* **应用程序拥有的运行时模式：** 验证 bearer token，并从 `authentication.verifyBearerToken` 原子地返回其连接。
* **插件管理的运行时模式：** 配置 `managedConnections`，然后从经过应用程序授权的管理员工作流中调用其可信服务器 API。

旧版运行时连接管理端点、SCIM 客户端插件、CLI 脚手架、`defaultSCIM`、`staticProviders`、`trustedDomains`、`providerOwnership` 以及组织作用域的提供方配置均已移除。配置的身份不再创建身份验证账号。使用 `identity` 回调链接现有的 Better Auth Users 并应用生命周期状态，使用 `projection` 回调将 Groups 映射到应用程序的角色。

**操作方法：** 选择一种受支持的连接模式，为接收生命周期和访问变更的应用程序边界分配稳定的 `provisioningDomainId`，并为配置的 Users 配置单独的登录方式。以下示例使用静态模式：

```ts title="auth.ts"
scim({
  connections: [
    {
      id: "workforce-acme",
      provisioningDomainId: "workspace-acme",
      credentials: [
        { type: "bearer", id: "workforce-primary", token: workforceToken },
      ],
    },
  ],
});
```

### SCIM 需要完整重新配置 [#scim-需要完整重新配置]

新的 SCIM 模型不会读取或转换 1.6 SCIM 表，因此此变更无法使用原地生成的迁移。

<Callout type="warn">
  请使用维护窗口。在更改 schema 之前，暂停配置，并停止每个运行旧版 SCIM 插件的应用程序实例。
</Callout>

**操作方法：**

1. 备份每一张旧版 SCIM 表。建立一份经过审核的清单，准确记录 `scimProvider` 行、由 SCIM 创建的 `account` 行、其关联的 `user` 行，以及由配置过程创建的任何组织成员关系或团队状态。使用已配置的提供方和目录主体来识别它们。不要仅根据 `providerId` 前缀对行进行分类。
2. 决定如何处理每个旧版 User 和 account 行。要保留某个 User，请将稳定的 connection-and-subject-to-`userId` 映射复制到应用自有存储中，仅删除已确认的旧版 SCIM account 行，并在重新配置之前配置 `identity.resolveUser`。要重新创建某个 User，请删除已确认的 SCIM account，并且只有在证明没有其他登录方式或应用数据依赖它之后，才能删除该 User。保留所有无关的 account 和应用行；Better Auth 不会自动识别或删除旧版 SCIM account 行。
3. 选择静态、应用自有运行时或插件管理的 connection 模式，并准备新的不透明 connection 和 credential 标识符。绝不要导入旧版 token 哈希、重复使用旧版原始 token，或添加接受旧版 bearer 语法的兼容路径。静态模式和应用自有模式使用新的高熵 secret；管理模式会在第 5 步创建管理表之后，于第 6 步生成新的 secret。
4. 在旧插件停止运行期间，通过旧版 SCIM 端点或由操作员控制的事务，按依赖顺序清理其 SCIM 自有资源。确认旧版插件表为空，并且清单中的每个 SCIM account 行都已有经过审核的处置结果。只有在完成审核备份和清理之后，才能删除或重命名不兼容的旧版物理表，包括 `scimProvider`。如果你自定义了模型名称，请使用架构中的物理表名。
5. 在数据库适配器中启用原生交互式事务，然后通过[适配器特定的工作流](#before-you-upgrade)应用 1.7 架构，以创建 `scimConnectionBinding`、`scimIdentityTombstone`、`scimSubject`、`scimUser`、`scimGroup`、`scimGroupMember` 和 `scimProjectionGrant`。管理模式还会创建 `scimManagedConnection`、`scimManagedCredential` 和 `scimManagedConnectionEvent`。Cloudflare D1 无法提供所需的事务行为。
6. 部署选定的模式。对于管理模式，请在新架构存在之后，通过受信任的服务器 API 创建 connection 并生成其第一个 credential。使用新 credential 配置目录，然后触发完整的 User 和 Group 配置周期。
7. 在应用中验证 User 关联、Group 状态、生命周期和角色投影，然后恢复配置。

<Callout type="warn">
  新插件绝不会通过 email 进行关联。保留的旧版 User 在重新配置期间会导致 `409 Conflict`，除非 `identity.resolveUser` 为其返回明确的关联决策。
</Callout>

请参阅 [SCIM 插件文档](/docs/plugins/scim)，了解最终配置和受支持的协议行为。

***

## Stripe [#stripe]

### 组织订阅需要 `organization.enabled` [#组织订阅需要-organizationenabled]

`referenceMiddleware` 现在会拒绝组织范围内的订阅，除非在 Stripe 插件配置中设置了 `organization: { enabled: true }`。

**该怎么做：** 对于组织范围内的订阅，请在你的 `stripe()` 插件选项中设置 `organization: { enabled: true }`。组织插件仍然需要单独启用，以解析当前活跃组织。

### `onSubscriptionCancel` 事件是必需的 [#onsubscriptioncancel-事件是必需的]

`onSubscriptionCancel` 回调中的 `event` 参数现在是必需的。

**该怎么做：** 更新你的 `onSubscriptionCancel` 回调，使其接受一个非可选的 `event`。

***

## 代理后面 [#代理后面]

### 动态基础 URL 默认不信任转发标头 [#动态基础-url-默认不信任转发标头]

当 `baseURL` 使用 `allowedHosts` 时，除非主动选择启用，否则 Better Auth 会忽略转发的标头。

**该怎么做：** 如果你的代理仅通过 `x-forwarded-host` 暴露公共主机名，请选择启用：

```ts
betterAuth({
  baseURL: { allowedHosts: [...] },
  advanced: {
    trustedProxyHeaders: true, // [!code highlight]
  },
});
```

代理会为你重写主机的配置（例如 nginx、Vercel、Cloudflare 和 Netlify）无需更改。

如果没有配置 `baseURL.fallback`，当请求没有可用主机，或其主机与 `allowedHosts` 不匹配时，动态 base URL 会默认拒绝。因此，直接的服务器 API 调用必须包含能够解析到允许的公共源的请求 URL 或标头数据，或者配置受信任的回退值。Better Auth 会在每个请求中根据解析出的源重建 base URL、受信任源、提供方 URL 和 cookies；OAuth issuer、discovery、protected-resource 和 JWKS URL 也会遵循相同的请求源。

### IdP 重定向和 DPoP 需要你的规范源 [#idp-重定向和-dpop-需要你的规范源]

有两种 OAuth 提供方行为会读取传入请求的源。在自定义服务器或终止 TLS 的代理后面，这个源可能是内部绑定地址，而不是你的公网源。提供方会将你的 `consentPage` 和 `loginPage` 作为相对路径返回，因此服务器端重定向（例如 `NextResponse.redirect`）需要把它们相对于一个绝对源来解析。原生 DPoP 还会把证明中的 `htu` 与令牌端点为自身计算出的 URL 进行比较。

**怎么做：** 将 `baseURL` 视为服务器身份，并在路由边界、供提供方读取请求之前，把传入请求的协议和主机规范化为它。只要在任何路由转发到提供方的地方应用一个辅助函数，就能同时修复 consent 重定向、DPoP 的 `htu` 检查，以及源检查。

## 自定义适配器和存储 [#自定义适配器和存储]

原子状态相关工作引入了必需方法。如果你只使用内置适配器和存储，可以跳过这一部分。

### 数据库适配器必须实现 `incrementOne` 和 `consumeOne` [#数据库适配器必须实现-incrementone-和-consumeone]

`incrementOne` 会以原子方式更新一行的计数器并返回该行；如果条件未匹配，则返回 null。`consumeOne` 会一次性读取并删除一行，适用于一次性凭证。这两个方法现在都是必需的，旧的回退逻辑已经移除。

**要做什么：** 在任何自定义适配器中实现 `incrementOne` 和 `consumeOne`。缺少 `consumeOne` 会在运行时抛出错误。所有内置适配器都已经实现。

### 二级存储必须实现 `increment` 和 `getAndDelete` [#二级存储必须实现-increment-和-getanddelete]

`increment(key, ttl)` 会将计数器加一，并且只在键首次创建时设置过期时间。`getAndDelete(key)` 会一次性读取并移除一个键。这两个方法之前是可选的，现在是必需的。

**要做什么：** 在自定义二级存储中实现 `increment` 和 `getAndDelete`。Redis 存储已经实现。

### 限流存储使用 `consume` [#限流存储使用-consume]

限流存储现在需要一个 `consume(key, rule)` 方法，在一次操作中完成检查和递增。独立的 `get` 和 `set` 不再被接受。

**要做什么：** 用 `consume` 替换自定义限流存储中的 `get` 和 `set`。

### Drizzle 模式在 `usePlural` 下使用单数关系键 [#drizzle-模式在-useplural-下使用单数关系键]

Drizzle 模式生成器现在会为多对一关系输出单数键。这只会影响配置了 `usePlural: true` 的项目；默认的单数配置不受影响，反向的“多”关系键也不会改变。

**要做什么：** 如果你设置了 `usePlural: true`，请重新生成你的 Drizzle 模式并检查关系键。

### `getIp` 已重命名为 `getIP` [#getip-已重命名为-getip]

公开导出的 `getIp` 现已重命名为 `getIP`。

**要做什么：** 将 IP 辅助函数的导入更新为 `getIP`。

***

## 验证码 [#验证码]

原子状态相关工作会改变自定义适配器和存储的契约。如果你只使用内置适配器和存储，可以跳过这一部分。

### 数据库适配器原子方法再次变为可选 [#数据库适配器原子方法再次变为可选]

1.7.0 到 1.7.2 版本要求自定义数据库适配器实现 `incrementOne` 和 `consumeOne`。现在它们再次变为可选。内置适配器仍会使用其原生实现。

**该怎么做：** 确保你的适配器提供原子条件写入和准确的受影响行数。工厂回退逻辑会在写入前检查存储值，并拒绝不安全的快照或已耗尽的重试。完整的回退要求请参阅[适配器指南](/docs/guides/create-a-db-adapter#consumeone-方法)。

### 双因素 `enableTwoFactor` 返回区分响应 [#双因素-enabletwofactor-返回区分响应]

`enableTwoFactor` 现在接受 `"otp"` 或 `"totp"`（默认 `"totp"`）的 `method`，并在响应中返回该方法。`totpURI` 和备用代码仅在 `"totp"` 时提供。`skipVerificationOnEnable` 选项仍然有效。

**该怎么做：** 更新读取 `totpURI` 或备用代码的调用方，根据返回的 `method` 分支处理。

### 魔法链接和电子邮件 OTP 登录可以清除未验证的关联账户 [#魔法链接和电子邮件-otp-登录可以清除未验证的关联账户]

魔法链接和电子邮件 OTP 登录会将已验证的邮箱控制权视为某个从未确认过邮箱的账户的事实来源。清除未验证的密码并撤销会话之前就已经发生；1.7 扩展了清理范围，在用户登录之前移除与该未确认身份关联的每个账户，包括 OAuth 和社交登录关联。

**该怎么做：** 如果某个用户注册了但从未确认邮箱，然后首次通过魔法链接或电子邮件 OTP 登录，预期其之前的密码以及任何社交关联都会消失。请让他们通过密码重置设置新密码，并重新关联任何社交账户。

### 数据库连接已移出 `experimental` [#数据库连接已移出-experimental]

将 `experimental: { joins: true }` 替换为 `advanced: { database: { joins: true } }`。如果启用连接，请重新生成 Drizzle 或 Prisma 关系。

### `usePlural` 下 Drizzle 关系键为单数 [#useplural-下-drizzle-关系键为单数]

当 `usePlural: true` 时，Drizzle 模式生成器现在会为多对一关系使用单数键。重新生成模式，并更新读取之前复数关系键的代码。默认的 `usePlural: false` 配置不变。

### `getIp` 已重命名为 `getIP` [#getip-已重命名为-getip-1]

公开导出的 `getIp` 已重命名为 `getIP`。

**要做什么：** 将 IP 辅助函数的导入更新为 `getIP`。

***

## 升级检查清单 [#升级检查清单]

某些手动步骤必须在 `migrate` 之前运行，而不是之后。请按以下顺序执行：

验证码规则现在会匹配完整的请求路径或显式通配符，这样就堵住了通过部分路径匹配来跳过验证码规则的一种方式。

**要做什么：** 将像 `/sign-in` 这样的部分路径替换为 `/sign-in/*` 或 `/sign-in/**`。

像 `/sign-in/*` 这样的通配符也会匹配 `/sign-in/email-otp`。如果该路由应继续免于验证，请列出确切的受保护端点，而不是使用通配符。

***

## 设备授权 [#设备授权]

### 设备代码使用唯一索引和有界值 [#设备代码使用唯一索引和有界值]

稳定版 1.6 模式不会对这些查找值强制执行唯一性。1.7 模式会在 `deviceCode` 和 `userCode` 上创建唯一索引，因此请在应用迁移之前，在每个适配器中解决两列中的重复值。MySQL 和 SQL Server 安装还必须将两列转换为有界字符串，并清理长度超过 191 个字符的值。自定义的 `generateDeviceCode` 和 `generateUserCode` 函数必须保持在 191 个字符的限制以内。

### OAuth 设备授权默认不启用 [#oauth-设备授权默认不启用]

在 1.6 中，`deviceAuthorization()` 会将设备登录到同一个 Better Auth 应用，并从 `/device/token` 返回 Better Auth 会话令牌。这个独立流程在 1.7 中仍然可用，并且不接受 RFC 8707 resource indicators，也不会向 `deviceCode` 表添加 OAuth 字段。

要让已注册的 CLI、电视应用或其他输入受限的客户端获取 OAuth 令牌，请在 OAuth Provider 旁添加 OAuth Device Authorization 集成：

```ts title="auth.ts"
import {
  oauthDeviceAuthorization,
  oauthProvider,
} from "@better-auth/oauth-provider";
import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    jwt(),
    oauthProvider({
      loginPage: "/sign-in",
      consentPage: "/consent",
      scopes: ["openid", "profile", "offline_access", "api:read"],
      resources: ["https://api.example.com"],
    }),
    oauthDeviceAuthorization(),
  ],
});
```

此集成会向 `deviceCode` 添加可为空的 `oauthClientId` 和 `resources` 字段，在创建代码时验证 OAuth 客户端、范围和资源，在 discovery 中公布设备授权端点，并在 `/oauth2/token` 处交换已批准的代码。启用此集成时，请重新生成并应用模式。现有的 1.6 行无需回填，即使其 `clientId` 后来与已注册的 OAuth 客户端匹配，也会继续使用会话令牌路径。让待处理的 1.6 客户端完成对 `/device/token` 的轮询，或让其代码过期，然后再将这些客户端迁移到 OAuth 流程。

只有当服务器使用 OAuth grant 时，客户端类型才会公开 RFC 8707 `resource` 请求字段：

```ts title="auth-client.ts"
import { oauthDeviceAuthorizationClient } from "@better-auth/oauth-provider/client";
import { createAuthClient } from "better-auth/client";

export const authClient = createAuthClient({
  plugins: [oauthDeviceAuthorizationClient()],
});
```

请参阅[授权 CLI 调用 API](/docs/plugins/device-authorization#授权-cli-调用-api)，了解客户端注册、批准和轮询示例。

***

## 双因素和无密码安全 [#双因素和无密码安全]

### 双因素 `enableTwoFactor` 返回区分响应 [#双因素-enabletwofactor-返回区分响应-1]

`enableTwoFactor` 现在接受值为 `"otp"` 或 `"totp"`（默认值为 `"totp"`）的 `method`，并在响应中返回该方法。`totpURI` 和备用代码仅在 `"totp"` 时存在。`skipVerificationOnEnable` 选项仍然有效。

**该怎么做：** 更新读取 `totpURI` 或备用代码的调用方，根据返回的 `method` 分支处理。

### 魔法链接和电子邮件 OTP 登录可以清除未经证明的凭证 [#魔法链接和电子邮件-otp-登录可以清除未经证明的凭证]

魔法链接和电子邮件 OTP 登录现在会将已证明的邮箱控制权视为某个邮箱从未确认过的账户的事实来源。如果该账户存在未经证明的密码或其他关联账户，Better Auth 会将其全部移除，并在用户登录之前撤销现有会话。

**该怎么做：** 如果用户使用电子邮件和密码注册，但在确认验证邮件之前改为通过魔法链接或电子邮件 OTP 首次登录，请要求他们通过密码重置设置新密码。

***

## 值得注意的行为变化 [#值得注意的行为变化]

对于大多数配置而言，这些变化不需要采取行动，但会改变你看到的结果。

* **更安全的 OAuth 令牌处理：** 不同客户端使用的 refresh token、重放的授权码，以及与登录时使用的 URI 不匹配的 `redirect_uri`，现在都会使用正确的标准错误被拒绝。重放的代码还会撤销已经签发的令牌。行为规范的客户端不受影响。
* **不缓存凭证：** token、introspection、userinfo、registration 和 device-authorization 响应现在都会发送 `Cache-Control: no-store`，因此代理和浏览器不会缓存它们。
* **userinfo 拒绝无效令牌：** userinfo 端点收到无效访问令牌时，会返回带有 `WWW-Authenticate` 标头的 `401 invalid_token`。
* **OAuth authorize 错误重定向：** 缺少 `response_type` 时，现在会将错误重定向到经过验证的客户端 `redirect_uri`，而不是通用错误页面。
* **Drizzle 受影响行验证：** Drizzle 适配器在受影响行数无效时会抛出异常，而不是返回 `0`。
* **`organization.updateTeam` 不可变字段：** 请求正文中不再接受 `id`、`createdAt` 和 `updatedAt`。
* **`updateMemberRole` 排序：** 角色存在性验证现在会在授权检查之后运行。
* **CLI `generate --output` 输出到目录：** 会选择适配器特定的默认文件名。
* **生成的模式和已禁用的迁移：** 会省略对已禁用迁移模型的引用。
* **Cookie-cache 会话绑定：** 缓存的会话现在会绑定到 `session_token` cookie。
* **SSRF 主机检查：** 出站主机分类现在会阻止更多保留范围（6to4 中继任播地址、本地站点 IPv6 和 IPv4 兼容 IPv6）。
* **标准令牌兑换错误：** 授权码兑换失败时会返回 `400 invalid_grant`，而不是 `401 invalid_client` 或 `invalid_request`。
* **使用外部会话存储时的登出钩子：** 即使使用 `secondaryStorage` 和 `preserveSessionInDatabase`，`session.delete` 钩子现在也会在登出时运行。
* **双因素失效错误代码：** 双因素挑战清理失败时，现在会返回 `FAILED_TO_INVALIDATE_TWO_FACTOR_CHALLENGE`。

***

## 验证升级 [#验证升级]

部署 1.7 后，验证应用使用的路径：

* 使用电子邮件和密码、每个 OAuth 或 SSO 提供方，以及配置后可用的 Google One Tap 登录。
* 关联、列出、刷新和取消关联外部账户。
* 如果你运行身份提供方，请测试 OAuth 授权、刷新、撤销、discovery 和受保护资源检查。
* 连接 MCP 客户端并完成一次受保护的请求。
* 确认 SAML SP 发起的登录，以及启用时的 IdP 发起的登录。
* 在恢复配置之前，确认 SCIM User 关联、Group 成员关系、生命周期变化和应用投影。
* 确认代理部署能够生成公共回调、issuer、discovery 和 JWKS URL。

