配置 SAML 单点登录
Set up SAML 2.0 Single Sign-On for self-hosted Supabase with Docker.
SAML 2.0 单点登录(SSO)让你的用户可以通过企业身份提供商(IdP)进行认证,比如 Okta、Azure AD(Entra ID)、Google Workspace,或任何兼容 SAML 2.0 的提供商。和 OAuth 提供商不同,SAML IdP 不是通过环境变量配置的——它们是通过 Auth 管理 API 在运行时动态管理的。
🌐 SAML 2.0 SSO lets your users authenticate through an enterprise Identity Provider (IdP) such as Okta, Azure AD (Entra ID), Google Workspace, or any SAML 2.0-compliant provider. Unlike OAuth providers, SAML IdPs are not configured through environment variables - they are managed dynamically at runtime through the Auth admin API.
本指南涵盖了完整的设置:生成签名密钥、在你的 Supabase 实例中启用 SAML、注册身份提供商(IdP),以及将单点登录(SSO)集成到你的应用中。
🌐 This guide covers the full setup: generating a signing key, enabling SAML in your Supabase instance, registering an IdP, and integrating SSO into your application.
客户端集成使用在 SSO with SAML 2.0 指南中记录的相同 supabase.auth.signInWithSSO() 方法。本指南重点介绍自托管服务器的配置。
🌐 Client-side integration uses the same supabase.auth.signInWithSSO() method documented in the SSO with SAML 2.0 guide. This guide focuses on the self-hosted server configuration.
在你开始之前 #
🌐 Before you begin
你需要:
🌐 You need:
- 一个正在运行的自托管 Supabase 实例(参见 设置指南)
- 已安装 Open SSL(用于生成密钥)
- 你的 IdP 的 SAML 元数据 URL 或元数据 XML
- 你
.env文件中的SERVICE_ROLE_KEY(用于管理员 API 调用) API_EXTERNAL_URL设置为你的 Supabase Auth 服务的公开可访问 URL(例如,https://<your-domain>/auth/v1)。用于构建 SAML 服务提供者实体 ID 和 ACS 端点 URL 的基础
Supabase 中 SAML SSO 的工作原理 #
🌐 How SAML SSO works in Supabase
SAML 单点登录配置了两层:
🌐 SAML SSO is configured in two layers:
- 全局 SAML 启用(环境变量) - 一小组环境变量,用于启用 SAML 引擎并提供签名密钥。这些放在
.env和docker-compose.yml。 - 每个身份提供商的配置(管理员 API) - 通过 Auth 管理 API,可以在运行时注册、更新和删除各个身份提供商。添加或移除提供商时无需重启。
登录流程如下:
🌐 The login flow works as follows:
- 你的应用会使用域名或 provider_id 调用
POST /auth/v1/sso - Auth 生成一个 SAML
AuthnRequest并返回到 IdP 的重定向 URL - 用户在身份提供者处进行身份验证
- 身份提供者会将 SAML 响应通过 POST 发送到
POST /auth/v1/sso/saml/acs - Auth 验证声明,创建或关联用户,并发放会话
- 用户会被重定向回你的应用,并带上会话令牌
步骤1:生成一个RSA私钥 #
🌐 Step 1: Generate an RSA private key
SAML 请求必须被签名。GOTRUE_SAML_PRIVATE_KEY 期望的值是一个 Base64 编码的 PKCS#1 DER RSA 私钥(最少需要 2048 位密钥)。可以用以下方式生成:
🌐 SAML requests must be signed. The value expected by GOTRUE_SAML_PRIVATE_KEY is a Base64-encoded PKCS#1 DER RSA private key (with a 2048-bit key as the minimum requirement). Generate it with:
1openssl genpkey -algorithm RSA -out pk_pkcs8.pem -quiet && \2openssl pkey -in pk_pkcs8.pem -out pk_rsa1.der -outform DER -traditional && \3base64 -w 0 -i pk_rsa1.der上面的命令:
🌐 The commands above:
- 生成一个 PKCS#8 PEM 格式的 RSA 私钥
- 把密钥转换成 PKCS#1 DER 格式
- 将密钥进行 Base64 编码,以用作
GOTRUE_SAML_PRIVATE_KEY的值
保存 Base64 输出——确保以单行复制,忽略末尾的换行。删除临时文件。
🌐 Save the Base64 output - make sure to copy it as single line, ignoring the trailing newline. Remove the temporary files.
保持这个密钥的秘密。任何拥有私钥的人都可以代表你的服务提供商伪造 SAML 请求。不要把它提交到版本控制系统。
🌐 Keep this key secret. Anyone with the private key can forge SAML requests on behalf of your Service Provider. Do not commit it to the version control system.
对于生产部署,可以考虑使用 4096 位密钥,只需在上面的 openssl genpkey 命令中添加 -pkeyopt rsa_keygen_bits:4096。
🌐 For a production deployment, consider using a 4096-bit key by adding -pkeyopt rsa_keygen_bits:4096 to the openssl genpkey command above.
步骤 2:添加环境变量 #
🌐 Step 2: Add environment variables
把以下内容添加到你的 .env 文件中:
🌐 Add the following to your .env file:
1############2# SAML SSO34############56SAML_ENABLED=true7SAML_PRIVATE_KEY=<your-base64-encoded-private-key>89# Optional: accept encrypted SAML assertions from IdPs (default: false)10# SAML_ALLOW_ENCRYPTED_ASSERTIONS=false1112# Optional: how long relay state tokens remain valid (default: 2m0s)13# SAML_RELAY_STATE_VALIDITY_PERIOD=2m0s1415# Optional: rate limit on the ACS endpoint (requests per second, default: 15)16# SAML_RATE_LIMIT_ASSERTION=15步骤 3:将 SAML 变量传递给 Auth 容器 #
🌐 Step 3: Pass SAML variables to the Auth container
在 docker-compose.yml 中,将 SAML 环境变量添加到 auth 服务。Auth 期望所有的配置变量都有 GOTRUE_ 前缀:
🌐 In docker-compose.yml, add the SAML environment variables to the auth service. Auth expects the GOTRUE_ prefix for all of its configuration variables:
1auth:2 environment:3 # ... existing variables ...45 # SAML SSO6 GOTRUE_SAML_ENABLED: ${SAML_ENABLED}7 GOTRUE_SAML_PRIVATE_KEY: ${SAML_PRIVATE_KEY}8 # GOTRUE_SAML_ALLOW_ENCRYPTED_ASSERTIONS: ${SAML_ALLOW_ENCRYPTED_ASSERTIONS}9 # GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD: ${SAML_RELAY_STATE_VALIDITY_PERIOD}10 # GOTRUE_SAML_RATE_LIMIT_ASSERTION: ${SAML_RATE_LIMIT_ASSERTION}步骤4:重启容器 #
🌐 Step 4: Restart the containers
应用配置更改:
🌐 Apply the configuration changes:
1sh run.sh recreate确认认证服务是否正常:
🌐 Verify the Auth service is healthy:
1sh run.sh status auth步骤5:获取你的服务提供商元数据 #
🌐 Step 5: Retrieve your service provider metadata
一旦启用 SAML,你的 Supabase 实例会在 {API_EXTERNAL_URL}/sso/saml/metadata 暴露服务提供者(SP)元数据。
🌐 Once SAML is enabled, your Supabase instance exposes service provider (SP) metadata at {API_EXTERNAL_URL}/sso/saml/metadata.
用 curl 验证一下:
🌐 Verify it using curl:
1curl http://<your-domain>/auth/v1/sso/saml/metadata这会返回一个 XML 文档,里面包含你的 SP 实体 ID、ACS 端点 URL 和签名证书。你需要把这些提供给你的 IdP。
🌐 This returns an XML document containing your SP entity ID, ACS endpoint URL, and signing certificate. You will need to provide this to your IdP.
在请求 URL 中添加 ?download=true,即可将元数据作为有效期为 5 年的可下载 XML 文件获取——这对需要上传文件而不是使用 URL 的身份提供商很有用。
🌐 Add ?download=true to the request URL to get the metadata as a downloadable XML file with a 5-year validity period - this is useful for IdPs that require a file upload instead of a URL.
元数据中的关键值:
🌐 Key values in the metadata:
| 字段 | 值 |
|---|---|
| 实体ID | {API_EXTERNAL_URL}/sso/saml/metadata |
| ACS URL | {API_EXTERNAL_URL}/sso/saml/acs |
| NameID 格式 | persistent, emailAddress |
| 签名证书 | 来源于你的 SAML_PRIVATE_KEY |
第6步:注册一个身份提供者 #
🌐 Step 6: Register an identity provider
使用 Auth 管理 API 注册你的 IdP。你需要 SERVICE_ROLE_KEY 来进行身份验证。
🌐 Use the Auth admin API to register your IdP. You need the SERVICE_ROLE_KEY for authentication.
选项 A:使用元数据网址注册(推荐) #
🌐 Option A: Register with a metadata URL (recommended)
如果你的身份提供者(IdP)提供了元数据 URL,Auth 会自动获取并缓存这些元数据,并在它过期时刷新它:
🌐 If your IdP provides a metadata URL, Auth will fetch and cache the metadata automatically and refresh it when it becomes stale:
1curl -X POST 'http://<your-domain>/auth/v1/admin/sso/providers' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'Content-Type: application/json' \4 -H 'apikey: your-service-role-key' \5 -d '{6 "type": "saml",7 "metadata_url": "https://idp.example.com/saml/metadata",8 "domains": ["example.com"],9 "attribute_mapping": {10 "keys": {11 "email": {12 "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"13 },14 "name": {15 "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"16 }17 }18 }19 }'选项B:使用内联元数据XML注册 #
🌐 Option B: Register with inline metadata XML
如果你有作为 XML 字符串的 IdP 元数据:
🌐 If you have the IdP metadata as an XML string:
1curl -X POST 'http://<your-domain>/auth/v1/admin/sso/providers' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'Content-Type: application/json' \4 -H 'apikey: your-service-role-key' \5 -d '{6 "type": "saml",7 "metadata_xml": "<EntityDescriptor ...>...</EntityDescriptor>",8 "domains": ["example.com"]9 }'使用 metadata_url 时,URL 必须使用 HTTPS。认证会验证元数据的 XML 格式,并检查 EntityID 在所有注册提供者中是否唯一。
🌐 When using metadata_url, the URL must use HTTPS. Auth validates the metadata XML format and checks that the EntityID is unique across all registered providers.
响应包括提供者 id(UUID)——保存它以便在你的应用中使用或以后管理:
🌐 The response includes the provider id (UUID) - save this for use in your application or for later management:
1{2 "id": "d3f5a1b2-...",3 "resource_id": null,4 "disabled": false,5 "saml": {6 "entity_id": "https://idp.example.com/saml",7 "metadata_url": "https://idp.example.com/saml/metadata"8 },9 "domains": [{ "domain": "example.com" }],10 "created_at": "...",11 "updated_at": "..."12}注册参数参考 #
🌐 Registration parameters reference
| 参数 | 必填 | 描述 |
|---|---|---|
type | 是 | 必须是 "saml" |
metadata_url | 以下之一 | IdP 的 SAML 元数据的 HTTPS URL(自动刷新) |
metadata_xml | 以下之一 | 原始 IdP 元数据 XML 字符串 |
domains | 否 | 要关联的电子邮件域数组(例如,["acme.com"])。用于基于域的 SSO 查找。 |
attribute_mapping | 否 | 将 SAML 属性映射到用户声明(参见 属性映射) |
name_id_format | 否 | 请求特定的 NameID 格式:persistent、emailAddress、transient 或 unspecified |
resource_id | 否 | 提供商的自定义外部标识符 |
disabled | 否 | 设置为 true 以注册但禁用提供商 |
第8步:配置你的身份提供者 #
🌐 Step 8: Configure your identity provider
在 IdP 端,创建一个新的 SAML 应用,并使用你的 SP 详细信息进行配置:
🌐 On the IdP side, create a new SAML application and configure it with your SP details:
| IdP 设置 | 值 |
|---|---|
| SP 实体 ID / 受众 | {API_EXTERNAL_URL}/sso/saml/metadata |
| ACS URL / 回复 URL | {API_EXTERNAL_URL}/sso/saml/acs |
| NameID 格式 | persistent(推荐)或 emailAddress |
| 签名证书 | 从 SP 元数据 XML 上传或提供元数据 URL |
特定身份提供商的配置 #
🌐 IdP-specific configuration
Okta 设置:
- 创建一个“SAML 2.0”应用
- 单点登录网址:
{API_EXTERNAL_URL}/sso/saml/acs - 受众 URI(SP 实体 ID):
{API_EXTERNAL_URL}/sso/saml/metadata - 默认中继状态:留空
- 名称 ID 格式:
Persistent
属性映射 #
🌐 Attribute mapping
属性映射让你可以控制 SAML 断言属性如何被转换为 Supabase 用户声明。如果没有提供映射,Auth 会使用合理的默认设置:
🌐 Attribute mapping lets you control how SAML assertion attributes are translated into Supabase user claims. If no mapping is provided, Auth uses sensible defaults:
默认电子邮件检测顺序:
urn:oid:0.9.2342.19200300.100.1.3(LDAP 邮件 OID)http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddresshttp://schemas.xmlsoap.org/claims/EmailAddress- 名为
mail、Mail或email的属性 - 主题名称ID(如果看起来像电子邮件地址)
默认用户ID检测:
urn:oasis:names:tc:SAML:attribute:subject-id属性- 主题 NameID(如果格式是
persistent)
自定义属性映射示例 #
🌐 Custom attribute mapping example
将 IdP 特定属性映射到用户元数据:
🌐 Map IdP-specific attributes to user metadata:
1{2 "attribute_mapping": {3 "keys": {4 "email": {5 "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"6 },7 "name": {8 "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"9 },10 "department": {11 "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department",12 "default": "unknown"13 },14 "groups": {15 "name": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups",16 "array": true17 },18 "role": {19 "names": ["http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "role", "Role"],20 "default": "member"21 }22 }23 }24}每个映射键支持:
🌐 Each mapping key supports:
| 字段 | 描述 |
|---|---|
name | 要查找的主要 SAML 属性名称(与 Name 和 FriendlyName 匹配,不区分大小写) |
names | 按顺序尝试的备用属性名称数组 |
default | 如果断言中没有该属性,则使用的默认值 |
array | 设置为 true 以收集所有值(用于像组这样的多值属性) |
映射的属性存储在用户的 raw_user_meta_data 中,并且可以通过你应用中的 user.user_metadata 使用。
🌐 Mapped attributes are stored in the user's raw_user_meta_data and are available via user.user_metadata in your application.
管理供应商 #
🌐 Managing providers
列出所有提供者 #
🌐 List all providers
1curl 'http://<your-domain>/auth/v1/admin/sso/providers' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'apikey: your-service-role-key'按资源 ID 进行精确匹配筛选:
🌐 Filter by resource ID using exact match:
1curl 'http://<your-domain>/auth/v1/admin/sso/providers?resource_id=my-idp' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'apikey: your-service-role-key'或前缀匹配:
🌐 or prefix match:
1curl 'http://<your-domain>/auth/v1/admin/sso/providers?resource_id_prefix=prod-' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'apikey: your-service-role-key'获取特定的供应商 #
🌐 Get a specific provider
1curl 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'apikey: your-service-role-key'更新供应商 #
🌐 Update a provider
1curl -X PUT 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'Content-Type: application/json' \4 -H 'apikey: your-service-role-key' \5 -d '{6 "domains": ["example.com", "subsidiary.com"],7 "attribute_mapping": {8 "keys": {9 "email": {10 "name": "mail"11 }12 }13 }14 }'禁用供应商(不删除) #
🌐 Disable a provider (without deleting)
1curl -X PUT 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'Content-Type: application/json' \4 -H 'apikey: your-service-role-key' \5 -d '{ "disabled": true }'删除供应商 #
🌐 Delete a provider
1curl -X DELETE 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \2 -H 'Authorization: Bearer your-service-role-key' \3 -H 'apikey: your-service-role-key'客户端集成 #
🌐 Client-side integration
使用 supabase-js #
🌐 Using supabase-js
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient('http://<your-domain>', 'your-anon-key')45// Option 1: SSO by email domain6const { data, error } = await supabase.auth.signInWithSSO({7 domain: 'example.com',8})910// Option 2: SSO by provider ID11const { data, error } = await supabase.auth.signInWithSSO({12 providerId: 'd3f5a1b2-...',13})1415// Redirect the user to the IdP16if (data?.url) {17 window.location.href = data.url18}这两种方法都会返回一个带有 url 属性的对象——将用户重定向到这个 URL,开始在身份提供者处的认证。
🌐 Both methods return an object with a url property - redirect the user to this URL to begin authentication at the IdP.
直接使用 REST API #
🌐 Using the REST API directly
按字段:
🌐 By domain:
1curl -X POST 'http://<your-domain>/auth/v1/sso' \2 -H 'Content-Type: application/json' \3 -H 'apikey: your-anon-key' \4 -d '{5 "domain": "example.com",6 "skip_http_redirect": true7 }'按提供者ID:
🌐 By provider ID:
1curl -X POST 'http://<your-domain>/auth/v1/sso' \2 -H 'Content-Type: application/json' \3 -H 'apikey: your-anon-key' \4 -d '{5 "provider_id": "d3f5a1b2-...",6 "skip_http_redirect": true7 }'两者都返回 { "url": "https://idp.example.com/sso?SAMLRequest=..." }。
🌐 Both return { "url": "https://idp.example.com/sso?SAMLRequest=..." }.
基于域名的查找 vs 基于提供商的查找 #
🌐 Domain-based vs provider-based lookup
| 方法 | 使用场景 |
|---|---|
domain | 从用户的邮箱中提取域名,让 Auth 找到合适的身份提供商。最适合用户先输入邮箱的登录表单。 |
providerId | 当你确切知道提供商时使用——例如,一个专用的 “用 Okta 登录” 按钮。 |
测试登录流程 #
🌐 Test the login flow
- 打开你的应用并触发单点登录(或者使用上面的 curl 命令)
- 你应该会被重定向到你的身份提供者的登录页面
- 认证之后,IdP 会把信息回传到 ACS 端点
- Auth 会处理断言,然后带着会话令牌把你重定向回你的
SITE_URL(或redirect_toURL)
要验证会话是否已创建:
🌐 To verify the session was created:
1curl 'http://<your-domain>/auth/v1/user' \2 -H 'Authorization: Bearer user-session-token' \3 -H 'apikey: your-anon-key'回复应该包括 app_metadata.provider: "sso:saml" 和 user_metadata 中的任何映射属性。
🌐 The response should include app_metadata.provider: "sso:saml" and any mapped attributes in user_metadata.
环境变量引用 #
🌐 Environment variable reference
| 变量 | 默认值 | 描述 |
|---|---|---|
SAML_ENABLED | false | 启用 SAML SSO 引擎 |
SAML_PRIVATE_KEY | - | Base64 编码的 PKCS#1 RSA 私钥(最少 2048 位)。用于签署 SAML 请求,也可以选择解密断言。 |
SAML_ALLOW_ENCRYPTED_ASSERTIONS | false | 接受来自 IdP 的加密 SAML 断言 |
SAML_RELAY_STATE_VALIDITY_PERIOD | 2m0s | 中继状态令牌的有效时间。如果用户在慢速网络上在 IdP 重定向期间超时,可以增加该值。 |
SAML_RATE_LIMIT_ASSERTION | 15 | 最大每秒 ACS 请求数。用于防止断言重放攻击。 |
故障排除 #
🌐 Troubleshooting
这台服务器没有启用 SAML #
🌐 "SAML is not enabled on this server"
GOTRUE_SAML_ENABLED 变量没有设置为 true,或者 Auth 容器没有检测到更改。确认环境变量通过 docker-compose.yml 传递,并重启:
🌐 The GOTRUE_SAML_ENABLED variable is not set to true, or the Auth container did not pick up the change. Verify the env var is passed through docker-compose.yml and restart:
1sh run.sh recreate启动时显示“无效的私钥” #
🌐 "Invalid private key" on startup
GOTRUE_SAML_PRIVATE_KEY 的值格式不正确。请确保它是:
🌐 The GOTRUE_SAML_PRIVATE_KEY value is malformed. Ensure it is:
- Base64 编码(单行,无换行)
- 在 PKCS#1 格式(
openssl pkey ... -traditional输出) - 至少 2048 位 RSA
如有需要,请重新生成:
🌐 Regenerate if needed:
1openssl genpkey -algorithm RSA -out pk_pkcs8.pem -quiet && \2openssl pkey -in pk_pkcs8.pem -out pk_rsa1.der -outform DER -traditional && \3base64 -w 0 -i pk_rsa1.der身份提供者无法访问 ACS 端点 #
🌐 IdP cannot reach the ACS endpoint
- 确认
API_EXTERNAL_URL设置为一个包含/auth/v1的 URL,IdP 能访问到(除非本地测试,否则不要用localhost) - 检查 API 网关的
/auth/v1/sso/saml/acs和/auth/v1/sso/saml/metadata路由是否配置为开放(没有key-auth插件)。 - 查看 Auth 容器日志:
docker compose logs auth
未找到此域的 SSO 提供商 #
🌐 "No SSO provider found for this domain"
- 确认域名是否已注册:列出提供商并检查
domains数组 - 域名匹配是精确且不区分大小写的 -
Example.com匹配example.com
断言验证失败 #
🌐 Assertion validation fails
- 确保 IdP 的签名证书与在 Auth 注册的元数据中的一致
- 如果使用
metadata_url,身份验证会自动刷新过期的元数据(在ValidUntil、CacheDuration或 24 小时后)。通过更新提供者可以强制刷新。 - 检查你的服务器和 IdP 之间的时钟同步——SAML 断言有基于时间的有效窗口(
NotBefore/NotOnOrAfter)
用户已创建,但属性缺失 #
🌐 User is created but attributes are missing
- 检查你的
attribute_mapping配置。使用 IdP 的 SAML 断言查看工具(大多数 IdP 都有)来查看发送的确切属性名称。 - 属性名称在断言中会不区分大小写地与
Name和FriendlyName字段匹配。 - 映射的属性出现在
user.user_metadata中。
中继状态已过期 #
🌐 Relay state expired
用户在启动单点登录(SSO)和在身份提供商(IdP)完成认证之间花的时间太长。增加 GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD(默认是 2 分钟)。
🌐 The user took too long between initiating SSO and completing authentication at the IdP. Increase GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD (default is 2 minutes).
额外资源 #
🌐 Additional resources
- 使用 SAML 2.0 的单点登录 (SSO) - 客户端 SAML 集成指南
- 认证服务器配置参考 - 完整的认证环境变量列表
- SAML 2.0 规范 - 基础标准