OAuth Proxy

开发和预览部署中的 OAuth 登录

OAuth Proxy 适用于本地开发和可信的预览部署,这些环境的回调 URL 会发生变化。它允许这些环境使用在生产服务器上注册了固定回调 URL 的 OAuth 客户端。

生产服务器负责处理提供商回调、交换授权码,并将加密后的用户资料返回给开发或预览部署。该部署会在自己的数据库中创建用户和会话。生产回调服务器以及每个参与此流程的可信开发或预览部署都需要安装此插件。

生产环境中的常规登录应使用正常的 OAuth 提供商流程。当前来源与 productionURL 匹配时,插件会跳过登录请求代理。生产服务器仍会处理参与其中的开发和预览部署发来的代理回调。

安装

在 auth 配置中添加插件

在生产回调服务器以及每个可信的开发或预览部署中添加以下配置,并使用相同的 productionURL 和代理 secret。

仅向你信任其能在所有参与部署(包括生产环境)中验证用户的环境和代码共享代理密钥。持有此密钥的任何人都可以通过代理声明提供商身份。不要将其提供给运行不可信贡献者代码的预览部署。使用不同的 BETTER_AUTH_SECRET 可以保护其他加密数据,但不会消除此共享认证权限。

auth.ts
import { betterAuth } from "better-auth"
import { oAuthProxy } from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    oAuthProxy({ 
      productionURL: "https://my-production-app.com", 
      secret: process.env.OAUTH_PROXY_SECRET, 
    }), 
  ],
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID || "",
      clientSecret: process.env.GITHUB_CLIENT_SECRET || "",
    },
  },
})

在所有可信的参与环境(production、preview、localhost)中将 OAUTH_PROXY_SECRET 设置为相同的值。插件会据此为 OAuth state 传输、代理载荷和提供商资料分别派生加密密钥。

插件会将参与其中的开发和预览部署所发起的 OAuth 请求路由到生产回调服务器。

在你的 OAuth 提供商处添加重定向 URL

在你的 OAuth 提供商的开发者控制台(例如 GitHub、Google)中,使用你的生产域名注册回调 URL。例如:

https://my-production-app.com/api/auth/callback/github

只需注册生产环境的回调 URL。插件会自动处理将预览和开发环境的 OAuth 请求路由到生产环境。

添加可信来源

由于预览和开发服务器会通过生产环境重定向,你需要将它们添加为 trustedOrigins:

auth.ts
export const auth = betterAuth({
  // ...其他配置
  trustedOrigins: [ 
    "http://localhost:3000", 
    "https://my-app-*-preview.example.com", 
  ], 
})

重要:需要共享密钥

所有可信的参与环境(production、preview、localhost)都必须使用相同的代理密钥通信。请在插件选项中配置专用的 secret:

auth.ts
oAuthProxy({
  productionURL: "https://my-production-app.com",
  secret: process.env.OAUTH_PROXY_SECRET, // 所有环境都使用相同的值
})

如果未配置共享的 secret,插件会回退到 BETTER_AUTH_SECRET。由于生产环境和预览环境通常使用不同的主密钥(这符合安全实践),OAuth 流程会因 state_mismatch 错误而失败。即使多个功能使用相同的密钥材料,OAuth 代理载荷和 OAuth state cookie 也会使用分别派生的密钥。

Upgrading existing deployments

oAuthProxy 配置无需更改。保留现有的 productionURL 和共享 secret。此次升级不要求轮换密钥;如果另外进行轮换,所有参与环境仍必须使用相同的值。

请在一次协调切换中,将生产环境以及参与同一 OAuth 代理流程的所有预览或开发部署升级到相同的 Better Auth 版本。不同版本无法交换代理 state 载荷或提供商资料。切换前开始的登录和账户关联流程必须重新启动,无论其 OAuth state 存储在数据库还是 cookie 中。使用 cookie 存储 state 时,还需同时升级为同一 auth base 提供服务的所有节点,因为 state cookie 的加密密钥也会改变。插件不会回退到之前的共享加密密钥。

工作原理

该插件允许你在多个环境中使用单一的 OAuth 客户端(注册于你的生产 URL),例如预览部署或本地开发。

  1. 预览服务器发起 OAuth,重定向至包含生产环境重定向 URI 的 OAuth 提供商
  2. OAuth 提供商回调至生产服务器
  3. 生产服务器用 code 换取令牌并获取用户信息
  4. 生产服务器加密用户信息并重定向回预览服务器(生产环境无数据库写入)
  5. 预览服务器解密用户信息,在自己的数据库创建用户/会话,并设置会话 Cookie
import { authClient } from "@/lib/auth-client"

await authClient.signIn.social({
    provider: "github",
    callbackURL: "/dashboard"
})

加密的用户信息通过 URL 查询参数传递,只有共享相同密钥的服务器能解密。这也允许预览环境使用与生产环境不同的数据库(如果需要)。

Options

productionURL:生产服务器的 URL,由它处理开发和预览环境登录时的提供商回调。当前来源与此 URL 的来源匹配时,登录请求不会经过代理。默认依次使用 BETTER_AUTH_URL 环境变量和 auth baseURL。

currentURL:插件会自动确定应用当前的 URL。它会优先检查请求的 URL、常见托管服务提供商的特定环境变量,最后回退到 auth 配置中的 baseURL。只有在环境中无法正确推断 URL 时才需要手动设置。

maxAge:加密载荷的最大有效期(秒)。超过此时间的载荷将被拒绝以防止重放攻击。保持较短的值(例如 30-60 秒)可以最大程度减少潜在重放攻击的窗口,同时仍允许正常的 OAuth 流程。默认为 60 秒。

secret:用于为 OAuth 代理流程派生密钥的专用密钥。设置后会使用此密钥替代全局 BETTER_AUTH_SECRET,因此泄露代理密钥不会导致仅由主密钥保护的数据被解密。持有代理密钥的人可以向参与部署声明提供商身份,并在身份被接受时获取会话。仅与可信环境和代码共享此密钥。所有参与代理流程的环境都必须使用相同的 secret 值。

故障排查

state_mismatch 或“State not persisted correctly”错误

当生产环境和预览环境使用不同的密钥,并且插件选项中没有配置共享的 secret 时,通常会出现此错误。

发生了什么:

  1. 预览环境使用自己的密钥加密 OAuth state
  2. OAuth 提供商重定向到生产环境
  3. 生产环境尝试使用不同的密钥解密 → 失败
  4. 常规 OAuth 回调运行失败,因为生产环境上不存在 state cookie

解决方案: 在所有环境的 oAuthProxy 选项中配置一个共享的 secret:

auth.ts
oAuthProxy({
  productionURL: "https://my-production-app.com",
  secret: process.env.OAUTH_PROXY_SECRET, 
})

确保生产环境以及参与此流程的每个可信预览或 localhost 部署中的 OAUTH_PROXY_SECRET 值相同。不要将其提供给运行不可信贡献者代码的部署。

使用专用代理密钥(而不是共享 BETTER_AUTH_SECRET)可以将仅由主密钥保护的数据隔离开来,但无法阻止代理密钥持有者通过代理声明提供商身份。请将代理密钥视为所有参与部署通用的认证凭证。

OAuth 在生产环境正常,但在 preview/localhost 上失败

请确保以下所有项都已配置:

  1. 已配置共享密钥(见上文)
  2. 可信来源包含你的 preview/localhost URL
  3. 已在 OAuth 提供商处注册生产回调 URL(例如 https://production.com/api/auth/callback/github)