Better-Auth 添加 authorizationUrlParams 后为何获取用户信息失败?
结论
先检查添加 resource 后获得的 accessToken 是否仍能访问 IDP 的 UserInfo endpoint,以及 getUserInfo 是否返回了有效的 id。
从代码中可以确认,getUserInfo 没有检查 HTTP 状态,就将响应解析为用户资料。如果 /userinfo 返回错误 JSON,profile.sub 可能是 undefined,进而导致用户创建或 session 建立失败。这可以解释“用户信息缺失”和“登录状态没有建立”同时出现的现象。
添加 resource 后,IDP 可能签发面向指定业务 API 的 access token,而该 token 未必能访问 UserInfo endpoint。建议优先验证这一点,但现有代码还不足以确认它是此次 400 的根因。问题中没有给出 Better-Auth 版本、400 的实际来源和 IDP 响应内容。
先定位失败发生在哪一步
一次登录大致会经过以下步骤:
- 浏览器跳转到 IDP 的 authorization endpoint。
- IDP 回调应用,应用使用 authorization code 换取 token。
getUserInfo使用 access token 请求用户信息。- 应用根据用户信息建立本地 session。
需要分别确认每一步的结果。浏览器看到应用回调接口返回 400,不能说明 IDP 的 UserInfo endpoint 也返回了 400。
对比添加 resource 前后的实际请求,检查:
- authorization URL 中是否包含预期的
resource,值是否为空字符串或undefined。 scope、state、nonce、redirect_uri和 PKCE 参数是否按当前流程正确生成。- token endpoint 是否成功返回 access token。
- UserInfo 请求的 HTTP 状态,以及返回的是用户资料还是错误对象。
不要记录完整的 access token、refresh token、ID token 或 authorization code。应移除原代码中的 console.log({ tokens })。
修正 getUserInfo 的错误处理
可以先沿用现有回调结构,增加检查,让代码在请求或数据出错时直接报错:
getUserInfo: async (tokens) => {
const accessToken = tokens?.accessToken
if (!accessToken) {
throw new Error('Missing access token before UserInfo request')
}
const res = await fetch(
`${process.env.AUTH_IDP_AUTHORITY!}/userinfo`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
},
)
if (!res.ok) {
throw new Error(`UserInfo request failed: HTTP ${res.status}`)
}
const profile = await res.json()
if (typeof profile?.sub !== 'string' || profile.sub.length === 0) {
throw new Error('UserInfo response is missing a valid sub')
}
return {
id: profile.sub,
email: profile.email ?? null,
name: profile.name ?? null,
image: profile.picture ?? null,
customerOrg: profile.customerOrg ?? null,
}
},
这里保留了原来的 /userinfo 地址。应核对 discovery document 中的 userinfo_endpoint,确认它与实际请求地址一致,不能仅根据 issuer 拼接路径。返回字段是否允许 null,也要以所用 Better-Auth 版本的接口要求为准。
这段修改有助于暴露错误原因,但不会自动解决 token 的 audience 或权限问题。
核对 resource 与 token 的用途
authorizationUrlParams 用于向授权请求添加参数,不会把 Better-Auth 的 session token 转换成业务 API 的 access token。
需要分清以下凭据的用途:
- Better-Auth session 凭据用于识别应用中的登录会话。
access_token用于访问其适用的资源服务。id_token用于 OIDC 客户端确认身份,不应直接作为业务 API 的 Bearer token。
resource 和 audience 是否受支持、应该放在 authorization 请求还是 token 请求中,以及是否需要同时传递,都取决于内部 IDP 的实现。两个参数名不能直接互换。
如果 access token 是 JWT,可以在受控环境中检查添加 resource 前后的 iss、aud、有效期和权限声明。解码有助于排查,但不能代替签名及声明校验。如果是 opaque token,则需要通过 IDP 提供的诊断方式检查。
如果确认指定业务资源后,token 无法用于 UserInfo,应按照 IDP 支持的方式分别取得身份资料和业务 API 所需的 token。不要假设一个 access token 能同时访问所有服务,也不要跳过用户身份校验来强行建立 session。
检查 authorizationUrlParams 和配置接口
以下写法是否有效,需要核对已安装版本的类型定义:
authorizationUrlParams: async (ctx) => {
return {
...ctx.params,
resource: process.env.AUTH_IDP_RESOURCE,
}
},
确认该版本是否支持异步函数、回调参数是否包含 params,以及返回值如何与插件生成的 OAuth 参数合并。不要假设 ctx.params 就是完整的授权查询参数,也不要让自定义参数意外覆盖 state、redirect_uri 或 PKCE 参数。
原配置中还有几处需要单独核对:
providerId是'MY id',而redirectURI以/callback/idp结尾。如果它们不是脱敏示例,应确认 provider 标识、插件生成的回调地址和 IDP 登记的地址一致。mapUser返回{ ...res.user }。如果该版本传入的是用户对象本身,读取res.user可能丢失映射结果。需要确认它与getUserInfo的调用关系及参数结构。session.additionalFields中的get是否属于该版本支持的配置,不能仅凭对象写法判断。增加字段声明也不意味着 provider token 会自动进入 session。
session 和刷新逻辑应随后检查
如果用户信息获取失败,session 可能根本没有建立。此时 getBetterAuth() 返回 null 是下游结果,优先修改 proxy.ts 通常无法解决前面的失败。
原来的 refreshAccessToken 也没有真正刷新 token:未过期时返回字符串,过期时返回原有 token 对象,没有向 IDP 的 token endpoint 发出 refresh 请求。应按当前版本的回调契约实现刷新,并处理 IDP 可能返回的新 refresh token。这是后续需要修复的问题,不能据此认定首次登录的 400 已找到原因。
如果 UserInfo 已成功返回、回调也已完成,再检查 cookie 是否写入并随下一次请求发送。当前配置同时出现 HTTP 地址和 secure: true,需要结合浏览器行为及该配置是否实际生效进行确认。另外,localhost 与 127.0.0.1 是不同的 cookie 主机,应统一访问地址。
排查时,先确认授权参数实际生效,再检查 token 返回结果、UserInfo 状态和 sub,最后检查 session、cookie 和账户信息读取。这样可以区分 IDP 拒绝 token、用户映射失败和会话持久化失败。
备注:内容仅供参考。
