OAuth 2.1 服务器入门
本指南将带你一步步设置你的 Supabase 项目成为 OAuth 2.1 身份提供者,从启用该功能到注册你的第一个客户端应用。
🌐 This guide will walk you through setting up your Supabase project as an OAuth 2.1 identity provider, from enabling the feature to registering your first client application.
先决条件 #
🌐 Prerequisites
在开始之前,确保你有:
🌐 Before you begin, make sure you have:
- 一个 Supabase 项目(在 supabase.com 创建一个)
- 你项目的管理员权限
- (可选) 本地开发请使用 Supabase CLI v2.54.11 或更高版本
概览 #
🌐 Overview
在你的 Supabase 项目中设置 OAuth 2.1 包括以下步骤:
🌐 Setting up OAuth 2.1 in your Supabase project involves these steps:
- 在你的项目中启用 OAuth 2.1 服务器功能
- 配置你的授权路径
- 搭建你的授权界面(前端)
- 注册 OAuth 客户端应用
在 Supabase 项目上测试 OAuth 流程通常更容易,因为它已经可以在网上访问,不需要隧道或额外的配置。
🌐 Testing OAuth flows is often easier on a Supabase project since it's already accessible on the web, no tunnel or additional configuration needed.
启用 OAuth 2.1 服务器 #
🌐 Enable OAuth 2.1 server
OAuth 2.1 服务器目前处于测试版,在测试期间,所有 Supabase 计划都可以免费使用。
🌐 OAuth 2.1 server is currently in beta and free to use during the beta period on all Supabase plans.
- 去你的项目仪表板
- 在侧边栏中导航到 身份验证 > OAuth 服务器
- 启用 OAuth 2.1 服务器功能
一旦启用,你的项目将会开放必要的 OAuth 端点:
🌐 Once enabled, your project will expose the necessary OAuth endpoints:
| 端点 | URL |
|---|---|
| 授权端点 | https://<project-ref>.supabase.co/auth/v1/oauth/authorize |
| 令牌端点 | https://<project-ref>.supabase.co/auth/v1/oauth/token |
| JWKS 端点 | https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json |
| 发现端点 | https://<project-ref>.supabase.co/.well-known/oauth-authorization-server/auth/v1 |
| OIDC 发现 | https://<project-ref>.supabase.co/auth/v1/.well-known/openid-configuration |
使用非对称 JWT 签名密钥以提高安全性
默认情况下,Supabase 使用 HS256(对称)来签署 JWT。对于 OAuth 使用场景,我们建议迁移到像 RS256 或 ES256 这样的非对称算法。非对称密钥更具可扩展性和安全性,因为:
🌐 By default, Supabase uses HS256 (symmetric) for signing JWTs. For OAuth use cases, we recommend migrating to asymmetric algorithms like RS256 or ES256. Asymmetric keys are more scalable and secure because:
- OAuth 客户端可以使用你 JWKS 端点的公钥来验证 JWT
- 不需要和第三方应用分享你的 JWT 秘密
- 分布式系统更有韧性的架构
了解更多关于配置 JWT 签名密钥的信息。
🌐 Learn more about configuring JWT signing keys.
注意: 如果你打算使用 OpenID Connect ID 令牌(通过请求 openid 范围),则必须使用非对称签名算法。使用 HS256 会导致 ID 令牌生成失败。
配置你的授权路径 #
🌐 Configure your authorization path
在注册客户之前,你需要先配置你的授权界面放在哪儿。
🌐 Before registering clients, you need to configure where your authorization UI will live.
- 在你的项目仪表板中,导航到 身份验证 > OAuth 服务器
- 设置授权路径(例如:
/oauth/consent)
授权路径会与你的网站 URL(在 身份验证 > URL 配置 中设置)结合,从而生成完整的授权端点 URL。
🌐 The authorization path is combined with your Site URL (configured in Authentication > URL Configuration) to create the full authorization endpoint URL.
你的授权界面将在合并后的站点网址 + 授权路径。例如:
🌐 Your authorization UI will be at the combined Site URL + Authorization Path. For example:
- 站点网址:
https://example.com(在 认证 > 网址配置 中) - 授权路径:
/oauth/consent(来自OAuth 服务器设置) - 你的授权界面:
https://example.com/oauth/consent
当 OAuth 客户端启动授权流程时,Supabase Auth 会将用户重定向到这个 URL,并附带一个 authorization_id 查询参数。你可以使用 Supabase JavaScript 库的 OAuth 方法 来处理授权:
🌐 When OAuth clients initiate the authorization flow, Supabase Auth will redirect users to this URL with an authorization_id query parameter. You'll use Supabase JavaScript library OAuth methods to handle the authorization:
supabase.auth.oauth.getAuthorizationDetails(authorization_id)- 获取客户端和授权详情supabase.auth.oauth.approveAuthorization(authorization_id)- 批准授权请求supabase.auth.oauth.denyAuthorization(authorization_id)- 拒绝授权请求
搭建你的授权界面 #
🌐 Build your authorization UI
这里是你为授权流程构建前端的地方。当第三方应用启动 OAuth 时,用户会被重定向到你在上一步配置的授权路径,并带有一个 authorization_id 查询参数。
🌐 This is where you build the frontend for your authorization flow. When third-party apps initiate OAuth, users will be redirected to your authorization path (configured in the previous step) with an authorization_id query parameter.
你的授权界面应该:
🌐 Your authorization UI should:
- 提取 authorization_id - 从 URL 查询参数中获取
authorization_id - 验证用户 - 如果尚未登录,请重定向到你的登录页面(保留 authorization_id)
- 获取授权详细信息 - 使用
supabase.auth.oauth.getAuthorizationDetails(authorization_id)获取客户端信息,包括请求的权限范围 - 显示同意屏幕 - 向用户展示哪个应用在请求访问权限,以及正在请求哪些权限/范围
- 处理用户决定 - 根据用户选择调用
approveAuthorization(authorization_id)或denyAuthorization(authorization_id)
授权详情包括一个 scope 字段(单数),其中包含客户端请求的以空格分隔的作用域字符串(例如,"openid email profile")。你应该向用户显示这些作用域,这样他们就能明白会共享哪些信息。
🌐 The authorization details include a scope field (singular) containing a space-separated string of scopes requested by the client (e.g., "openid email profile"). You should display these scopes to the user so they understand what information will be shared.
这是一个前端实现。你在构建显示同意屏幕的用户界面并处理用户交互。实际的 OAuth 令牌生成是由 Supabase Auth 处理的,在你调用批准/拒绝方法之后。
🌐 This is a frontend implementation. You're building the UI that displays the consent screen and handles user interactions. The actual OAuth token generation is handled by Supabase Auth after you call the approve/deny methods.
示例授权界面 #
🌐 Example authorization UI
下面是如何在你配置的路径(例如 /oauth/consent)上创建一个最小化授权页面:
🌐 Here's how to build a minimal authorization page at your configured path (e.g., /oauth/consent):
Supabase Auth SDK 包含三种不同的函数,用于验证用户对应用的访问权限:
🌐 The Supabase Auth SDK contains three different functions for authenticating user access to applications:
方法总结 #
🌐 Summary of the methods
- 使用
getClaims来保护页面和用户数据。它会从存储中读取访问令牌并进行验证。在本地通过 WebCrypto API 和缓存的 JWKS 端点进行操作,当项目使用非对称签名密钥时(这是新项目的默认设置);如果使用对称密钥,则仅通过调用getUser来验证。返回的声明总是来自解析 JWT,而不是通过用户查询获得。 [getUser](/docs/reference/javascript/auth-getuser)会向项目的 Auth 实例发起网络请求以获取用户记录,这样可以获得用户的最新信息,但需要进行一次网络请求。getSession当你需要原始会话(访问令牌、刷新令牌和过期时间)时使用。例如,将访问令牌转发到另一个服务。会话是直接从本地存储加载的,并不会重新向认证服务器验证,因此当存储与客户端共享(如 cookies、请求头)时,嵌入的用户对象不应单独信任。要验证身份,请使用getClaims验证访问令牌,或调用getUser获取一个新的、服务器确认的用户记录。
总结:使用 getClaims 来验证身份(通常用于保护页面和数据),当你需要从认证服务器获取最新的用户记录时用 getUser,而当你直接需要访问或刷新令牌时用 getSession,但不要依赖它返回的用户对象来做授权决策。
1// app/oauth/consent/page.tsx2import { createServerClient } from '@supabase/ssr'3import { cookies } from 'next/headers'4import { redirect } from 'next/navigation'56export default async function ConsentPage({7 searchParams,8}: {9 searchParams: { authorization_id?: string }10}) {11 const authorizationId = (await searchParams).authorization_id1213 if (!authorizationId) {14 return <div>Error: Missing authorization_id</div>15 }1617 const supabase = createServerClient(18 process.env.NEXT_PUBLIC_SUPABASE_URL!,19 process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,20 {21 cookies: {22 getAll: async () => (await cookies()).getAll(),23 setAll: async (cookiesToSet, _headers) => {24 const cookieStore = await cookies()25 cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))26 },27 },28 }29 )3031 // Check if user is authenticated32 const { data } = await supabase.auth.getClaims()33 const claims = data?.claims3435 if (!claims) {36 // Redirect to login, preserving authorization_id37 redirect(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`)38 }3940 // Get authorization details using the authorization_id41 const { data: authDetails, error } =42 await supabase.auth.oauth.getAuthorizationDetails(authorizationId)4344 if (error || !authDetails) {45 return <div>Error: {error?.message || 'Invalid authorization request'}</div>46 }4748 // if no authorization_id returned, user has previously consented, redirect them49 if (!('authorization_id' in authDetails)) {50 redirect(authDetails['redirect_url'])51 }5253 return (54 <div>55 <h1>Authorize {authDetails.client.name}</h1>56 <p>This application wants to access your account.</p>5758 <div>59 <p>60 <strong>Client:</strong> {authDetails.client.name}61 </p>62 <p>63 <strong>Redirect URI:</strong> {authDetails.redirect_uri}64 </p>65 {authDetails.scope && authDetails.scope.trim() && (66 <div>67 <strong>Requested permissions:</strong>68 <ul>69 {authDetails.scope.split(' ').map((scopeItem) => (70 <li key={scopeItem}>{scopeItem}</li>71 ))}72 </ul>73 </div>74 )}75 </div>7677 <form action="/api/oauth/decision" method="POST">78 <input type="hidden" name="authorization_id" value={authorizationId} />79 <button type="submit" name="decision" value="approve">80 Approve81 </button>82 <button type="submit" name="decision" value="deny">83 Deny84 </button>85 </form>86 </div>87 )88}1// app/api/oauth/decision/route.ts2import { createServerClient } from '@supabase/ssr'3import { cookies } from 'next/headers'4import { NextResponse } from 'next/server'56export async function POST(request: Request) {7 const formData = await request.formData()8 const decision = formData.get('decision')9 const authorizationId = formData.get('authorization_id') as string1011 if (!authorizationId) {12 return NextResponse.json({ error: 'Missing authorization_id' }, { status: 400 })13 }1415 const supabase = createServerClient(16 process.env.NEXT_PUBLIC_SUPABASE_URL!,17 process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,18 {19 cookies: {20 getAll: async () => (await cookies()).getAll(),21 setAll: async (cookiesToSet, _headers) => {22 const cookieStore = await cookies()23 cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))24 },25 },26 }27 )2829 if (decision === 'approve') {30 const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId)3132 if (error) {33 return NextResponse.json({ error: error.message }, { status: 400 })34 }3536 // Redirect back to the client with authorization code37 return NextResponse.redirect(data.redirect_url)38 } else {39 const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId)4041 if (error) {42 return NextResponse.json({ error: error.message }, { status: 400 })43 }4445 // Redirect back to the client with error46 return NextResponse.redirect(data.redirect_url)47 }48}它是怎么运作的 #
🌐 How it works
- 用户导航到你的授权路径 - 当第三方应用启动 OAuth 时,Supabase Auth 会将用户重定向到你配置的授权路径(例如,
https://example.com/oauth/consent?authorization_id=<id>) - 提取 authorization_id - 你的页面从 URL 查询参数中提取
authorization_id - 检查认证 - 你的页面会检查用户是否已登录,如果没有,会重定向到登录页(会保留 authorization_id)
- 获取详情 - 调用
supabase.auth.oauth.getAuthorizationDetails(authorization_id)来获取请求客户端的信息 - 显示同意界面 - 显示一个界面,询问用户是否批准或拒绝访问
- 处理决定 - 当用户点击批准/拒绝时:
- 打电话给
supabase.auth.oauth.approveAuthorization(authorization_id)或denyAuthorization(authorization_id) - 这些方法会在内部处理所有 OAuth 逻辑(生成授权码等)
- 它们返回一个
redirect_urlURL
- 打电话给
- 重定向回去 - 将用户重定向到
redirect_urlURL,这个 URL 会把他们带回第三方应用,并附带授权码(批准)或错误信息(拒绝)
注册一个 OAuth 客户端 #
🌐 Register an OAuth client
在第三方应用可以将你的项目作为身份提供者之前,你需要先把它们注册为 OAuth 客户端。
🌐 Before third-party applications can use your project as an identity provider, you need to register them as OAuth clients.
- 去 身份验证 > OAuth 应用(在 管理 部分下)
- 点击 添加新客户
- 输入客户信息:
- 客户端名称:你应用的友好名称
- 重定向 URI:一个或多个用户在授权后会被重定向到的网址
- 客户类型:请选择:
- 公开 - 适用于移动端和单页应用(无需客户端密钥)
- 保密 - 用于服务器端应用(包括客户端密钥)
- 点击 创建
你将收到:
🌐 You'll receive:
- 客户端ID:客户端的唯一标识符
- 客户端密钥(用于保密客户端):用于验证客户端的一个秘密密钥
请妥善保存客户端密钥。它只会显示一次。如果丢失了,可以在 OAuth Apps 页面重新生成一个新的。
🌐 Store the client secret securely. It will only be shown once. If you lose it, you can regenerate a new one from the OAuth Apps page.
令牌端点认证方法 #
🌐 Token endpoint authentication method
当客户端交换授权码或刷新令牌时,它必须向令牌端点进行身份验证。token_endpoint_auth_method 控制这种身份验证的方式:
🌐 When a client exchanges an authorization code or refreshes a token, it must authenticate with the token endpoint. The token_endpoint_auth_method controls how this authentication happens:
| 方法 | 描述 | 使用者 |
|---|---|---|
none | 不进行客户端认证。请求体中只发送 client_id。 | 公开客户端(必需) |
client_secret_basic | 通过 HTTP 基本认证 (Authorization: Basic <base64(client_id:client_secret)>) 发送客户端凭证。这是机密客户端的默认方式。 | 机密客户端 |
client_secret_post | 在请求体中发送客户端凭证(client_id 和 client_secret 作为表单参数)。 | 机密客户端 |
默认值: 公开客户端默认使用 none。机密客户端默认使用 client_secret_basic(根据 RFC 7591)。
限制条件: 公开客户端必须使用 none。机密客户端不能使用 none。
你可以在通过仪表板注册客户端时设置,或者通过程序方式设置。查看 OAuth 流程 了解每种方法的示例。
🌐 You can set this when registering a client via the dashboard or programmatically. See OAuth Flows for examples of each method in action.
自定义令牌(可选) #
🌐 Customizing tokens (optional)
默认情况下,OAuth 访问令牌包含像 user_id、role 和 client_id 这样的标准声明。如果你需要自定义令牌——例如,为第三方验证设置特定的 audience 声明,或添加客户端特定的元数据——可以使用 自定义访问令牌钩子。
🌐 By default, OAuth access tokens include standard claims like user_id, role, and client_id. If you need to customize tokens—for example, to set a specific audience claim for third-party validation or add client-specific metadata—use Custom Access Token Hooks.
自定义访问令牌钩子会在所有令牌发放时触发,包括 OAuth 流程。你可以使用 client_id 参数根据请求令牌的 OAuth 客户端来自定义令牌。
🌐 Custom Access Token Hooks are triggered for all token issuance, including OAuth flows. You can use the client_id parameter to customize tokens based on which OAuth client is requesting them.
常见用例 #
🌐 Common use cases
- 自定义
audience声明:将aud声明设置为第三方 API 端点,以便正确进行 JWT 验证 - 添加客户端特定权限:根据请求访问的OAuth客户端包含自定义声明
- 实现动态作用域:添加 RLS 策略可以使用的元数据,以进行精细化访问控制
更多示例,请参见 Token 安全与 RLS。
🌐 For more examples, see Token Security & RLS.
重定向 URI 配置 #
🌐 Redirect URI configuration
重定向 URI 对 OAuth 安全非常关键。Supabase Auth 只会重定向到客户端明确注册的 URI。
🌐 Redirect URIs are critical for OAuth security. Supabase Auth will only redirect to URIs that are explicitly registered with the client.
不要与普通重定向 URL 混淆
本节是关于OAuth 客户端重定向 URI的——用户授权第三方应用访问你的 Supabase 项目后,将他们重定向到哪里。这跟一般的 重定向 URL 设置不一样,后者控制的是用户通过社交登录在你的应用里登录后被重定向到哪里。
🌐 This section is about OAuth client redirect URIs - where to send users after they authorize third-party apps to access your Supabase project. This is different from the general Redirect URLs setting, which controls where to send users after they sign in TO your app using social providers.
仅限完全匹配 - 不使用通配符或模式
OAuth 客户端的重定向 URI 需要完全匹配 URL。和一般的重定向 URL(支持通配符)不同,OAuth 客户端的重定向 URI 不支持通配符、模式或部分 URL。你必须注册完整、准确的回调 URL。
🌐 OAuth client redirect URIs require exact, complete URL matches. Unlike general redirect URLs (which support wildcards), OAuth client redirect URIs do NOT support wildcards, patterns, or partial URLs. You must register the full, exact callback URL.
最佳实践 #
🌐 Best practices
- 在生产环境中使用 HTTPS - 在生产环境中,重定向 URI 始终使用 HTTPS
- 注册准确完整的 URL - 每个重定向 URI 必须是完整的 URL,包括协议、域名、路径以及必要时的端口
- 为每个环境使用不同的 OAuth 客户端 - 为开发、测试和生产环境创建独立的 OAuth 客户端。这样可以提供更好的安全隔离,允许独立更换密钥,并提升可审计性。如果你需要在不同环境中使用相同的客户端,可以注册多个重定向 URI,但还是建议使用独立的客户端。
下一步 #
🌐 Next steps
既然你已经注册了第一个 OAuth 客户端,你现在可以:
🌐 Now that you've registered your first OAuth client, you're ready to:
- 了解 OAuth 流程 - 学习授权码和刷新令牌流程是如何运作的
- 实现 MCP 身份验证 - 启用 AI 代理身份验证
- 使用 RLS 保护 - 控制 OAuth 客户端的数据访问