基于Supabase实现企业SSO接入

更新时间:
复制为 MD 格式

云原生数据仓库AnalyticDB PostgreSQL深度集成开源Supabase身份认证系统,支持企业通过配置SAML 2.0单点登录(SSO)接入,满足企业场景的使用需求,尤其适合构建企业私域的Coding Agent平台。本文介绍如何为Supabase项目配置SAML SSO,实现企业用户的统一身份认证。

前提条件

  • 创建Supabase项目。创建时间需晚于以下日期:

    • 中国地域:20260401日。

    • 新加坡:20250401日。

  • 如果您的SSO身份提供商(IdP)和Supabase不在同一个VPC,需为Supabase开通公网访问

操作步骤

步骤一:获取Supabase项目信息

开通Supabase项目后,可获取以下Supabase信息项。

信息项

说明

示例

Supabase URL

Supabase项目的访问地址。

https://spb-xxxxx.supabase.example.com

Anon Key(公钥)

可在前端代码中使用的公开密钥。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Service Role Key(密钥)

仅限后端使用的密钥,用于管理操作,切勿泄露到前端代码中。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

验证项目状态

curl 'https://<your-supabase-url>/auth/v1/health' \
  -H 'apikey: <your-anon-key>'

返回示例

{"version":"vunspecified","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}

步骤二:获取SP元数据

SAML服务提供商(SP)的元数据可通过以下地址公开访问:

https://<your-supabase-url>/sso/saml/metadata

您需要用到以下两个关键地址,在后续步骤中将其配置到IdP中:

地址类型

ACS URL

https://<your-supabase-url>/sso/saml/acs

Entity ID

https://<your-supabase-url>/sso/saml/metadata

步骤三:配置重定向URL

  1. 登录Supabase Dashboard,在Dashboard侧边栏单击Authentication>URL Configuration,进入Site URL配置页面。

  2. Site URL区域,填入前端域名地址,然后单击Save changes保存。

步骤四:注册身份提供商(IdP)

使用Management API将您的IdP注册到Supabase。

方式一:通过Metadata URL注册(推荐)

如果您的IdP提供公开的Metadata URL(大多数IdP都支持),运行以下命令:

curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "saml",
    "metadata_url": "https://<your-idp>/samlp/metadata/<client-id>",
    "domains": ["yourdomain.com"]
  }'

方式二:通过Metadata XML注册

如果Metadata URL不可公开访问,可直接提供XML内容:

# 1. 在可访问IdP的环境中获取Metadata XML
curl 'https://<your-idp>/samlp/metadata/<client-id>' > idp-metadata.xml

# 2. 注册到Supabase
curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "saml",
    "metadata_xml": "<EntityDescriptor ...>...</EntityDescriptor>",
    "domains": ["yourdomain.com"]
  }'

Example:Auth0注册示例

Auth0Metadata URL格式为:

https://<tenant>.auth0.com/samlp/metadata/<client-id>

完整注册命令:

curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "saml",
    "metadata_url": "https://dev-xxx.auth0.com/samlp/metadata/YOUR_CLIENT_ID",
    "domains": ["example.com"]
  }'

保存返回的Provider ID

重要

请保存返回结果中的id字段,前端发起登录时需要用到。

注册成功后返回示例:

{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "type": "saml",
  "saml": {
    "entity_id": "urn:dev-xxx.auth0.com"
  },
  "domains": [{"domain": "example.com"}]
}

步骤五:在IdP侧配置SP信息

注册完成后,需要在您的IdP中配置SupabaseSP信息。

需要填写的配置项

配置项

ACS URL(回调地址)

https://<your-supabase-url>/sso/saml/acs

Entity ID(SP实体ID)

https://<your-supabase-url>/sso/saml/metadata

NameID Format

emailAddress

前端回调URL

https://<your-app-domain>

Auth0为例的配置步骤

  1. 登录Auth0 Dashboard

  2. 进入Applications,单击目标应用。

  3. 单击Addons页签,启用SAML2 Web App

  4. 在弹出窗口中完成以下配置:

    • Application Callback URL

    • Settings

  5. 单击Enable,然后单击Save

  6. 在应用主页的Allowed Callback URLs中,添加您的前端地址。

步骤六:前端集成

  1. 安装依赖。

    npm install @supabase/supabase-js
  2. 初始化客户端。SAML SSO必须使用implicit flow。

    import { createClient } from '@supabase/supabase-js'
    
    const supabase = createClient(
      'https://<your-supabase-url>',
      '<your-anon-key>',
      {
        auth: {
          flowType: 'implicit',
          detectSessionInUrl: true,
          persistSession: true,
          autoRefreshToken: true,
        }
      }
    )
  3. 发起SSO登录。

    • 通过Provider ID发起登录:

      const { data, error } = await supabase.auth.signInWithSSO({
        providerId: '<your-provider-id>',
        options: {
          redirectTo: window.location.origin
        }
      })
      
      if (error) {
        console.error('登录失败:', error.message)
        return
      }
      
      if (data?.url) {
        window.location.href = data.url
      }
    • 通过域名自动匹配Provider:

      const { data, error } = await supabase.auth.signInWithSSO({
        domain: 'yourdomain.com'
      })
  4. 处理登录回调。

    IdP认证完成后会重定向回前端,URL格式为:

    https://<your-app-domain>/#access_token=<jwt>&refresh_token=<token>&expires_in=3600&token_type=bearer

    由于supabase-js的初始化是异步的,建议手动解析URL hash:

    function decodeJWT(token) {
      try {
        return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))
      } catch {
        return null
      }
    }
    
    async function init() {
      const hash = window.location.hash.substring(1)
      if (hash.includes('access_token')) {
        const params = new URLSearchParams(hash)
        const accessToken = params.get('access_token')
        const refreshToken = params.get('refresh_token')
        const expiresAt = params.get('expires_at')
        
        window.history.replaceState({}, '', window.location.pathname)
        
        const payload = decodeJWT(accessToken)
        if (payload) {
          const session = {
            access_token: accessToken,
            refresh_token: refreshToken || '',
            expires_at: expiresAt ? parseInt(expiresAt) : payload.exp,
            token_type: 'bearer',
            user: {
              id: payload.sub,
              email: payload.email,
              role: payload.role,
              app_metadata: payload.app_metadata || {},
              user_metadata: payload.user_metadata || {},
            }
          }
          onLoginSuccess(session)
          return
        }
      }
    
      const { data: { session } } = await supabase.auth.getSession()
      if (session) {
        onLoginSuccess(session)
      } else {
        showLoginPage()
      }
    }
    
    document.addEventListener('DOMContentLoaded', init)
  5. 查看JWT Payload结构。

    登录成功后,JWT解码后包含以下关键字段:

    {
      "sub": "8c308927-4ba1-4507-b6f8-f751b791****",
      "email": "user@example.com",
      "role": "authenticated",
      "aal": "aal1",
      "amr": [{
        "method": "sso/saml",
        "timestamp": 1774928127,
        "provider": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }],
      "app_metadata": {
        "provider": "sso:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      },
      "session_id": "aeaf9cdb-dbcd-46ab-95a5-ff956e69****"
    }

    通过amr[0].method === 'sso/saml'可确认用户通过SSO登录。

完整示例项目

以下是一个可直接运行的最小化示例。在线Demo,请参见sso-demo

  • main.js

    import { createClient } from '@supabase/supabase-js'
    
    const SUPABASE_URL = 'https://<your-supabase-url>'
    const SUPABASE_ANON_KEY = '<your-anon-key>'
    const AUTH0_PROVIDER_ID = '<auth0-provider-id>'
    
    const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
      auth: { flowType: 'implicit', detectSessionInUrl: true }
    })
    
    function decodeJWT(token) {
      try {
        return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))
      } catch { return null }
    }
    
    window.signInWithAuth0 = async () => {
      const { data, error } = await supabase.auth.signInWithSSO({
        providerId: AUTH0_PROVIDER_ID,
        options: { redirectTo: window.location.origin }
      })
      if (error) return alert(error.message)
      if (data?.url) window.location.href = data.url
    }
    
    window.signOut = async () => {
      await supabase.auth.signOut()
      window.location.reload()
    }
    
    async function init() {
      const hash = window.location.hash.substring(1)
      
      if (hash.includes('access_token')) {
        const p = new URLSearchParams(hash)
        const token = p.get('access_token')
        const payload = decodeJWT(token)
        window.history.replaceState({}, '', window.location.pathname)
        
        if (payload) {
          showUser({ token, payload, refresh_token: p.get('refresh_token') })
          return
        }
      }
      
      const { data: { session } } = await supabase.auth.getSession()
      if (session) {
        showUser({ token: session.access_token, payload: decodeJWT(session.access_token) })
      }
    }
    
    function showUser({ token, payload }) {
      document.getElementById('login').style.display = 'none'
      document.getElementById('user').innerHTML = `
        <p><strong>邮箱:</strong>${payload.email}</p>
        <p><strong>用户 ID:</strong>${payload.sub}</p>
        <p><strong>认证方式:</strong>${payload.amr?.[0]?.method}</p>
        <button onclick="signOut()">退出登录</button>
      `
    }
    
    document.addEventListener('DOMContentLoaded', init)
  • index.html

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>SSO Login</title>
    </head>
    <body>
      <div id="login">
        <button onclick="signInWithAuth0()">使用 Auth0 登录</button>
      </div>
      <div id="user"></div>
      <script type="module" src="/main.js"></script>
    </body>
    </html>

基于上述代码可以实现Auth0SSO登录。

SSO登录页面

SSO登录成功

您可以在Supabaseauth.users表中查看SSO用户记录,其is_sso_user字段标记为TRUE

auth.users表中的SSO用户记录

管理Provider

您可以通过下面的接口新增或者删除SSO Provider,以下操作均需使用Service Role Key。

查询Provider列表

curl 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>'

您也可以通过Supabase Dashboard查看auth.sso_domainsauth.sso_providers表,确认已注册的SSO Provider。

Dashboard中查看SSO Provider

查询单个Provider

curl 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>'

更新Provider

curl -X PUT 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{"metadata_url": "https://<your-idp>/samlp/metadata/<client-id>"}'

删除Provider

curl -X DELETE 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>'

常见问题

单击登录按钮后没有跳转

确认createClient中设置了flowType: 'implicit'。默认的PKCE flowSAML回调场景下可能导致问题。

回调后仍显示登录页

不要仅依赖supabase.auth.getSession()处理回调。SDK的初始化是异步的,请在DOMContentLoaded时手动解析URL hash中的access_token

登录失败:No such SSO provider

  1. 检查前端代码中的providerId是否与注册时返回的ID一致。

  2. 强制刷新浏览器(Cmd+Shift+R / Ctrl+Shift+R)清除缓存。

登录失败:Failed to fetch

  1. 检查网络是否能访问Supabase项目。

  2. 在浏览器控制台执行测试命令验证连通性。

登录后跳回错误的地址

signInWithSSO()options.redirectTo中明确指定前端地址,该参数会覆盖实例默认的Site URL。

SAML RelayState has expired

SSO登录URL有时效性(通常几分钟)。请勿复用旧的登录URL,每次单击登录按钮时应重新调用signInWithSSO()

Auth0报错:invalid_request

检查Auth0Allowed Callback URLs是否包含您的前端域名,Application Callback URL是否为SupabaseACS URL。