TechLead
Lesson 18 of 22
5 min read
Supabase

Advanced Authentication Patterns

Deep dive into Supabase auth with MFA, custom claims, PKCE flow, SAML SSO, and server-side authentication

Advanced Authentication Patterns

Supabase Auth supports far more than basic email/password login. For production applications, you need multi-factor authentication, custom roles via JWT claims, enterprise SSO, and secure server-side auth flows. This guide covers advanced patterns used in real SaaS products.

🚀 Advanced Auth Features

  • MFA/TOTP: Multi-factor authentication with authenticator apps
  • Custom Claims: Add roles and permissions to JWTs
  • PKCE Flow: Secure auth for server-rendered applications
  • SAML SSO: Enterprise single sign-on

Custom Claims & Roles via JWT

Add custom data to JWTs so RLS policies can check roles without extra database queries.

-- Create a function to add custom claims to the JWT
CREATE OR REPLACE FUNCTION custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
  claims jsonb;
  user_role text;
BEGIN
  -- Get the user's role from your custom table
  SELECT role INTO user_role
  FROM user_roles
  WHERE user_id = (event->>'user_id')::uuid;

  claims := event->'claims';

  IF user_role IS NOT NULL THEN
    -- Add custom claims to the JWT
    claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
  ELSE
    claims := jsonb_set(claims, '{user_role}', '"user"');
  END IF;

  -- Update the claims in the event
  event := jsonb_set(event, '{claims}', claims);
  RETURN event;
END;
$$;

-- Grant necessary permissions
GRANT USAGE ON SCHEMA public TO supabase_auth_admin;
GRANT EXECUTE ON FUNCTION custom_access_token_hook TO supabase_auth_admin;

-- Use the custom claim in RLS policies
CREATE POLICY "Admins can do everything"
  ON posts FOR ALL
  USING (
    (auth.jwt()->>'user_role') = 'admin'
  );

Multi-Factor Authentication (MFA)

// Step 1: Enroll a TOTP factor
async function enrollMFA() {
  const { data, error } = await supabase.auth.mfa.enroll({
    factorType: 'totp',
    friendlyName: 'Authenticator App',
  })

  if (data) {
    // Show QR code to user
    console.log('Scan this QR code:', data.totp.qr_code)
    // data.totp.uri — for manual entry
    // data.id — factor ID needed for verification
    return data
  }
}

// Step 2: Verify and activate the factor
async function verifyMFA(factorId: string, code: string) {
  const { data: challenge } = await supabase.auth.mfa.challenge({
    factorId,
  })

  const { data, error } = await supabase.auth.mfa.verify({
    factorId,
    challengeId: challenge.id,
    code, // 6-digit TOTP code from authenticator app
  })

  return data
}

// Step 3: Check MFA status on sign in
async function checkMFAStatus() {
  const { data } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()

  if (data.currentLevel === 'aal1' && data.nextLevel === 'aal2') {
    // User has MFA enrolled but hasn't verified yet
    // Redirect to MFA verification page
    return 'needs_verification'
  }

  return data.currentLevel // 'aal1' or 'aal2'
}

PKCE Flow for Server-Side Auth

// The PKCE flow is automatically used with @supabase/ssr
// It's more secure than the implicit flow for server-rendered apps

// In your OAuth sign-in handler:
async function signInWithGoogle() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: 'http://localhost:3000/auth/callback',
      // PKCE is used automatically when flowType is 'pkce'
    },
  })
}

// Auth callback route handler (app/auth/callback/route.ts)
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = await createClient()
    // Exchange the code for a session
    await supabase.auth.exchangeCodeForSession(code)
  }

  return NextResponse.redirect(new URL('/dashboard', request.url))
}

Enterprise SAML SSO

// Sign in with SAML SSO (for enterprise customers)
async function signInWithSSO(domain: string) {
  const { data, error } = await supabase.auth.signInWithSSO({
    domain, // e.g., 'company.com'
    options: {
      redirectTo: 'https://app.example.com/auth/callback',
    },
  })

  if (data?.url) {
    // Redirect to the IdP login page
    window.location.href = data.url
  }
}

// Account linking — connect multiple auth methods
// Users can sign in with email AND Google to the same account
// This is configured in Supabase Dashboard > Auth > Providers

⚠️ Security Best Practices

Always use the PKCE flow for server-rendered applications. Enable MFA for admin accounts. Store refresh tokens securely using HTTP-only cookies (handled by @supabase/ssr). Regularly rotate your JWT secret and API keys.

💡 Key Takeaways

  • • Custom JWT claims eliminate extra queries in RLS policies
  • • MFA with TOTP adds a critical security layer for sensitive apps
  • • PKCE flow is the standard for server-rendered applications
  • • SAML SSO enables enterprise authentication workflows
  • • Use auth hooks to customize the JWT without modifying Supabase internals

📚 Learn More

Continue Learning