令牌安全和行级安全
当你在 Supabase 项目中启用 OAuth 2.1 时,第三方应用可以代表用户访问他们的数据。行级安全(RLS)策略对于控制每个 OAuth 客户端可以访问哪些数据非常重要。
🌐 When you enable OAuth 2.1 in your Supabase project, third-party applications can access user data on their behalf. Row Level Security (RLS) policies are crucial for controlling exactly what data each OAuth client can access.
作用域控制 OIDC 数据,而不是数据库访问
OAuth 范围(openid、email、profile、phone)决定了 ID 令牌中包含哪些用户信息以及 UserInfo 端点返回哪些信息。它们不控制你对数据库表或 API 端点的访问。
🌐 The OAuth scopes (openid, email, profile, phone) control what user information is included in ID tokens and returned by the UserInfo endpoint. They do not control access to your database tables or API endpoints.
使用 RLS 来定义哪些 OAuth 客户端可以访问哪些数据,而不管它们请求了哪些权限。
🌐 Use RLS to define which OAuth clients can access which data, regardless of the scopes they requested.
OAuth 令牌是如何与 RLS 一起工作的 #
🌐 How OAuth tokens work with RLS
Supabase Auth 发出的 OAuth 访问令牌是 JWT,包含所有标准的 Supabase 声明以及特定于 OAuth 的声明。这意味着你现有的 RLS 策略仍然有效,同时你可以添加特定于 OAuth 的逻辑来创建更细粒度的访问控制。
🌐 OAuth access tokens issued by Supabase Auth are JWTs that include all standard Supabase claims plus OAuth-specific claims. This means your existing RLS policies continue to work, and you can add OAuth-specific logic to create granular access controls.
令牌结构 #
🌐 Token structure
每个 OAuth 访问令牌都包含:
🌐 Every OAuth access token includes:
1{2 "sub": "user-uuid",3 "role": "authenticated",4 "aud": "authenticated",5 "user_id": "user-uuid",6 "email": "user@example.com",7 "client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",8 "aal": "aal1",9 "amr": [{ "method": "password", "timestamp": 1735815600 }],10 "session_id": "session-uuid",11 "iss": "https://<project-ref>.supabase.co/auth/v1",12 "iat": 1735815600,13 "exp": 173581920014}关键的 OAuth 特定声明是:
🌐 The key OAuth-specific claim is:
| 声明 | 描述 |
|---|---|
client_id | 获取此令牌的 OAuth 客户端的唯一标识符 |
你可以在 RLS 策略中使用这个声明,为不同的客户端授予不同的权限。
🌐 You can use this claim in RLS policies to grant different permissions to different clients.
在 RLS 中提取 OAuth 声明 #
🌐 Extracting OAuth claims in RLS
在你的策略中使用 auth.jwt() 函数来访问令牌声明:
🌐 Use the auth.jwt() function to access token claims in your policies:
1-- Get the client ID from the token2(auth.jwt() ->> 'client_id')34-- Check if the token is from an OAuth client5(auth.jwt() ->> 'client_id') IS NOT NULL67-- Check if the token is from a specific client8(auth.jwt() ->> 'client_id') = 'mobile-app-client-id'OAuth 的常见 RLS 模式 #
🌐 Common RLS patterns for OAuth
模式1:授予特定客户完全访问权限 #
🌐 Pattern 1: Grant specific client full access
允许特定的 OAuth 客户端访问所有用户数据:
🌐 Allow a specific OAuth client to access all user data:
1CREATE POLICY "Mobile app can access user data"2ON user_data FOR ALL3USING (4 auth.uid() = user_id AND5 (auth.jwt() ->> 'client_id') = 'mobile-app-client-id'6);模式2:授予多个客户端只读访问权限 #
🌐 Pattern 2: Grant multiple clients read-only access
允许多个 OAuth 客户端读取数据,但不能修改它:
🌐 Allow several OAuth clients to read data, but not modify it:
1CREATE POLICY "Third-party apps can read profiles"2ON profiles FOR SELECT3USING (4 auth.uid() = user_id AND5 (auth.jwt() ->> 'client_id') IN (6 'analytics-client-id',7 'reporting-client-id',8 'dashboard-client-id'9 )10);模式3:限制OAuth客户端访问敏感数据 #
🌐 Pattern 3: Restrict sensitive data from OAuth clients
防止 OAuth 客户端访问敏感数据:
🌐 Prevent OAuth clients from accessing sensitive data:
1CREATE POLICY "OAuth clients cannot access payment info"2ON payment_methods FOR ALL3USING (4 auth.uid() = user_id AND5 (auth.jwt() ->> 'client_id') IS NULL -- Only direct user sessions6);模式4:客户专用数据访问 #
🌐 Pattern 4: Client-specific data access
不同的客户端访问不同的数据子集:
🌐 Different clients access different subsets of data:
1-- Analytics client can only read aggregated data2CREATE POLICY "Analytics client reads summaries"3ON user_metrics FOR SELECT4USING (5 auth.uid() = user_id AND6 (auth.jwt() ->> 'client_id') = 'analytics-client-id'7);89-- Admin client can read and modify all data10CREATE POLICY "Admin client full access"11ON user_data FOR ALL12USING (13 auth.uid() = user_id AND14 (auth.jwt() ->> 'client_id') = 'admin-client-id'15);真实案例 #
🌐 Real-world examples
示例 1:多平台应用 #
🌐 Example 1: Multi-platform application
你有一个网页应用、移动应用和第三方集成:
🌐 You have a web app, mobile app, and third-party integrations:
1-- Web app: Full access2CREATE POLICY "Web app full access"3ON profiles FOR ALL4USING (5 auth.uid() = user_id AND6 (7 (auth.jwt() ->> 'client_id') = 'web-app-client-id'8 OR (auth.jwt() ->> 'client_id') IS NULL -- Direct user sessions9 )10);1112-- Mobile app: Read-only access to profiles13CREATE POLICY "Mobile app reads profiles"14ON profiles FOR SELECT15USING (16 auth.uid() = user_id AND17 (auth.jwt() ->> 'client_id') = 'mobile-app-client-id'18);1920-- Third-party integration: Limited data access21CREATE POLICY "Integration reads public data"22ON profiles FOR SELECT23USING (24 auth.uid() = user_id AND25 (auth.jwt() ->> 'client_id') = 'integration-client-id' AND26 is_public = true27);自定义访问令牌钩子 #
🌐 Custom access token hooks
自定义访问令牌钩子 可以与 OAuth 令牌一起使用,允许你根据 OAuth 客户端注入自定义声明。这对于自定义标准 JWT 声明,比如 audience (aud),或者添加特定客户端的元数据特别有用。
自定义访问令牌钩子会在所有令牌发布时触发。使用 client_id 或 authentication_method(OAuth 流程使用 oauth_provider/authorization_code)来区分 OAuth 和普通认证。
🌐 Custom Access Token Hooks are triggered for all token issuance. Use client_id or authentication_method (oauth_provider/authorization_code for OAuth flows) to differentiate OAuth from regular authentication.
自定义受众声明 #
🌐 Customizing the audience claim
一个常见的用例是为不同的 OAuth 客户端定制 audience 声明。这允许第三方服务验证令牌是专门为它们颁发的:
🌐 A common use case is customizing the audience claim for different OAuth clients. This allows third-party services to validate that tokens were issued specifically for them:
1Deno.serve(async (req) => {2 const { user, claims, client_id } = await req.json()34 // Customize audience based on OAuth client5 if (client_id === 'mobile-app-client-id') {6 return new Response(7 JSON.stringify({8 claims: {9 aud: 'https://api.myapp.com',10 app_version: '2.0.0',11 },12 }),13 { headers: { 'Content-Type': 'application/json' } }14 )15 }1617 if (client_id === 'analytics-partner-id') {18 return new Response(19 JSON.stringify({20 claims: {21 aud: 'https://analytics.partner.com',22 access_level: 'read-only',23 },24 }),25 { headers: { 'Content-Type': 'application/json' } }26 )27 }2829 // Default audience for non-OAuth flows30 return new Response(JSON.stringify({ claims: {} }), {31 headers: { 'Content-Type': 'application/json' },32 })33})audience 申明尤其对以下情况很重要:
🌐 The audience claim is especially important for:
- 第三方验证JWT:服务可以验证令牌是否是为它们特定的API颁发的
- 多租户应用:针对不同客户端应用的不同受众
- 合规性:满足要求受众验证的安全要求
添加特定客户的声明 #
🌐 Adding client-specific claims
你也可以根据 OAuth 客户端添加自定义声明和元数据:
🌐 You can also add custom claims and metadata based on the OAuth client:
1import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'23Deno.serve(async (req) => {4 const { user, claims, client_id } = await req.json()56 const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SECRET_KEY')!)78 // Add custom claims based on OAuth client9 let customClaims = {}1011 if (client_id === 'mobile-app-client-id') {12 customClaims.aud = 'https://mobile.myapp.com'13 customClaims.app_version = '2.0.0'14 customClaims.platform = 'mobile'15 } else if (client_id === 'analytics-client-id') {16 customClaims.aud = 'https://analytics.myapp.com'17 customClaims.read_only = true18 customClaims.data_retention_days = 9019 } else if (client_id?.startsWith('mcp-')) {20 // MCP AI agents21 const { data: agent } = await supabase22 .from('approved_ai_agents')23 .select('name, max_data_retention_days')24 .eq('client_id', client_id)25 .single()2627 customClaims.aud = `https://mcp.myapp.com/${client_id}`28 customClaims.ai_agent = true29 customClaims.agent_name = agent?.name30 customClaims.max_retention = agent?.max_data_retention_days31 }3233 return new Response(JSON.stringify({ claims: customClaims }), {34 headers: { 'Content-Type': 'application/json' },35 })36})在 RLS 中使用这些自定义声明:
🌐 Use these custom claims in RLS:
1-- Policy based on custom claims2CREATE POLICY "Read-only clients cannot modify"3ON user_data FOR UPDATE4USING (5 auth.uid() = user_id AND6 (auth.jwt() -> 'user_metadata' ->> 'read_only')::boolean IS NOT TRUE7);89-- Policy based on audience claim10CREATE POLICY "Only specific audience can access"11ON api_data FOR SELECT12USING (13 auth.uid() = user_id AND14 (auth.jwt() ->> 'aud') IN (15 'https://api.myapp.com',16 'https://mobile.myapp.com'17 )18);安全最佳实践 #
🌐 Security best practices
1. 最小权限原则 #
🌐 1. Principle of least privilege
只给 OAuth 客户端他们需要的最低权限:
🌐 Grant OAuth clients only the minimum permissions they need:
1-- Bad: Grant all access by default2CREATE POLICY "OAuth clients full access"3ON user_data FOR ALL4USING (auth.uid() = user_id);56-- Good: Grant specific access per client7CREATE POLICY "Specific client specific access"8ON user_data FOR SELECT9USING (10 auth.uid() = user_id AND11 (auth.jwt() ->> 'client_id') = 'trusted-client-id'12);2. 为 OAuth 客户端设置单独策略 #
🌐 2. Separate policies for OAuth clients
为 OAuth 客户端创建专门的策略,而不是把它们和用户策略混在一起:
🌐 Create dedicated policies for OAuth clients rather than mixing them with user policies:
1-- User access2CREATE POLICY "Users access their own data"3ON user_data FOR ALL4USING (5 auth.uid() = user_id AND6 (auth.jwt() ->> 'client_id') IS NULL7);89-- OAuth client access (separate policy)10CREATE POLICY "OAuth clients limited access"11ON user_data FOR SELECT12USING (13 auth.uid() = user_id AND14 (auth.jwt() ->> 'client_id') IN ('client-1', 'client-2')15);3. 定期审查 OAuth 客户端 #
🌐 3. Regularly audit OAuth clients
跟踪并查看哪些客户有权限:
🌐 Track and review which clients have access:
1-- View all active OAuth clients2SELECT3 oc.client_id,4 oc.name,5 oc.created_at,6 COUNT(DISTINCT s.user_id) as active_users7FROM auth.oauth_clients oc8LEFT JOIN auth.sessions s ON s.client_id = oc.client_id9WHERE s.created_at > NOW() - INTERVAL '30 days'10GROUP BY oc.client_id, oc.name, oc.created_at;测试你的政策 #
🌐 Testing your policies
在部署到生产环境之前,务必先测试你的 RLS 策略:
🌐 Always test your RLS policies before deploying to production:
1-- Test as a specific OAuth client2SET request.jwt.claims = '{3 "sub": "test-user-uuid",4 "role": "authenticated",5 "client_id": "test-client-id"6}';78-- Test queries9SELECT * FROM user_data WHERE user_id = 'test-user-uuid';1011-- Reset12RESET request.jwt.claims;或者使用 Supabase 仪表板的 RLS 策略测试器。
🌐 Or use the Supabase Dashboard's RLS policy tester.
故障排除 #
🌐 Troubleshooting
策略对 OAuth 客户端不起作用 #
🌐 Policy not working for OAuth client
问题:尽管有有效的令牌,OAuth 客户端仍然无法访问数据。
检查:
- 确认保单包含客户的
client_id - 确保表上启用了 RLS
- 检查是否有冲突的限制性政策
- 用秘密密钥测试以隔离 RLS 问题
1-- Debug: See what client_id is in the token2SELECT auth.jwt() ->> 'client_id';34-- Debug: Test without RLS5SET LOCAL role = service_role;6SELECT * FROM your_table;政策太宽松 #
🌐 Policy too permissive
问题:OAuth 客户端访问了它不该访问的数据。
解决方案:使用 AS RESTRICTIVE 策略来添加额外的约束条件:
1-- This policy runs in addition to permissive policies2CREATE POLICY "Restrict OAuth clients"3ON sensitive_data4AS RESTRICTIVE5FOR ALL6TO authenticated7USING (8 -- OAuth clients cannot access this table at all9 (auth.jwt() ->> 'client_id') IS NULL10);无法区分用户和 OAuth 客户端 #
🌐 Can't differentiate between users and OAuth clients
问题:需要对直接用户会话和 OAuth 应用不同的逻辑。
解决方案:检查是否存在 client_id:
1-- Direct user sessions (no OAuth)2CREATE POLICY "Direct users full access"3ON user_data FOR ALL4USING (5 auth.uid() = user_id AND6 (auth.jwt() ->> 'client_id') IS NULL7);89-- OAuth clients (limited access)10CREATE POLICY "OAuth clients read only"11ON user_data FOR SELECT12USING (13 auth.uid() = user_id AND14 (auth.jwt() ->> 'client_id') IS NOT NULL15);下一步 #
🌐 Next steps