Files
gh-hopeoverture-worldbuildi…/skills/supabase-rls-policy-generator/references/supabase-auth.md
2025-11-29 18:46:56 +08:00

6.1 KiB

Supabase Auth Reference

This document provides reference information for Supabase authentication functions and JWT structure.

Core Auth Functions

auth.uid()

Returns the UUID of the currently authenticated user.

-- Usage in RLS policy
CREATE POLICY "User access own records"
  ON table_name FOR SELECT
  USING (auth.uid() = user_id);

Returns: UUID or NULL if unauthenticated

Common Uses:

  • User ownership checks
  • Filtering user-specific data
  • Inserting records with current user ID

auth.jwt()

Returns the full JSON Web Token for the authenticated user.

-- Access top-level claims
auth.jwt() ->> 'role'  -- Returns: 'authenticated', 'anon', custom role

-- Access nested app_metadata
auth.jwt() -> 'app_metadata' ->> 'subscription_tier'
auth.jwt() -> 'app_metadata' ->> 'organization_id'

-- Access user_metadata
auth.jwt() -> 'user_metadata' ->> 'display_name'

Returns: JSONB object or NULL if unauthenticated

JWT Structure

Standard Supabase JWT contains:

{
  "aud": "authenticated",
  "exp": 1234567890,
  "sub": "user-uuid",
  "email": "user@example.com",
  "phone": "",
  "app_metadata": {
    "provider": "email",
    "providers": ["email"]
  },
  "user_metadata": {
    "display_name": "John Doe",
    "avatar_url": "https://..."
  },
  "role": "authenticated",
  "aal": "aal1",
  "amr": [
    {
      "method": "password",
      "timestamp": 1234567890
    }
  ],
  "session_id": "session-uuid"
}

Custom Claims

Add custom claims via database functions or auth hooks:

-- Example: Add custom role from database
CREATE OR REPLACE FUNCTION auth.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
  user_role TEXT;
  org_id UUID;
BEGIN
  -- Get user's role and organization
  SELECT role, organization_id INTO user_role, org_id
  FROM user_profiles
  WHERE id = (event->>'user_id')::uuid;

  -- Add custom claims
  event := jsonb_set(event, '{claims,role}', to_jsonb(user_role));
  event := jsonb_set(event, '{claims,organization_id}', to_jsonb(org_id::text));

  RETURN event;
END;
$$;

Access custom claims in policies:

-- Check custom role
CREATE POLICY "Admin access"
  ON table_name FOR ALL
  USING (auth.jwt() ->> 'role' = 'admin');

-- Check custom organization
CREATE POLICY "Organization access"
  ON table_name FOR SELECT
  USING (
    organization_id = (auth.jwt() ->> 'organization_id')::uuid
  );

Common Patterns

Check Authentication Status

-- Require authenticated user
USING (auth.uid() IS NOT NULL)

-- Allow both authenticated and anonymous
USING (true)

-- Authenticated users only
USING (auth.jwt() ->> 'role' = 'authenticated')

Role-Based Access

-- Simple role check
USING (auth.jwt() ->> 'role' = 'admin')

-- Multiple roles
USING (
  auth.jwt() ->> 'role' IN ('admin', 'moderator')
)

-- Custom role from app_metadata
USING (
  auth.jwt() -> 'app_metadata' ->> 'custom_role' = 'premium_user'
)

Email Verification

-- Require verified email
USING (
  (auth.jwt() -> 'email_confirmed_at') IS NOT NULL
)

Multi-Factor Authentication

-- Require MFA
USING (
  auth.jwt() ->> 'aal' = 'aal2'
)

Session Management

Session ID

Access current session:

-- Filter by session
USING (
  session_id = (auth.jwt() ->> 'session_id')::uuid
)

Authentication Method

Check how user authenticated:

-- Check for password authentication
USING (
  EXISTS (
    SELECT 1 FROM jsonb_array_elements(auth.jwt() -> 'amr') AS method
    WHERE method ->> 'method' = 'password'
  )
)

-- Check for OAuth
USING (
  EXISTS (
    SELECT 1 FROM jsonb_array_elements(auth.jwt() -> 'amr') AS method
    WHERE method ->> 'method' = 'oauth'
  )
)

User Metadata vs App Metadata

User Metadata

  • User-controlled data
  • Can be updated by user
  • Store preferences, profile info
  • Access: auth.jwt() -> 'user_metadata'

App Metadata

  • Admin-controlled data
  • Cannot be updated by user
  • Store roles, permissions, org IDs
  • Access: auth.jwt() -> 'app_metadata'

Security Considerations

Always Validate User ID

-- GOOD: Enforce user ID on insert
CREATE POLICY "Insert own records"
  ON table_name FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- BAD: Allow any user_id
CREATE POLICY "Insert records"
  ON table_name FOR INSERT
  WITH CHECK (true);

Don't Trust Client-Provided IDs

-- GOOD: Use auth.uid() directly
INSERT INTO posts (user_id, content)
VALUES (auth.uid(), 'content');

-- BAD: Trust client-provided user_id
INSERT INTO posts (user_id, content)
VALUES ($1, $2);  -- Client could pass any user_id

Use SECURITY DEFINER Carefully

-- Mark function as SECURITY DEFINER only when necessary
CREATE FUNCTION check_permission()
RETURNS BOOLEAN
SECURITY DEFINER  -- Runs with function owner's privileges
AS $$
  -- Be very careful here - this bypasses RLS
$$;

Testing Auth Context

Test policies with different auth states:

-- Set user ID
SET request.jwt.claim.sub = 'user-uuid';

-- Set role
SET request.jwt.claim.role = 'admin';

-- Set custom claims
SET request.jwt.claims = '{"role": "admin", "organization_id": "org-uuid"}';

-- Reset
RESET request.jwt.claim.sub;
RESET request.jwt.claim.role;
RESET request.jwt.claims;

Common Errors

"function auth.uid() does not exist"

  • RLS is not enabled on the database
  • Not using Supabase's auth schema

NULL when expecting user ID

  • User is not authenticated
  • Token has expired
  • Policy executed outside auth context

"permission denied for relation"

  • RLS is enabled but no policies match
  • User doesn't have base GRANT permissions

Performance Tips

  1. Cache JWT lookups: Use helper functions to avoid repeated JWT parsing
  2. Index foreign keys: Always index user_id, tenant_id, etc.
  3. Avoid complex JWT parsing: Move complex logic to helper functions
  4. Use materialized views: For complex role hierarchies

Additional Resources