Skip to content
Auth

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:

  1. 在你的项目中启用 OAuth 2.1 服务器功能
  2. 配置你的授权路径
  3. 搭建你的授权界面(前端)
  4. 注册 OAuth 客户端应用

启用 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.

  1. 去你的项目仪表板
  2. 在侧边栏中导航到 身份验证 > OAuth 服务器
  3. 启用 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

配置你的授权路径 #

🌐 Configure your authorization path

在注册客户之前,你需要先配置你的授权界面放在哪儿。

🌐 Before registering clients, you need to configure where your authorization UI will live.

  1. 在你的项目仪表板中,导航到 身份验证 > OAuth 服务器
  2. 设置授权路径(例如:/oauth/consent

你的授权界面将在合并后的站点网址 + 授权路径。例如:

🌐 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:

  1. 提取 authorization_id - 从 URL 查询参数中获取 authorization_id
  2. 验证用户 - 如果尚未登录,请重定向到你的登录页面(保留 authorization_id)
  3. 获取授权详细信息 - 使用 supabase.auth.oauth.getAuthorizationDetails(authorization_id) 获取客户端信息,包括请求的权限范围
  4. 显示同意屏幕 - 向用户展示哪个应用在请求访问权限,以及正在请求哪些权限/范围
  5. 处理用户决定 - 根据用户选择调用 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.

示例授权界面 #

🌐 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.tsx
2
import { createServerClient } from '@supabase/ssr'
3
import { cookies } from 'next/headers'
4
import { redirect } from 'next/navigation'
5
6
export default async function ConsentPage({
7
searchParams,
8
}: {
9
searchParams: { authorization_id?: string }
10
}) {
11
const authorizationId = (await searchParams).authorization_id
12
13
if (!authorizationId) {
14
return <div>Error: Missing authorization_id</div>
15
}
16
17
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
)
30
31
// Check if user is authenticated
32
const { data } = await supabase.auth.getClaims()
33
const claims = data?.claims
34
35
if (!claims) {
36
// Redirect to login, preserving authorization_id
37
redirect(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`)
38
}
39
40
// Get authorization details using the authorization_id
41
const { data: authDetails, error } =
42
await supabase.auth.oauth.getAuthorizationDetails(authorizationId)
43
44
if (error || !authDetails) {
45
return <div>Error: {error?.message || 'Invalid authorization request'}</div>
46
}
47
48
// if no authorization_id returned, user has previously consented, redirect them
49
if (!('authorization_id' in authDetails)) {
50
redirect(authDetails['redirect_url'])
51
}
52
53
return (
54
<div>
55
<h1>Authorize {authDetails.client.name}</h1>
56
<p>This application wants to access your account.</p>
57
58
<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>
76
77
<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
Approve
81
</button>
82
<button type="submit" name="decision" value="deny">
83
Deny
84
</button>
85
</form>
86
</div>
87
)
88
}
1
// app/api/oauth/decision/route.ts
2
import { createServerClient } from '@supabase/ssr'
3
import { cookies } from 'next/headers'
4
import { NextResponse } from 'next/server'
5
6
export 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 string
10
11
if (!authorizationId) {
12
return NextResponse.json({ error: 'Missing authorization_id' }, { status: 400 })
13
}
14
15
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
)
28
29
if (decision === 'approve') {
30
const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId)
31
32
if (error) {
33
return NextResponse.json({ error: error.message }, { status: 400 })
34
}
35
36
// Redirect back to the client with authorization code
37
return NextResponse.redirect(data.redirect_url)
38
} else {
39
const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId)
40
41
if (error) {
42
return NextResponse.json({ error: error.message }, { status: 400 })
43
}
44
45
// Redirect back to the client with error
46
return NextResponse.redirect(data.redirect_url)
47
}
48
}

它是怎么运作的 #

🌐 How it works

  1. 用户导航到你的授权路径 - 当第三方应用启动 OAuth 时,Supabase Auth 会将用户重定向到你配置的授权路径(例如,https://example.com/oauth/consent?authorization_id=<id>
  2. 提取 authorization_id - 你的页面从 URL 查询参数中提取 authorization_id
  3. 检查认证 - 你的页面会检查用户是否已登录,如果没有,会重定向到登录页(会保留 authorization_id)
  4. 获取详情 - 调用 supabase.auth.oauth.getAuthorizationDetails(authorization_id) 来获取请求客户端的信息
  5. 显示同意界面 - 显示一个界面,询问用户是否批准或拒绝访问
  6. 处理决定 - 当用户点击批准/拒绝时:
    • 打电话给 supabase.auth.oauth.approveAuthorization(authorization_id)denyAuthorization(authorization_id)
    • 这些方法会在内部处理所有 OAuth 逻辑(生成授权码等)
    • 它们返回一个 redirect_url URL
  7. 重定向回去 - 将用户重定向到 redirect_url URL,这个 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.

  1. 身份验证 > OAuth 应用(在 管理 部分下)
  2. 点击 添加新客户
  3. 输入客户信息:
    • 客户端名称:你应用的友好名称
    • 重定向 URI:一个或多个用户在授权后会被重定向到的网址
    • 客户类型:请选择:
      • 公开 - 适用于移动端和单页应用(无需客户端密钥)
      • 保密 - 用于服务器端应用(包括客户端密钥)
  4. 点击 创建

你将收到:

🌐 You'll receive:

  • 客户端ID:客户端的唯一标识符
  • 客户端密钥(用于保密客户端):用于验证客户端的一个秘密密钥

令牌端点认证方法 #

🌐 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_idclient_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_idroleclient_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.

最佳实践 #

🌐 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: