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 服务器登录的用户会计入你项目的月活跃用户 (MAU)。

🌐 OAuth 2.1 server is in beta and available on all Supabase plans. It has no separate charge. Users who sign in through your OAuth server count toward your project's Monthly Active Users (MAUs).

  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 解码,而不是用户查找。当访问令牌快到期时,getClaims 会在验证前刷新会话,这就是服务器渲染会话保持活跃的方式。
  • [getUser](/docs/reference/javascript/auth-getuser) 会向项目的 Auth 实例发起网络请求以获取用户记录,这样可以获得用户的最新信息,但需要进行一次网络请求。
  • getSession 当你需要原始会话(访问令牌、刷新令牌和过期时间)时使用。例如,将访问令牌转发到另一个服务。会话是直接从本地存储加载的,并不会重新向认证服务器验证,因此当存储与客户端共享(如 cookies、请求头)时,嵌入的用户对象不应单独信任。要验证身份,请使用 getClaims 验证访问令牌,或调用 getUser 获取一个新的、服务器确认的用户记录。

总结:使用 getClaims 来验证身份(通常用于保护页面和数据),当你需要从认证服务器获取最新的用户记录时用 getUser,而当你直接需要访问或刷新令牌时用 getSession,但不要依赖它返回的用户对象来做授权决策。

// app/oauth/consent/page.tsx
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function ConsentPage({
searchParams,
}: {
searchParams: { authorization_id?: string }
}) {
const authorizationId = (await searchParams).authorization_id
if (!authorizationId) {
return <div>Error: Missing authorization_id</div>
}
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll: async () => (await cookies()).getAll(),
setAll: async (cookiesToSet, _headers) => {
const cookieStore = await cookies()
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
},
},
}
)
// Check if user is authenticated
const { data } = await supabase.auth.getClaims()
const claims = data?.claims
if (!claims) {
// Redirect to login, preserving authorization_id
redirect(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`)
}
// Get authorization details using the authorization_id
const { data: authDetails, error } =
await supabase.auth.oauth.getAuthorizationDetails(authorizationId)
if (error || !authDetails) {
return <div>Error: {error?.message || 'Invalid authorization request'}</div>
}
// if no authorization_id returned, user has previously consented, redirect them
if (!('authorization_id' in authDetails)) {
redirect(authDetails['redirect_url'])
}
return (
<div>
<h1>Authorize {authDetails.client.name}</h1>
<p>This application wants to access your account.</p>
<div>
<p>
<strong>Client:</strong> {authDetails.client.name}
</p>
<p>
<strong>Redirect URI:</strong> {authDetails.redirect_uri}
</p>
{authDetails.scope && authDetails.scope.trim() && (
<div>
<strong>Requested permissions:</strong>
<ul>
{authDetails.scope.split(' ').map((scopeItem) => (
<li key={scopeItem}>{scopeItem}</li>
))}
</ul>
</div>
)}
</div>
<form action="/api/oauth/decision" method="POST">
<input type="hidden" name="authorization_id" value={authorizationId} />
<button type="submit" name="decision" value="approve">
Approve
</button>
<button type="submit" name="decision" value="deny">
Deny
</button>
</form>
</div>
)
}
// app/api/oauth/decision/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const formData = await request.formData()
const decision = formData.get('decision')
const authorizationId = formData.get('authorization_id') as string
if (!authorizationId) {
return NextResponse.json({ error: 'Missing authorization_id' }, { status: 400 })
}
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll: async () => (await cookies()).getAll(),
setAll: async (cookiesToSet, _headers) => {
const cookieStore = await cookies()
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
},
},
}
)
if (decision === 'approve') {
const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 })
}
// Redirect back to the client with authorization code
return NextResponse.redirect(data.redirect_url)
} else {
const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 })
}
// Redirect back to the client with error
return NextResponse.redirect(data.redirect_url)
}
}

它是怎么运作的 #

🌐 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_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.

最佳实践 #

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