OAuth 代理

Better Auth 的 OAuth 代理插件

一个允许你代理 OAuth 请求的代理插件。在开发环境和预发布部署中非常有用,因为重定向 URL 无法提前确定并添加到 OAuth 提供商。

安装

在你的 auth 配置中添加此插件

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 || "",
    },
  },
})

OAUTH_PROXY_SECRET 在所有环境(production、preview、localhost)中设置为相同的值。

该插件会自动将 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 客户端(注册于你的生产 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 查询参数传递,只有共享相同密钥的服务器能解密。这也允许预览环境使用与生产环境不同的数据库(如果需要)。

此插件仅适用于开发和预发布环境。如果 baseURLproductionURL 相同,插件不会代理请求。

选项

productionURL:你的生产服务器的 URL。如果此值与 auth 配置中的 baseURL 相匹配,请求将不会被代理。默认为 BETTER_AUTH_URL 环境变量。

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, 
})

确保 OAUTH_PROXY_SECRET 在 production、preview 和 localhost 上的值相同。

建议使用专用的代理密钥(而不是共享 BETTER_AUTH_SECRET),这样更安全。如果代理密钥泄露,攻击者无法伪造会话或访问其他加密数据——他们最多只能在较短的 maxAge 窗口内劫持 OAuth 流程。

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

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

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