升级到 Better Auth 1.7
将 Better Auth 从 1.6 升级到 1.7,包括 Expo、OAuth、OpenID Connect、MCP、SAML、SCIM、代理和自定义适配器变更。
Better Auth 1.7 的大多数变更都是新增性的。大多数项目只需先执行一条命令:
npx auth upgrade将 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 |
| 运行自己的 OAuth 或 OpenID 提供方 | 作为身份提供方 |
| 运行 MCP | MCP |
| 使用 SAML 或 SSO 域验证 | 企业级 SSO |
| 使用 SCIM | SCIM |
| 使用 Stripe 计费 | Stripe |
| 使用双因素认证、魔法链接或邮箱 OTP | 双因素和无密码安全 |
| 使用设备授权插件 | 设备授权 |
| 使用自定义数据库适配器、辅助存储或速率限制存储 | 自定义适配器和存储 |
升级之前
升级命令会处理软件包更新。数据库需要更加谨慎,因为某些 1.7 功能会更改表或要求手动执行数据步骤。auth CLI 要求 Node.js 22.12 或更高版本。
这些功能会更改 schema:
| 功能 | 增加内容 | 需要手动准备? |
|---|---|---|
| 受保护资源 | 新的资源表和键列 | 否 |
| 资源绑定令牌 | 令牌表中的资源列 | 否 |
| DPoP | 令牌绑定列 | 否 |
| 刷新令牌重用窗口 | 刷新令牌上的缓存重放响应列 | 否 |
| 授权码重放 | 两个令牌表上带索引的 authorizationCodeId 列 | 否 |
| 后端注销 | 注销 URL 和撤销列 | 否 |
| 请求的用户信息声明 | 令牌表和同意表中的请求声明列 | 否 |
| 账号身份 | issuer 列,以及与现有 provider-account 列的复合索引 | 是,需要回填 |
| SCIM | 七个配置模型,以及三个可选的托管目录模型,其布局会替代旧版 SCIM 模型 | 是,需要重新配置 |
| 组织团队计数器 | team.memberCount 和 teamMember.membershipKey 列 | 否 |
| 提供方客户端存储 | oauthApplication 变为 oauthClient,并增加新的令牌表 | 是,需要移动客户端数据 |
| 设备授权 | deviceCode 和 userCode 上的唯一索引;可选 OAuth 授权还会增加 oauthClientId 和 resources | 是,需要去重;MySQL/SQL Server 还需要绑定字符串 |
在应用生成的 1.7 schema 之前,准备好账号身份、OAuth 客户端和 SCIM 数据。CLI 会增加表、列和索引,但不会为你选择发行者、复制 OAuth 客户端或转换旧版 SCIM 数据。
请按以下顺序操作:
- 当相关章节适用于你的项目时,完成账号身份按发行者进行限定、OAuth 客户端记录、SCIM 和设备授权中的手动准备工作。
- 应用 1.7 schema。使用内置 Kysely 适配器时,运行
npx auth migrate。使用 Drizzle、Prisma 或自定义 schema 工作流时,运行npx auth generate,检查输出,然后使用你的迁移工具应用它。 - 一起部署 1.7 软件包和配置变更。
- 完成所有部署后工作,包括重新配置 SCIM。
作为登录客户端
本节涵盖邮箱和密码、社交登录、通用 OAuth 插件、One Tap 以及使用 SSO。
账号身份按发行者进行限定
Better Auth 现在通过 issuer 和 accountId 的唯一组合识别外部账号。providerId 仍然是本地提供方配置,而 account.id 标识 Better Auth 账号行,account.accountId 仍然是提供方分配的稳定标识符。账号 schema 增加了必需的 issuer 字段,并在两个身份字段上创建唯一复合索引,但不会重命名 accountId。
在更改账号 schema 之前,请使用维护窗口并停止身份验证写入,包括直接向 account 插入数据的后台任务和管理 API。生成的迁移无法替你选择可信发行者或解决身份冲突。
如果你在完成回填之前已经针对 MySQL 运行了 auth migrate,请先检查是否发生数据损坏。在 SQLite、Postgres 和 SQL Server 上,为没有默认值的必填列增加数据会安全失败,但 MySQL 的默认 sql_mode 会静默接受该操作,并将所有现有行的 issuer 回填为空字符串,而不是抛出错误。
SELECT COUNT(*) FROM account WHERE issuer = '';非零计数意味着数据库需要修复,而不只是回填:重新回填之前先删除复合唯一索引,因为该索引建立在损坏的空字符串值之上,当行解析为相同的真实发行者时会抛出重复键错误。MySQL 对表不支持 DROP INDEX IF EXISTS,并且如果索引创建从未运行,该索引可能尚不存在,因此请先检查。
SHOW INDEX FROM account WHERE Key_name = 'account_issuer_accountId_uidx';
-- Run the DROP INDEX below only if the query above returned a row.
DROP INDEX account_issuer_accountId_uidx ON account;所有行都有正确值后,作为下面第 5 步的一部分重新创建索引。
CREATE UNIQUE INDEX account_issuer_accountId_uidx ON account (issuer, accountId);回填账号身份:
-
备份
account和user表,然后盘点每个不同的providerId。将account.fields中的自定义物理字段名也纳入盘点。 -
在回填期间将
issuer添加为可空字段。保留现有的物理accountId列。 -
根据账号类型填充两个字段:
账号类型 issueraccountId凭据 local:credential链接的 user行中的稳定idSIWE 钱包, providerId为siwelocal:siwe现有的 <address>:<chainId>值Google One Tap, providerId为googlehttps://accounts.google.com现有的 Google sub具有发行者的提供方 提供方使用的确切可信发行者 现有的提供方账号标识符 没有发行者的 OAuth 提供方 local:oauth:<encoded providerId>现有的提供方账号标识符 合成发行者会严格按照
encodeURIComponent(providerId)对其提供方 ID 片段进行百分号编码;例如,local:oauth:github和local:oauth:team%2Fgithub。请为你的部署构建明确的providerId到发行者映射。代表同一 OpenID Connect 权威机构的多个提供方配置必须使用同一个发行者。不要根据邮箱、显示名称、未经验证的请求值或可变的授权端点推导发行者。SIWE 和 Google One Tap 不是 OAuth 提供方,因此合成的local:oauth:命名空间不适用于它们。 -
在创建唯一索引之前查找冲突。根据你的物理表和字段名调整此查询:
Identity collision check SELECT issuer, accountId, COUNT(*) AS accountCount, COUNT(DISTINCT userId) AS userCount FROM account GROUP BY issuer, accountId HAVING COUNT(*) > 1;如果重复行属于同一个用户,请选择要保留的账号记录,并在删除其他记录之前协调其提供方配置、令牌、作用域和时间戳。如果一个键属于多个用户,请停止迁移,并根据可信的提供方数据确定所有者。绝不要仅通过匹配邮箱来合并用户。
-
确认每一行都有两个身份字段,然后将
issuer设为非空,并在issuer和accountId上添加唯一复合索引。auth migrate永远不会生成将现有可空列设为非空的语句;请自行运行此 DDL。Postgres ALTER TABLE account ALTER COLUMN issuer SET NOT NULL; CREATE UNIQUE INDEX account_issuer_accountId_uidx ON account (issuer, accountId);MySQL ALTER TABLE account MODIFY COLUMN issuer VARCHAR(255) NOT NULL; CREATE UNIQUE INDEX account_issuer_accountId_uidx ON account (issuer, accountId);SQL Server ALTER TABLE account ALTER COLUMN issuer VARCHAR(255) NOT NULL; CREATE UNIQUE INDEX account_issuer_accountId_uidx ON account (issuer, accountId);SQLite 没有可以添加
NOT NULL约束的ALTER COLUMN,因此请改为重建表:创建带有该约束的替代表,将所有行复制过去,删除原表,重命名替代表,然后重新创建原表拥有的所有索引和外键。SQLite table rebuild CREATE TABLE account_new (/* same columns as account, with issuer TEXT NOT NULL */); INSERT INTO account_new SELECT * FROM account; DROP TABLE account; ALTER TABLE account_new RENAME TO account; CREATE UNIQUE INDEX account_issuer_accountId_uidx ON account (issuer, accountId); -
更新自定义适配器、数据库钩子和生成的 schema,使其包含
issuer。凭据账号继续将所链接用户的稳定id作为accountId;邮箱仍然是可变的登录标识符,不会改变账号键。
账号专用 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 和 SSO 登录、显式账号链接、取消链接、提供方配置文件获取以及令牌刷新。
检查每个自定义 OAuth 提供方的账号 subject。OpenID Connect 发现型提供方现在使用经过验证的 sub;普通 OAuth 提供方使用 id;之前在这两个字段之间进行运行时回退的行为已移除。当任一默认值不是提供方的不可变标识符时,请设置 accountSubject。现在 getUserInfo().user 只包含可变的本地用户字段;请将提供方标识符保留在 getUserInfo().data 中。mapProfileToUser 不再能返回 id。accountInfo 响应会将选定的身份公开为 account.accountId,而不是 user.id。
迁移 Microsoft 账号标识符
Microsoft 账号现在使用稳定的目录 oid 声明,而不是成对且特定于应用的 sub 声明。请在账号身份回填的第 3 步中完成此操作,即检查冲突或添加复合账号索引之前:
- 盘点内置提供方中
providerId: "microsoft"的行,以及 Generic OAuth 辅助工具中providerId: "microsoft-entra-id"的行。 - 在账号身份回填过程中为每一行分配其可信发行者,并将其旧的基于
sub的accountId替换为该目录用户经过验证的oid。 - 如果有已存储的 Microsoft ID 令牌,请验证每个令牌并复制其
oid声明。mapProfileToUser无法覆盖由提供方拥有的账号标识符。 - 如果没有已存储的 ID 令牌,请在切换期间暂停 Microsoft 登录和账号链接,并从可信的 Microsoft Entra 导出中获取映射。Better Auth 无法仅从旧账号行推导
oid;在迁移之前接受流量可能会创建重复账号。
如果你使用带有自定义作用域或 getToken 的 Generic OAuth microsoftEntraId 辅助工具,请确保令牌交换仍然返回 Microsoft 的 id_token,并且发现机制提供验证它所需的发行者和 JWKS 元数据。该辅助工具还要求提供具体的租户 GUID。请将 common、organizations 或 consumers 配置替换为内置的 Microsoft 社交提供方,该提供方会验证租户声明并推导令牌的实际发行者。
通用 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 登录流程现在需要 S256 PKCE,不再信任自定义 origin 头,并且对自定义 URL scheme 的匹配更加安全。
操作方法: 同时升级 @better-auth/electron 客户端和服务器。确保你的应用 URL scheme 位于 trustedOrigins 中。删除旧的 disableOriginOverride 选项。检查带有主机的可信条目,例如 myapp://callback,因为它们不再匹配外观相似的主机。
generateState() 签名已更改
公开的 generateState() 辅助函数现在接收一个选项对象,而不是位置参数。
你需要做的: 如果你直接调用 generateState(),请将 generateState(c, link, additionalData) 替换为选项形式 generateState(c, options)。
OAuth 回调错误码已重命名
OAuth 回调重定向错误值 email_doesn't_match 已重命名为 email_does_not_match。
你需要做的: 如果你从回调重定向中读取此错误码,请将字符串更新为 email_does_not_match。
Google One Tap 需要客户端 ID
在 oneTap() 上设置 clientId,或使用客户端 ID 配置 Google 社交提供方。One Tap 现在会根据经过验证的 Google subject 绑定账号,而不是通过邮箱匹配。
作用域会在登录之间保留
已授予的作用域以前会彼此覆盖。先前授予的权限可能会在后续请求更少权限的登录之后消失。
Better Auth 现在会在重新登录和令牌刷新期间保留账号上已有的作用域,使用现有的 account.scope 字段。这是一个行为修复:没有架构变更、没有回填,大多数项目也无需操作。
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 客户端现在使用异步 SecureStore API 访问 Cookie 和会话缓存。authClient.getCookie() 返回 promise,因此请直接 await,并将读取 Cookie 的回调设为异步。
const cookie = await authClient.getCookie();直接传递 expo-secure-store 仍然可以正常工作,无需包装器。自定义存储实现必须提供 getItem、getItemAsync、setItem 和 setItemAsync。导出的 storageAdapter.setItem() 方法是同步的;当调用方必须等待持久化完成时,请使用 setItemAsync()。
作为身份提供方
本节涵盖 @better-auth/oauth-provider。
迁移 OAuth 客户端记录
如果你使用 1.6 内置的 oidcProvider 或 MCP 插件,请将每个 oauthApplication 复制或重新注册为 oauthClient。将 redirectUrls 映射为 redirectUris,将 metadata 转换为 JSON,并设置客户端的授权类型和令牌端点认证方法。让旧访问令牌过期,然后在创建 1.7 令牌表之前删除或重命名旧的 oauthAccessToken 表。CLI 不会复制这些记录,也不会将 oauthAccessToken.accessToken 重命名为 token。
如果你已经使用 @better-auth/oauth-provider,请按以下方式迁移现有的 oauthClient 行:
- 增加可空的
applicationType、clientDiscoveryId和clientCredentialsScopes列。除非你拥有客户端的可信发现来源,否则将clientDiscoveryId保留为 null。 - 将现有的
web和native客户端类型映射到applicationType。单独检查user-agent-based客户端。仅对公共客户端将tokenEndpointAuthMethod设置为none;其他所有方法都是机密的。 - 将
clientCredentialsScopes设置为空数组,然后为使用client_credentials授权的每个客户端分配获准的机器作用域。从提供方配置中删除clientCredentialGrantDefaultScopes。 - 在添加复合唯一索引之前,删除相同
clientId和resourceId的重复oauthClientResource行。 - 回填后删除已移除的
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 重命名令牌验证器
普通的令牌检查辅助函数 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 发起的注销可能需要浏览器确认
在 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 令牌声明现在不能再设置标准为服务器保留的协议声明:发行者、主题、受众、过期时间、nonce、会话绑定、auth_time、acr、amr 和 azp。你自己的命名空间声明仍然会出现。ID 令牌也会报告 acr: "0",而不是某个供应商特定的值。
操作方法: 如果 customIdTokenClaims 回调、扩展声明贡献器或每次签发的 idTokenClaims 设置了这些保留声明之一,该值现在会被忽略。请将数据移到你自己的命名空间声明中,或依赖服务器的值。
ID 令牌不再包含 profile 和 email scope 声明
通过授权码流程签发的 ID 令牌不再携带 profile 和 email scope 声明。这些声明可从 UserInfo 端点获取。
该怎么做: 请改为从 UserInfo 读取 profile 和 email 声明,而不是从 ID 令牌中读取。
jwt.sign 回调必须与配置的 alg 匹配
当自定义 jwt.sign 回调的算法与 ID 令牌签发期间的 keyPairConfig.alg 不同时,该回调会被拒绝。
要做什么: 将你的自定义签名算法与 keyPairConfig.alg 保持一致。
/oauth2/revoke 拒绝有效的 JWT 访问令牌
撤销一个仍然有效的 JWT 访问令牌现在会返回 400 unsupported_token_type。
该怎么做: 撤销 refresh token 或不透明访问令牌;不要对 JWT 访问令牌调用 revoke。
max_age 已强制执行
当客户端请求 max_age,而用户的登录时间早于该值时,提供方会将他们重定向回登录。之前,这个请求会被忽略。
需要做什么: 无需配置任何内容。如果某个客户端发送了 max_age 并期望它被忽略,现在则应预期会出现重新登录提示。
注意你的日期列。 max_age 检查会将会话的创建时间按日期读回。如果你的自定义模式将会话时间戳存储为文本,而不是实际的日期或整数时间戳类型,该检查可能会误读该值,并将用户送入登录循环。请将 user、session、account 和 verification 的时间戳存储为日期或时间戳类型。Better Auth CLI 会生成正确的类型。
客户端创建返回 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 端点(token、authorize、revoke、introspect、register、end-session)上的验证失败和格式错误请求失败现在返回 RFC 6749 的 { error, error_description } 信封,而不是之前通用的验证错误形状。
要做什么: 如果客户端或工具解析了旧的错误形状,请更新为读取 error 和 error_description。
内省返回一致的声明
/oauth2/introspect 现在对不透明令牌返回的声明与对 JWT 返回的声明相同,并且资源服务器可以内省发给不同客户端的令牌。
要做什么: 无需操作。对于不透明令牌,预期会获得更丰富且一致的内省响应。
UserInfo 接受表单正文中的 bearer token
userinfo 端点现在接受在表单编码正文中的访问令牌,并会拒绝同时在请求头和正文中发送令牌的请求。
该怎么做: 将访问令牌只放在一个位置发送,Authorization 请求头或表单正文,二者选其一,不要同时发送。
客户端认证与授权类型绑定
通过扩展接口注册的自定义客户端认证方法,现在只能证明是哪个客户端在调用。由服务器决定该客户端被允许执行什么操作。
要做什么: 添加了客户端认证方法的配套插件应依赖服务器解析出的客户端,而不是返回它们自己的客户端判断。
服务器端 OAuth 请求拒绝重定向
Better Auth 现在会拒绝其发出的服务器端 OAuth 请求中的 HTTP 重定向:令牌交换、令牌刷新、客户端凭证、令牌自省以及 JWKS 请求。符合规范的 OAuth 提供商会直接响应这些端点,而不会进行重定向。
需要做什么: 对于标准提供商,无需任何操作。如果自定义提供商端点会重定向,请让它直接返回最终响应。
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
如果你通过打补丁或 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 插件已被移除
操作方法: 将 better-auth/plugins 中的 oidcProvider 替换为 @better-auth/oauth-provider 中的 oauthProvider,并迁移配置。
MCP
MCP 迁移到独立包
MCP 插件从 better-auth 移至 @better-auth/mcp,并使用 @better-auth/oauth-provider。
你需要做什么:
- 安装
@better-auth/mcp、@better-auth/cimd以及应用所需的官方版本 2 MCP 客户端或服务器软件包。从@better-auth/mcp导入 MCP 授权和受保护请求辅助工具。将已移除的内置客户端和适配器替换为@modelcontextprotocol/client或@modelcontextprotocol/server。 - 添加必需的
jwt()插件。使用cimd({ fetchClientMetadataResource, metadataProfile: "mcp-2026-07-28" })组合mcp()以进行客户端注册。 - 将选项从
oidcConfig移动到mcp({ ... })的顶层。设置一个规范的 HTTPSresource,例如https://api.example.com/mcp,并用该值替换resourceMetadataMappings。 - 将
withMcpAuth重命名为requireMcpAuth,将mcpHandler重命名为createMcpProtectedRequestHandler。应用受保护资源作用域选项变更。 - 使用带有
legacy: "reject"的版本 2createMcpHandler,用requireMcpAuth包装它,并且只公开POST。删除 MCP 路由的GET和DELETE导出,以及redisUrl等会话存储选项。 - 通过迁移 OAuth 客户端记录迁移已注册的客户端。
OAuth 端点从 /mcp/* 移至 /oauth2/*。基于发现机制的客户端会自动找到新端点。MCP 刷新令牌重用间隔现在对每个客户端默认为 30 秒;在 mcp() 上设置 refreshTokenReuseInterval: 0 以要求严格的重放处理。
mcp() 不再启用未经身份验证的动态客户端注册。如果你确实支持它,请同时设置 allowDynamicClientRegistration 和 allowUnauthenticatedClientRegistration。否则,请使用上面的 CIMD 配置。
企业级 SSO
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。在账号身份回填期间,使用确切的 OIDC 发行者和 subject,或确切的 SAML IdP entity ID 和 NameID。具有相同 OIDC 发行者和 subject 的提供方别名会去重为一个外部身份,但此变更不会为这些别名引入独立的授权或提供方生命周期记录。
如果之前的映射使用了 mapping.id,请在维护窗口前准备替代值。构建从每个旧账号 subject 到协议定义身份的可信映射,然后在添加复合索引之前重写这些账号行:OIDC 需要确切的发行者和经过验证的 sub;SAML 需要实际的 IdP 元数据 entity ID 和已签名的 NameID。请从身份提供方获取此映射,而不是使用邮箱或可变的配置文件属性。不要为旧映射部署运行时回退。
IdP 发起的 SAML 默认关闭
身份提供商发起的未请求登录现在默认被禁用。登录响应会针对 InResponseTo 进行校验,因此不能重放到它未发起请求的登录上,而单点注销请求会通过 SessionIndex 匹配到对应会话。
该怎么做: 如果你依赖旧行为,请设置 saml.allowIdpInitiated: true 来恢复它。
SAML 证书可以是一个列表
签名证书现在既可以接受单个值,也可以接受列表,这使你能够在不停机的情况下轮换证书。管理端点会将证书作为列表返回,或者当证书位于 idpMetadata 文档中时省略它。
该怎么做: 更新任何从这些端点读取证书的代码,使其预期接收列表或不存在的情况。确保每个 SAML 配置都提供一个签名证书来源,即明确的证书或 idpMetadata 文档,否则注册会失败。
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 签名强制规则与其配置一致
在 1.6 中,wantAssertionsSigned 强制要求 SAML 响应消息签名,而不是断言签名。1.7 会验证断言元素本身,并对其应用配置的要求,因此当服务提供方要求断言签名时,仅签署响应消息的 IdP 登录会失败。携带 RelayState 的回调现在会无条件验证它,并且提供方注册会拒绝弱化已签名断言策略、超过元数据大小限制或声明包含 URL 片段的 ACS 位置的服务提供方元数据。
操作方法: 确认每个 IdP 都会签署断言,或者对于无法签署断言的 IdP,明确设置 wantAssertionsSigned: false。修复或重新注册不再通过验证的已存储 SP 元数据。如果自定义集成在 SAML 回调中提供自己的 RelayState,请确保它能够往返传递 Better Auth 签发的值。
/sso/update-provider 会拒绝部分映射
更新 SSO provider 现在会拒绝不完整的 OIDC 或 SAML 映射对象。
该怎么做: 在调用 /sso/update-provider 时发送完整的映射对象。
OIDC SSO 可在 Cloudflare Workers 上运行
带发现机制的 OIDC SSO 现在可在 Cloudflare Workers 上运行。会重定向的 discovery 或 token 端点会被拒绝,并给出清晰的配置错误,而不是以运行时特定方式失败。
该怎么做: 不需要做任何事。如果某个 provider 端点会重定向,请将配置指向最终 URL。
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 配置单独的登录方式。以下示例使用静态模式:
scim({
connections: [
{
id: "workforce-acme",
provisioningDomainId: "workspace-acme",
credentials: [
{ type: "bearer", id: "workforce-primary", token: workforceToken },
],
},
],
});SCIM 需要完整重新配置
新的 SCIM 模型不会读取或转换 1.6 SCIM 表,因此此变更无法使用原地生成的迁移。
请使用维护窗口。在更改 schema 之前,暂停配置,并停止每个运行旧版 SCIM 插件的应用程序实例。
操作方法:
- 备份每一张旧版 SCIM 表。建立一份经过审核的清单,准确记录
scimProvider行、由 SCIM 创建的account行、其关联的user行,以及由配置过程创建的任何组织成员关系或团队状态。使用已配置的提供方和目录主体来识别它们。不要仅根据providerId前缀对行进行分类。 - 决定如何处理每个旧版 User 和 account 行。要保留某个 User,请将稳定的 connection-and-subject-to-
userId映射复制到应用自有存储中,仅删除已确认的旧版 SCIM account 行,并在重新配置之前配置identity.resolveUser。要重新创建某个 User,请删除已确认的 SCIM account,并且只有在证明没有其他登录方式或应用数据依赖它之后,才能删除该 User。保留所有无关的 account 和应用行;Better Auth 不会自动识别或删除旧版 SCIM account 行。 - 选择静态、应用自有运行时或插件管理的 connection 模式,并准备新的不透明 connection 和 credential 标识符。绝不要导入旧版 token 哈希、重复使用旧版原始 token,或添加接受旧版 bearer 语法的兼容路径。静态模式和应用自有模式使用新的高熵 secret;管理模式会在第 5 步创建管理表之后,于第 6 步生成新的 secret。
- 在旧插件停止运行期间,通过旧版 SCIM 端点或由操作员控制的事务,按依赖顺序清理其 SCIM 自有资源。确认旧版插件表为空,并且清单中的每个 SCIM account 行都已有经过审核的处置结果。只有在完成审核备份和清理之后,才能删除或重命名不兼容的旧版物理表,包括
scimProvider。如果你自定义了模型名称,请使用架构中的物理表名。 - 在数据库适配器中启用原生交互式事务,然后通过适配器特定的工作流应用 1.7 架构,以创建
scimConnectionBinding、scimIdentityTombstone、scimSubject、scimUser、scimGroup、scimGroupMember和scimProjectionGrant。管理模式还会创建scimManagedConnection、scimManagedCredential和scimManagedConnectionEvent。Cloudflare D1 无法提供所需的事务行为。 - 部署选定的模式。对于管理模式,请在新架构存在之后,通过受信任的服务器 API 创建 connection 并生成其第一个 credential。使用新 credential 配置目录,然后触发完整的 User 和 Group 配置周期。
- 在应用中验证 User 关联、Group 状态、生命周期和角色投影,然后恢复配置。
新插件绝不会通过 email 进行关联。保留的旧版 User 在重新配置期间会导致 409 Conflict,除非 identity.resolveUser 为其返回明确的关联决策。
请参阅 SCIM 插件文档,了解最终配置和受支持的协议行为。
Stripe
组织订阅需要 organization.enabled
referenceMiddleware 现在会拒绝组织范围内的订阅,除非在 Stripe 插件配置中设置了 organization: { enabled: true }。
该怎么做: 对于组织范围内的订阅,请在你的 stripe() 插件选项中设置 organization: { enabled: true }。组织插件仍然需要单独启用,以解析当前活跃组织。
onSubscriptionCancel 事件是必需的
onSubscriptionCancel 回调中的 event 参数现在是必需的。
该怎么做: 更新你的 onSubscriptionCancel 回调,使其接受一个非可选的 event。
代理后面
Dynamic base URLs do not trust forwarded headers by default
当 baseURL 使用 allowedHosts 时,除非主动选择启用,否则 Better Auth 会忽略转发的标头。
What to do: 如果你的代理仅通过 x-forwarded-host 暴露公共主机名,请选择启用:
betterAuth({
baseURL: { allowedHosts: [...] },
advanced: {
trustedProxyHeaders: true,
},
});代理会为你重写主机的配置(例如 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 需要你的规范源
有两种 OAuth 提供方行为会读取传入请求的源。在自定义服务器或终止 TLS 的代理后面,这个源可能是内部绑定地址,而不是你的公网源。提供方会将你的 consentPage 和 loginPage 作为相对路径返回,因此服务器端重定向(例如 NextResponse.redirect)需要把它们相对于一个绝对源来解析。原生 DPoP 还会把证明中的 htu 与令牌端点为自身计算出的 URL 进行比较。
怎么做: 将 baseURL 视为服务器身份,并在路由边界、供提供方读取请求之前,把传入请求的协议和主机规范化为它。只要在任何路由转发到提供方的地方应用一个辅助函数,就能同时修复 consent 重定向、DPoP 的 htu 检查,以及源检查。
自定义适配器和存储
原子状态相关工作引入了必需方法。如果你只使用内置适配器和存储,可以跳过这一部分。
数据库适配器必须实现 incrementOne 和 consumeOne
incrementOne 会以原子方式更新一行的计数器并返回该行;如果条件未匹配,则返回 null。consumeOne 会一次性读取并删除一行,适用于一次性凭证。这两个方法现在都是必需的,旧的回退逻辑已经移除。
要做什么: 在任何自定义适配器中实现 incrementOne 和 consumeOne。缺少 consumeOne 会在运行时抛出错误。所有内置适配器都已经实现。
二级存储必须实现 increment 和 getAndDelete
increment(key, ttl) 会将计数器加一,并且只在键首次创建时设置过期时间。getAndDelete(key) 会一次性读取并移除一个键。这两个方法之前是可选的,现在是必需的。
要做什么: 在自定义二级存储中实现 increment 和 getAndDelete。Redis 存储已经实现。
限流存储使用 consume
限流存储现在需要一个 consume(key, rule) 方法,在一次操作中完成检查和递增。独立的 get 和 set 不再被接受。
要做什么: 用 consume 替换自定义限流存储中的 get 和 set。
Drizzle 模式在 usePlural 下使用单数关系键
Drizzle 模式生成器现在会为多对一关系输出单数键。这只会影响配置了 usePlural: true 的项目;默认的单数配置不受影响,反向的“多”关系键也不会改变。
要做什么: 如果你设置了 usePlural: true,请重新生成你的 Drizzle 模式并检查关系键。
getIp 已重命名为 getIP
公开导出的 getIp 现已重命名为 getIP。
要做什么: 将 IP 辅助函数的导入更新为 getIP。
验证码
验证码匹配完整路径
验证码规则现在会匹配完整的请求路径或显式通配符,这样就堵住了通过部分路径匹配来跳过验证码规则的一种方式。内置的 /sign-in/email-otp 例外也已移除。
该怎么做: 将像 /sign-in 这样的部分路径替换为 /sign-in/* 或 /sign-in/**。请注意,/sign-in/* 通配符现在也会匹配 /sign-in/email-otp,而之前的例外并不会覆盖它:如果对其加上门禁,那么对于不发送 x-captcha-response 的客户端,email-OTP 登录将返回 400 MISSING_RESPONSE。如果要让 email-OTP 登录不受门禁限制,请列出你要保护的确切端点(例如 /sign-in/email),而不是使用宽泛的 /sign-in 通配符;或者让你的 email-OTP 客户端发送验证码令牌。
双因素和无密码安全
双因素 enableTwoFactor 返回区分响应
enableTwoFactor 现在接受 "otp" 或 "totp"(默认 "totp") 的 method,并在响应中返回该方法。totpURI 和备用代码仅在 "totp" 时提供。skipVerificationOnEnable 选项仍然有效。
该怎么做: 更新读取 totpURI 或备用代码的调用方,根据返回的 method 分支处理。
魔法链接和电子邮件 OTP 登录可以清除未验证的关联账户
魔法链接和电子邮件 OTP 登录会将已验证的邮箱控制权视为某个从未确认过邮箱的账户的事实来源。清除未验证的密码并撤销会话之前就已经发生;1.7 扩展了清理范围,在用户登录之前移除与该未确认身份关联的每个账户,包括 OAuth 和社交登录关联。
该怎么做: 如果某个用户注册了但从未确认邮箱,然后首次通过魔法链接或电子邮件 OTP 登录,预期其之前的密码以及任何社交关联都会消失。请让他们通过密码重置设置新密码,并重新关联任何社交账户。
数据库连接已移出 experimental
将 experimental: { joins: true } 替换为 advanced: { database: { joins: true } }。如果启用连接,请重新生成 Drizzle 或 Prisma 关系。
usePlural 下 Drizzle 关系键为单数
当 usePlural: true 时,Drizzle 模式生成器现在会为多对一关系使用单数键。重新生成模式,并更新读取之前复数关系键的代码。默认的 usePlural: false 配置不变。
getIp 已重命名为 getIP
公开导出的 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 设备授权默认不启用
在 1.6 中,deviceAuthorization() 会将设备登录到同一个 Better Auth 应用,并从 /device/token 返回 Better Auth 会话令牌。这个独立流程在 1.7 中仍然可用,并且不接受 RFC 8707 resource indicators,也不会向 deviceCode 表添加 OAuth 字段。
要让已注册的 CLI、电视应用或其他输入受限的客户端获取 OAuth 令牌,请在 OAuth Provider 旁添加 OAuth Device Authorization 集成:
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 请求字段:
import { oauthDeviceAuthorizationClient } from "@better-auth/oauth-provider/client";
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
plugins: [oauthDeviceAuthorizationClient()],
});请参阅授权 CLI 调用 API,了解客户端注册、批准和轮询示例。
双因素和无密码安全
双因素 enableTwoFactor 返回区分响应
enableTwoFactor 现在接受值为 "otp" 或 "totp"(默认值为 "totp")的 method,并在响应中返回该方法。totpURI 和备用代码仅在 "totp" 时存在。skipVerificationOnEnable 选项仍然有效。
要做什么: 更新读取 totpURI 或备用代码的调用方,根据返回的 method 分支处理。
魔法链接和电子邮件 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_tokencookie。 - 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。