Better Auth 1.7 RC
我们在 Better Auth 1.7 上投入了很多。这是一个重大版本,其中一部分意味着需要进行迁移工作。我们希望支持更广泛的登录流程,让系统更稳健,并推出那些需要结构性改动的安全修复。
以下是此版本带来的内容。
你的应用可以成为其他应用的登录提供方。 如果其他应用通过你的应用登录,Better Auth 现在可以处理更多 OAuth 和 OpenID Connect 规则。这包括 DPoP token、单点登出、强制重新登录、按 API 的 token 规则,以及更安全的 token 检查。
更多登录提供方可用。 Better Auth 现在支持此前表现不佳的登录配置:Amazon Cognito、使用证书登录的 Microsoft Entra ID、标准 OpenID 提供方,以及像 Clever 这样的学校登录提供方。
默认情况下更安全。 我们加强了此前不安全的流程。随着安全性越来越成为重点,我们宁愿披露安全漏洞并迅速修复,也不愿为了向后兼容而继续保留它们。
亮点
重大变更Better Auth 作为身份提供者
这是本次发布中最重要的部分。OAuth 提供程序从“只负责发放 token”成长为一个基于标准的授权服务器,其他应用可以通过它登录。
- 受保护资源: 将登录服务器后的每个 API 描述为独立资源,并为其设置独立的 token 生命周期、scopes 和 claims。token 会锁定到其签发的 API,不再能针对另一个 API 重放。这取代了扁平的
validAudiences列表。 - DPoP 绑定 token: access token 会绑定到客户端持有的密钥,因此仅窃取 token 不足以调用 API。
- 单点登出: OIDC Back-Channel Logout 会告知用户通过你的提供方登录的每个应用,在用户从你的提供方登出时结束自身会话。
- 强制重新登录: 现在会遵守
max_age,而不是忽略它。 - 一致的 introspection: opaque token 会返回与 JWT 相同的 claims,并且独立的 API 服务可以 introspect 发放给另一个客户端的 token。
- 符合协议规范的 ID token: 自定义 claims 不再能覆盖 issuer、subject 和 audience 等保留的协议 claims。
- 自助服务客户端和机器客户端: 后端可以使用预共享 token,在没有登录用户的情况下完成注册;Client ID Metadata Documents(
@better-auth/cimd)允许客户端通过托管 URL 标识自身,这也是 MCP 客户端无需提前注册即可连接的方式。像 Claude 和 Codex 这样的真实 MCP 客户端现在会保密其注册时使用的身份验证方法,默认保持 confidential,而不是被强制设为 public。 - OAuth 客户端的设备授权: 已注册的 CLI、智能电视和其他输入受限的客户端可以请求用户在浏览器中批准访问,然后将 device code 兑换为具有限定 scopes 且绑定资源的 OAuth token。
- 请求特定的用户详细信息: 客户端可以通过标准的
claims.userinfo参数请求单独的用户 claims,并且该请求会成为用户 consent 的一部分。 - 扩展接口:
extendOAuthProvider允许配套插件添加 grant types、客户端身份验证方法、发现元数据和 claims,而无需修改核心代码。 - 协议一致性完善: 标准的
{ error, error_description }响应格式、凭据响应中的no-store、ID token 中的at_hashclaim、form-encoded 请求,以及面向客户端的可选 refresh-retry 窗口。
旧的 oidcProvider 插件已被移除,它在 1.6 中已被弃用。请将 OpenID provider 配置迁移到 @better-auth/oauth-provider。
破坏性变更
连接到更多身份提供商
OAuth 工作的另一半,是关于 Better Auth 作为接收登录的应用。
- 通用 OAuth 已重构,与内置社交提供商走同一路径,默认启用 PKCE,并通过发现机制自动进行发行者验证。你调用的 API 和回调 URL 都会改变。
- 身份令牌会一致地校验,并使用提供商发布的密钥,包括移动端和单页应用。自定义提供商从
verifyIdToken方法迁移到idToken配置。 - 已授权的范围会被保留,在重新登录和令牌刷新时不会被覆盖,而 Google 的
includeGrantedScopes现在也可以配置了(默认仍开启)。 - Google One Tap 会更严格地验证身份令牌,拒绝格式错误的令牌,并遵循
requireEmailVerification。 - 每次请求的登录选项(
additionalParams)支持启用 Cognito 上游路由、Microsoft Entra ID 的domain_hint,以及 Google 的离线或增量访问。证书和签名断言登录(clientAssertion、tokenEndpointAuth)支持 Entra ID 证书登录和通用的private_key_jwt。 - 由提供商发起的登录 可通过
allowIdpInitiated: true安全地重新开始,按提供商的邮箱验证(requireEmailVerification)会在邮箱验证完成前暂不建立会话,而 匿名账户关联 现在可在 Expo 和其他应用内浏览器中正常工作。
新
新增可用的集成
这些配置在以前是不可能实现、会出错或不安全的,现在已经可以正常工作。
使用外部提供商登录你的用户
| 提供商或配置 | 之前受阻的功能 | 现在的工作方式 |
|---|---|---|
| Amazon Cognito 联合登录 | 你无法在每次登录时将用户路由到特定的上游提供商。 | 使用类型化的 identityProvider 和按请求传入的 additionalParams。 |
| Microsoft Entra ID 证书登录 | 仅支持共享密钥登录。 | 使用 clientAssertion 进行证书登录,并在每次登录时使用 domain_hint。 |
| Google 离线和增量访问 | 选项是全局性的,并且每次登录都会覆盖已授予的 scope。 | 使用按请求配置的选项、保留的 scopes 和 includeGrantedScopes。 |
| 通过发现机制接入的任何 OpenID 提供商 | 未验证提供商的身份令牌。 | 指向一个 discovery URL,令牌会自动验证。 |
| Zitadel、Auth0 以及其他多租户 OIDC 提供商 | 刷新令牌时无法发送额外参数。 | 刷新时使用 refreshTokenParams,无需完整重定向。 |
| 需要签名断言登录的提供商 | 仅支持基于密钥的登录。 | 使用带签名 JWT 的 tokenEndpointAuth。 |
| Clever 以及类似的教育类提供商 | 由提供商发起的登录会失败。 | 使用 allowIdpInitiated: true 安全地重新启动流程。 |
其他应用通过你进行登录
| 配置 | 之前受阻的功能 | 现在的工作方式 |
|---|---|---|
| 需要防窃取 token 的 API | 只有普通 bearer token。 | 使用绑定到客户端密钥的 DPoP token。 |
| 一个登录服务器后的多个 API | 一个 token 可以用于任何 API。 | 使用按 API 划分的资源,因此 token 会锁定到其签发的 API。 |
| 自行注册的机器客户端 | 注册需要已登录用户。 | 使用预共享注册 token。 |
| 通过 URL 标识的 MCP 客户端 | 只有基于 endpoint 的 Dynamic Client Registration 可用。 | 将 mcp() 与 cimd() 组合使用以启用 Client ID Metadata Documents,或显式选择 DCR。 |
| CLI 和其他输入受限的 OAuth 客户端 | device flow 返回的是应用会话。 | 添加 oauthDeviceAuthorization() 以发放具有限定 scopes 且绑定资源的 OAuth token。 |
| 应用之间的单点登出 | 登出不会通知其他应用。 | 使用 OIDC Back-Channel Logout。 |
| 强制重新登录 | 请求会被忽略。 | max_age 会被强制执行。 |
企业级 SSO
| 配置 | 之前受阻的功能 | 现在的工作方式 |
|---|---|---|
| SAML 证书轮换 | 只接受一个证书。 | 轮换期间会接受证书列表。 |
| SCIM 组供应 | 没有持久化的组资源。 | 组拥有一级生命周期端点。 |
| Cloudflare Workers 上的 OpenID SSO | 重定向端点会破坏流程。 | 它们会以清晰的配置错误失败。 |
破坏性变更
企业 SSO、SAML 和 SCIM
- 无停机轮换 SAML 证书: 签名证书现在可以是列表,因此管理员可以在旧证书旁发布新证书。管理端点会将证书作为列表返回;如果证书位于
idpMetadata文档中,则会省略该字段。 - 默认关闭未经请求的 SAML 登录:
allowIdpInitiated现在默认为false,因此除非通过allowIdpInitiated: true选择启用,否则由提供商发起的登录会被拒绝。 - 更简单的 SAML 配置: 回调 URL 会自动派生,服务提供商元数据会自动生成,多个未使用字段会被移除,并且 Single Logout 会正确结束会话。其中一个 endpoint 路径会发生变化。
- SCIM 供应域: SCIM 不再依赖 Organization 或 SSO 插件,也永远不会将已供应的身份表示为身份验证账户。使用代码定义的连接,在请求时解析由应用拥有的连接,或选择启用插件管理的连接目录。新模型不会转换 1.6 的供应状态,因此升级需要经过审核的切换,以及完整的 User 和 Group 重新供应。
新增
Drizzle 关系 v2
Drizzle ORM v1 现已进入候选发布版,并带来了全新的 relations API。Better Auth 通过一个新的适配器入口点支持它,因此生成的 auth 关系可以与你应用自身的关系并排使用。
import { betterAuth } from "better-auth/minimal";
import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2";
import { db } from "./db";
import * as schema from "./schema";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg", schema }),
});新
22 种语言的内置 i18n
@better-auth/i18n 内置了 22 种语言的翻译,并且英文回退是固定的。将其接入即可本地化 Better Auth 面向用户的消息,而无需维护你自己的翻译表。
重大变更
默认安全
- 默认不信任转发代理头: 你的应用从
Host头读取自身地址,并忽略x-forwarded-*,除非你通过advanced.trustedProxyHeaders: true显式启用。nginx、Vercel、Cloudflare 和 Netlify 等平台通常无需更改。 - 原子状态,不再有竞争条件: 数据库中的“先检查,再写入”逻辑被单个原子操作取代,因此一次性令牌不能被重复使用,团队不会超过其限额,限流在高负载下也能保持有效。自定义适配器和存储后端现在需要实现必要的方法。
- Electron 需要现代 PKCE: Electron 流程现在要求使用 S256 PKCE,并且不再信任自定义 origin 头。请同时升级客户端和服务器。
- 验证码匹配完整路径: 将诸如
/sign-in这样的部分路径替换为/sign-in/*或/sign-in/**。 - 更严格的自定义协议源: 带主机的
trustedOrigins条目,例如myapp://callback,现在只精确匹配该主机,不再接受myapp://callback.attacker.tld。 - 无密码登录会清除未经证实的凭据: 当账户邮箱从未被确认时,已验证的邮箱控制权优先,因此在登录前会移除未经证实的密码以及任何其他已关联账户。
- 新身份的门禁: 新的
user.validateUserInfo钩子可以在创建用户或链接账户之前拒绝某个身份,适用于每一种注册方式。
重要变更
重大变更
| 变更 | 需要执行的操作 |
|---|---|
| 账户身份按 issuer 划分 | 回填 issuer 和 accountId,解决冲突,然后在部署前添加唯一的 (issuer, accountId) 索引。 |
Microsoft 账户使用稳定的 oid claim | 在添加账户索引前,将每个现有 Microsoft 账户旧的 sub 标识符替换为经过验证的目录 oid。 |
| SIWE 从签名消息派生身份 | 从 nonce 请求中移除 wallet 和 chain 字段;从自定义 getNonce 返回 8 到 250 个字符的字母数字 nonce。 |
受保护资源取代 validAudiences | 将每个 audience 移入 resources,将客户端关联到资源,并运行 schema 迁移。 |
| DPoP 改变 token 验证方式 | 重命名 bearer-token helper,在需要时使用 DPoP 请求验证器,并运行 schema 迁移。 |
| Back-channel logout 会撤销绑定会话的 token | 运行 schema 迁移,预期已登出的 token 会变为 inactive,并审查 logout URI,确保其使用公开的 HTTPS,且不包含凭据或片段。 |
| 通用 OAuth 使用社交提供商路径 | 更新登录调用、关联调用、回调 URL 和客户端插件。 |
| 自定义社交提供商使用一个身份令牌验证器 | 将提供商的 verifyIdToken 方法替换为 idToken 配置。 |
旧的 oidcProvider 插件已移除 | 将提供商配置迁移到 @better-auth/oauth-provider。 |
OAuth Provider 移除 silenceWarnings | 从 oauthProvider() 配置中删除 silenceWarnings。 |
MCP 移至 @better-auth/mcp | 更新导入、endpoint 路径、helper 名称、配置结构和 schema。 |
| SAML 默认值和配置发生变化 | 更新已移除的字段,更新回调 URL,并审查由 IdP 发起的流程。 |
| SCIM 需要完整重新供应 | 停止供应,审查旧身份,替换不兼容的表和凭据,然后运行完整的 User 和 Group 供应周期。 |
| 默认不信任代理头 | 仅当你的代理需要时,才通过 advanced.trustedProxyHeaders: true 选择启用。 |
| 自定义适配器和存储需要原子方法 | 实现 incrementOne 和 consumeOne(适配器)、increment 和 getAndDelete(secondary storage),或 consume(rate-limit storage)。 |
| Expo 安全存储访问是异步的 | 等待 getCookie(),在自定义存储中提供异步方法,并在必须等待写入时使用 storageAdapter.setItemAsync()。 |
| Captcha 规则匹配完整路径 | 将 /sign-in 这样的部分路径替换为 /sign-in/* 或 /sign-in/**。 |
| 自定义协议的可信来源按主机匹配 | 类似 myapp://callback 的带主机条目不再接受 myapp://callback.attacker.tld。重新检查原生端和移动端的 trustedOrigins。 |
| Electron 要求 S256 PKCE | 同时升级 Electron 客户端和服务器。 |
| OIDC ID token 不再包含 profile/email scope claims | 从 UserInfo endpoint,而不是 ID token 中读取 profile 和 email claims。 |
| 同步 OAuth2 请求构建器已移除 | 将 createAuthorizationCodeRequest、createRefreshAccessTokenRequest 和 createClientCredentialsTokenRequest 替换为对应的异步版本。 |
jwt.sign 回调必须匹配 keyPairConfig.alg | 让自定义 ID-token 签名 alg 与配置的密钥对保持一致,否则会拒绝签发。 |
| 更严格的 Dynamic Client Registration 验证 | 发送相互对应的 response_types/grant_types(code response type 要求 authorization_code grant)。 |
| 未认证注册会保留客户端身份验证方法 | 注册默认是 confidential。对于必须保持 public 的客户端,将 token_endpoint_auth_method: "none"。 |
/oauth2/revoke 拒绝有效的 JWT access token | 预期返回 400 unsupported_token_type。请改为撤销 refresh 或 opaque token。 |
| OAuth 回调错误代码重命名 | 将 email_doesn't_match 的处理更新为 email_does_not_match。 |
generateState() 签名发生变化 | 使用新的 options 对象调用它,而不是传入位置参数 (c, link, additionalData)。 |
| SSO SAML 配置注册发生变化 | 提供签名证书来源,并预期获得完整的小写 ACS 错误重定向代码,而不是简短别名。 |
/sso/update-provider 拒绝部分映射 | 发送完整的 OIDC/SAML 映射对象,而不是部分对象。 |
| auth CLI 要求 Node.js 22.12+ | 升级用于运行 CLI 的 Node.js。 |
| 新增 organization 列 | 运行 team.memberCount 和 teamMember.membershipKey 的迁移。 |
Stripe 组织订阅需要 organization.enabled | 除非在 Stripe 插件配置中设置 organization: { enabled: true },否则 referenceMiddleware 会拒绝组织范围的订阅。 |
数据库 joins 移出 experimental | 将 experimental.joins 替换为 advanced.database.joins,然后重新生成 Drizzle 或 Prisma relations。 |
| 默认 Drizzle schema 使用单数 relation key | 重新生成并审查你的 Drizzle schema relations。 |
| Device Authorization schema 和 OAuth grant | 为两个 code 列添加索引,将 code 保持在 191 个字符以内,并且仅在启用 OAuth Device Authorization 时添加 oauthClientId 和 resources。 |
Stripe onSubscriptionCancel event 是必需的 | 更新回调,使其预期接收非可选的 event。 |
| 仅 OTP 的双因素启用 | enableTwoFactor 接受新的 method 参数并返回判别联合响应。更新调用方。 |
公共导出 getIp 重命名为 getIP | 更新 IP helper 的导入。 |
行为变化
| 变更 | 具体变化 |
|---|---|
max_age 现在会被强制执行 | 现在,请求新鲜登录的客户端会真正获得新登录,而不再被忽略。 |
| Token introspection 保持一致 | 不透明 token 返回与 JWT 相同的 claims,而且资源服务器可以 introspect 另一个客户端的 token。 |
| ID token claims 保持协议安全 | 自定义 claims 不再能覆盖保留的协议 claims,并且 ID token 会报告 acr: "0"。 |
| 已授予的 scopes 会被保留 | 后续登录不再会清除之前授予的 scopes。 |
| Google One Tap 更严格地验证 token | 格式错误的 identity token 会被拒绝并返回 400,requireEmailVerification 会返回 403。 |
| Magic-link 和 email-OTP 登录可以清除未经证实的凭据 | 在同一账户上,已验证的邮箱控制权会优先于未确认的密码。 |
userinfo 会用 401 拒绝错误的 access token | 无效 token 会返回 401 invalid_token,并带有 WWW-Authenticate 头。 |
OAuth authorize 会将缺少 response_type 的错误重定向 | 错误会发送到已验证的客户端 redirect_uri,而不是通用错误页。 |
OAuth 客户端创建返回 201 | 创建客户端会返回 201 Created 而不是 200 OK,并且注册过程会强制执行相同的权限检查。 |
| Drizzle adapter 会验证受影响行数 | 无效的受影响行数会直接抛出错误,而不是返回 0。 |
organization.updateTeam 会忽略不可变字段 | 请求体中不再接受 id、createdAt 和 updatedAt。 |
updateMemberRole 会先检查授权 | 角色是否存在的校验会在授权检查之后执行。 |
CLI generate --output 到目录 | 会选择与适配器相关的默认文件名。 |
| 生成的 schema 会跳过禁用迁移的模型 | 会省略对已禁用迁移的模型的引用。 |
| Cookie-cache session 绑定到其 cookie | 缓存的 session 会绑定到 session_token cookie。 |
| SSRF 主机检查覆盖更多保留范围 | 出站主机分类现在会阻止额外的保留范围(6to4 relay anycast、site-local IPv6,以及 IPv4-compatible IPv6)。 |
| OAuth token 兑换错误使用标准代码 | 授权码兑换失败会返回 400 invalid_grant,而不是 401 invalid_client 或 invalid_request。 |
| Sign-out 在外部 session 存储下运行 session-delete hooks | 即使使用 secondaryStorage 和 preserveSessionInDatabase,session.delete hooks 也会在登出时运行。 |
| 双因素失效失败有了自己的错误码 | 失败的双因素 challenge 清理现在会返回 FAILED_TO_INVALIDATE_TWO_FACTOR_CHALLENGE。 |
新增内容
| 变更 | 现在可用的功能 |
|---|---|
| Refresh-token 重试 | 在短暂的重用窗口内重放相同的 refresh 响应。 |
| OAuth provider 扩展接口 | 添加 grant types、客户端身份验证方法、发现元数据和 claims。 |
| Client ID Metadata Documents | 允许客户端通过托管的元数据文档标识自身。 |
| 按请求传入登录选项 | 从单次登录请求中传入提供商特定的登录选项。 |
| 证书和签名断言登录 | 使用证书登录 Microsoft Entra ID,并使用签名 JWT 登录通用 OAuth。 |
| Private-key-JWT 客户端身份验证 | 使用签名 JWT(RFC 7523)而不是共享密钥,对 OAuth provider 和 SSO 客户端进行身份验证。 |
| SCIM groups | 通过持久化的生命周期端点管理 SCIM 组。 |
| 请求特定的用户 claims | 使用 claims.userinfo 参数请求单独的用户详细信息。 |
| Drizzle Relations v2 支持 | 新的 @better-auth/drizzle-adapter/relations-v2 入口点会与你应用的 relations 合并。 |
| Sessions 和 tools | 使用公钥 session 验证、hydrateSession、i18n 和 create-admin。 |
弃用与移除
| 更改 | 替代方案 |
|---|---|
oidcProvider 插件已移除 | 使用 @better-auth/oauth-provider。 |
MCP 插件路径已移出 better-auth | 使用 @better-auth/mcp。 |
| 通用 OAuth 客户端 API 已更改 | 使用标准的社交客户端 API。 |
迁移到 v1.7
由于此版本包含较大的变更,我们会先以候选发布版的形式提供,因此会有一个迁移窗口。我们会继续关注反馈,并在此基础上发布稳定版 1.7。
请从 rc 标签安装它,并对你使用的任何 @better-auth/* 包也执行同样操作:
npm install better-auth@rc在应用 1.7 schema 之前,完成 account identities、OAuth clients、SCIM 和 Device Authorization 所需的任何手动准备工作。然后,对于内置 Kysely adapter,使用 npx auth@rc migrate;或者运行 npx auth@rc generate,并通过你的 Drizzle、Prisma 或自定义迁移流程应用生成的结果。在 1.7 稳定之前,不带范围的 npx auth 仍会解析到 1.6 CLI。有关所需顺序和详细步骤,请参阅 1.7 升级指南。
| 如果你使用 | 预期情况 |
|---|---|
| Basic Better Auth setup | 通常只需要安装 @rc 版本并审查 schema 变更 |
| Existing external account rows | 在应用 account index 前,回填按 issuer 划分的身份并解决冲突 |
| Social login、通用 OAuth、One Tap 或 SIWE | 新的登录客户端行为,包括 Microsoft oid、无地址的 SIWE nonce,以及 Google One Tap |
@better-auth/oauth-provider | 客户端数据准备,以及 resources、DPoP、back-channel logout、max_age 和更安全的 OAuth 检查 |
| MCP | 迁移到 @better-auth/mcp 包、官方 v2 客户端或服务器、新的 endpoint 路径,以及 schema 变更 |
| SAML 或 SCIM | 更安全的 SAML 和 SSO 检查,以及完整的 SCIM 切换和 User、Group 重新供应 |
| Device Authorization | 具备索引且受长度限制的 code;还可以选择添加包含 oauthClientId 和 resources 的 oauthDeviceAuthorization() |
| Magic links 或 email OTP | 对邮箱从未确认的账户进行更安全的处理 |
| Expo 或 React Native | 等待 cookie 读取,并更新自定义 SecureStore 兼容存储实现 |
| Custom adapters、storage 或 rate-limit stores | 需要新的原子方法 |
| Custom proxy 或 TLS termination | 在依赖 DPoP 或 identity-provider 重定向前,检查应用如何计算其公开 origin |
贡献者
感谢所有贡献者让本次发布成为可能!