PAYATHON 2026

如何通过 Express 透明代理 Supabase auth v1,实现 OAuth 白标品牌展示?

技术老齐

Express 可以转发 Supabase Auth 请求,并保持 OAuth、PKCE 流程完整。不过,仅靠改写 Location,无法可靠地让 Google/X 显示自定义域名。

要让 OAuth 回调使用自定义域名,Supabase Auth 必须认可这个域名,并用它生成回调地址;身份提供方也必须登记同一个地址。如果托管项目仍以 xyz.supabase.co 作为对外认证地址,反向代理只能改变请求入口,无法保证整个认证流程都使用 api.mydomain.com。Google/X 最终显示什么名称和域名,还取决于各自的应用品牌配置和验证规则。

先区分两个回调地址

一次 Supabase OAuth 登录通常经过以下路径:

应用
  → Supabase /auth/v1/authorize
  → Google/X 授权页面
  → Supabase /auth/v1/callback
  → 应用自己的登录回调页面

其中有两个容易混淆的回调地址:

地址用途
https://xyz.supabase.co/auth/v1/callback身份提供方把授权结果交给 Supabase
https://app.mydomain.com/auth/callbackSupabase 完成处理后,把用户送回应用

signInWithOAuth() 的 options.redirectTo 控制第二个地址。修改它或 Supabase 的 Site URL,都不能直接把身份提供方使用的 /auth/v1/callback 换成自定义域名。即使浏览器从 api.mydomain.com 发起登录,Google/X 收到的 redirect_uri 也未必使用这个域名。

为什么字符串替换容易出问题

Supabase 返回的 Location 可能指向身份提供方,同时在查询参数中携带 Supabase 回调地址。例如,下面的 URL 为便于阅读省略了编码:

https://accounts.google.com/...?
  client_id=...
  &redirect_uri=https://xyz.supabase.co/auth/v1/callback
  &state=...

对它执行这样的字符串替换,可能会改到嵌套的 redirect_uri:

location.replace('xyz.supabase.co', 'api.mydomain.com')

URL 编码不一定会隐藏域名中的普通字符。替换后要让登录正常完成,需要同时满足以下条件:

  • 身份提供方已登记新的回调地址。
  • Supabase 能通过新地址接收回调,并恢复登录上下文。
  • Supabase 向身份提供方交换授权码时,使用与授权请求一致的 redirect_uri。

仅修改浏览器收到的响应头,无法保证最后一个条件成立。因此,授权页面可能正常打开,随后却报 redirect_uri_mismatch 或授权码交换失败。

OAuth 流程跳转到 Google/X 是正常行为。排查时应检查其中的回调地址,不必要求每次跳转都停留在代理域名下。

changeOrigin 和 xfwd 分别解决什么问题

配置作用使用边界
changeOrigin: true将发给上游的 HTTP Host 改为目标主机访问托管 Supabase 时通常应保留目标项目的 Host
secure: true校验上游 HTTPS 证书应保持开启;它不控制 OAuth 校验
xfwd: true添加 X-Forwarded-* 请求头上游是否信任、如何使用这些头,由上游配置决定

changeOrigin 不会修改 OAuth 参数中的 redirect_uri,也不会自动重写浏览器的 Origin。

开启 xfwd 同样不能自动启用自定义域名。即使发送以下请求头,托管 Supabase 仍可能使用预设的对外认证地址,或者拒绝不被认可的主机信息:

X-Forwarded-Host: api.mydomain.com
X-Forwarded-Proto: https

服务端仍需配置域名,不能靠伪造转发头来替代。

如果 Express 前面还有负载均衡器,需要限定可信代理,并在入口清理、重建转发头。Express 的 trust proxy 与代理中间件的 xfwd 是两套配置,不能互相替代。

Express 中可以怎样处理重定向

如果只是为了统一 API 入口,可以仅改写目标为 Supabase 上游的重定向,保留身份提供方 URL 及其查询参数。

下面的示例使用 http-proxy-middleware v3 配置。旧版本的 onProxyRes、logLevel 写法不能直接与这套 API 混用。

import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

const upstream = new URL('https://xyz.supabase.co');
const publicOrigin = new URL('https://api.mydomain.com');

const supabaseProxy = createProxyMiddleware({
  target: upstream.origin,

  // 挂在根路径,保留 /auth/v1、/rest/v1 前缀。
  pathFilter: (pathname) =>
    ['/auth/v1', '/rest/v1'].some(
      (prefix) =>
        pathname === prefix || pathname.startsWith(`${prefix}/`)
    ),

  changeOrigin: true,
  secure: true,
  followRedirects: false,

  // 此例使用托管项目原有的上游身份,不通过转发头声明新域名。
  xfwd: false,

  on: {
    proxyReq(proxyReq) {
      // xfwd: false 不会主动清除客户端已经提交的转发头。
      for (const name of [
        'forwarded',
        'x-forwarded-for',
        'x-forwarded-host',
        'x-forwarded-proto',
        'x-forwarded-port',
      ]) {
        proxyReq.removeHeader(name);
      }
    },

    proxyRes(proxyRes) {
      const location = proxyRes.headers.location;
      if (typeof location !== 'string') return;

      try {
        const destination = new URL(location, upstream.origin);

        // 只修改最外层 URL,不替换查询参数中的其他 URL。
        if (destination.origin === upstream.origin) {
          destination.protocol = publicOrigin.protocol;
          destination.host = publicOrigin.host;
          proxyRes.headers.location = destination.href;
        }
      } catch {
        // 无法解析时保留上游响应。
      }
    },
  },
});

// 放在 express.json() 等读取请求体的中间件之前。
app.use(supabaseProxy);

这段代码保留请求路径,只在限定范围内改写重定向,同时保留身份提供方 URL 中的原始 redirect_uri,不能单独实现 OAuth 白标。

示例没有转发真实客户端 IP。如果业务依赖该信息做限流或审计,应在可信入口生成相应请求头,不能直接信任客户端提交的值。部署时还需要配置对外 HTTPS 和跨域访问策略。

代理会不会破坏 PKCE 和 state

正常转发不会破坏 PKCE。PKCE 校验的是 code_verifier 与先前提交的 code_challenge 是否对应,不要求请求直连某个域名。

配置时要注意 flowType 的大小写:

const supabase = createClient(
  'https://api.mydomain.com',
  SUPABASE_PUBLISHABLE_KEY,
  {
    auth: {
      flowType: 'pkce',
      storageKey: 'myapp-auth',
    },
  }
);

await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://app.mydomain.com/auth/callback',
  },
});

正确名称是 flowType,不是 flowtype。这里的 storageKey 用于显式固定客户端存储名称;如果已有线上会话,修改它时需要考虑迁移。

代理和客户端需要满足以下要求:

  • 发起登录和处理应用回调时,能够读取同一个 PKCE verifier。浏览器存储受 origin 限制;SSR 则需要正确传递相应 Cookie。
  • 代理完整保留请求方法、请求体、查询参数和认证头,尤其是授权码交换使用的 /auth/v1/token 请求。
  • state 按原值传递,不要解码后重新拼接、替换其中的域名或自行生成新值。

这里还有两段授权码交换需要区分:一段是身份提供方向 Supabase 返回授权码,另一段是 Supabase 向应用返回供 PKCE 会话交换使用的授权码。应用端启用 PKCE,无法修复前一段的回调地址不一致问题。

如果使用 Cookie,还需检查 Domain、Path、Secure 和 SameSite。使用浏览器本地存储的客户端则应重点检查 verifier 的保存位置,不能只排查 Cookie。

按这个顺序检查配置

先在浏览器开发者工具中保留网络日志,找到前往 Google/X 的授权请求,检查解码后的 redirect_uri。

如果它仍然是下面的地址,说明 Supabase 还在使用项目原域名:

https://xyz.supabase.co/auth/v1/callback

此时继续调整普通的 Location 改写,无法可靠解决品牌展示问题。

如果准备使用以下回调地址,需要同时完成后面的配置:

https://api.mydomain.com/auth/v1/callback
  1. 让 Supabase Auth 认可该域名,并据此生成回调地址。
  2. 在身份提供方登记完全匹配的回调 URI,包括协议、主机、路径和尾部斜杠。
  3. 将应用的 redirectTo 地址加入 Supabase 的 Redirect URLs 允许列表。
  4. 检查完整流程,确认应用成功交换会话,并验证刷新会话是否正常。

排查时避免把完整授权码、state、verifier 或 token 写入日志。

不想为自定义域名升级,可以考虑哪些方案

如果主要希望 Google 页面显示应用名称和 Logo,可以先完善 Google OAuth 应用的品牌信息,并完成所需验证。具体页面显示应用名称、回调域名还是其他文案,由 Google 决定。X 也有自己的规则,无法保证两者展示一致。

Google 登录还可以考虑另一种接入方式:在自有站点通过 Google Identity Services 获取 ID token,再调用 Supabase 的 signInWithIdToken() 建立会话。需要正确匹配 Google Client ID,并按所选流程处理 nonce。这会改变登录接入方式,也不能直接套用到 X。

如果必须控制认证服务的公开域名,可以考虑自托管 Supabase/Auth,在部署端配置对外地址,同时承担升级、密钥、邮件和认证安全等运维工作。若只迁移 Auth、保留托管数据库,还必须解决 JWT 信任关系与 RLS 用户身份映射,不能假设新服务签发的 token 可以直接使用。

Express BFF 也可以自行管理 OAuth 和应用会话,但这涉及认证架构调整。如果 BFF 仍然调用同一个托管 Supabase 的 signInWithOAuth(),回调域名问题依然存在。

继续使用托管 Supabase Auth 时,受支持的自定义域名机制通常更容易维护。具体套餐要求和附加费用以控制台为准,不能假设升级 Pro 就自动包含这项能力。

备注:内容仅供参考。