Skip to content

authgear/authgear-example-supabase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Authgear + Supabase Integration Example

A complete tutorial demonstrating how to integrate Authgear authentication with Supabase, including JWT token exchange and Row Level Security (RLS).

Overview

This project demonstrates a simple instrument tracking app where users can manage their personal collection after logging in with Authgear.

My Instrument List App Screenshot

This project shows how to:

  • Use Authgear for user authentication in a React app
  • Exchange Authgear JWT tokens for Supabase JWTs
  • Implement Row Level Security (RLS) with user-specific data
  • Build a CRUD app with per-user data isolation

Features

  • Authentication: Authgear OAuth 2.0 with PKCE flow
  • Token Exchange: Automatic JWT exchange via Supabase Edge Function
  • Database: Supabase PostgreSQL with RLS policies
  • Frontend: React + Vite with minimal CSS (browser defaults)
  • CRUD Operations: User-specific instrument management

Architecture

┌─────────────┐          ┌──────────────┐          ┌──────────────┐
│   Browser   │          │   Authgear   │          │   Supabase   │
│  (React App)│◄────────►│  (Auth IdP)  │          │  (Database)  │
└──────┬──────┘          └──────────────┘          └──────┬───────┘
       │                                                   │
       │  1. Login with Authgear                          │
       │  2. Get Authgear JWT                             │
       │                                                   │
       │  3. Call Supabase with Authgear JWT              │
       └──────────────────────────────────────────────────►
       │                                                   │
       │  4. Exchange JWT (Edge Function)                 │
       │  5. Return Supabase JWT                          │
       │◄──────────────────────────────────────────────────
       │                                                   │
       │  6. Access data with Supabase JWT                │
       └──────────────────────────────────────────────────►

Setup Guide

Prerequisites

  • Node.js 18+ and npm
  • Supabase account and project
  • Authgear account and project
  • Supabase CLI (for deploying functions)

1. Clone and Install

git clone <repository-url>
cd authgear-supabase-demo
cd my-app
npm install

2. Database Setup

Create the Instruments Table

Run this SQL in your Supabase SQL Editor:

-- Create a helper function to get the current user ID from JWT
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS TEXT AS $$
  SELECT auth.jwt() ->> 'sub';
$$ LANGUAGE SQL STABLE;

-- Create instruments table
CREATE TABLE instruments (
  id BIGSERIAL PRIMARY KEY,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
  name TEXT NOT NULL,
  user_id TEXT NOT NULL
);

-- Enable Row Level Security
ALTER TABLE instruments ENABLE ROW LEVEL SECURITY;

-- Create RLS policies for user-specific access
CREATE POLICY "users can read their own instruments" 
ON "public"."instruments"
FOR SELECT
TO authenticated
USING (user_id = current_user_id());

CREATE POLICY "users can insert their own instruments" 
ON "public"."instruments"
FOR INSERT
TO authenticated
WITH CHECK (user_id = current_user_id());

CREATE POLICY "users can update their own instruments" 
ON "public"."instruments"
FOR UPDATE
TO authenticated
USING (user_id = current_user_id())
WITH CHECK (user_id = current_user_id());

CREATE POLICY "users can delete their own instruments" 
ON "public"."instruments"
FOR DELETE
TO authenticated
USING (user_id = current_user_id());

Key Points:

  • current_user_id() extracts the user ID from the JWT's sub claim
  • user_id column stores the Authgear user ID
  • RLS policies use current_user_id() to ensure users only access their own data
  • All policies require authenticated role (set by the exchange function)

3. Deploy Supabase Edge Function

The JWT exchange function converts Authgear tokens to Supabase tokens.

Configure Function Environment Variables

In your Supabase Dashboard → Edge Functions → Secrets, add:

AUTHGEAR_ENDPOINT=https://your-project.authgear.cloud
SB_JWT_SECRET=your-supabase-jwt-secret

Finding your JWT Secret:

  • Go to: Supabase Dashboard → Project Settings → API → JWT Secret
  • Copy the value (it's different from the anon key!)

Deploy the Function

cd supabase
npx supabase functions deploy exchange-jwt

How the Exchange Function Works

  1. Receives Authgear JWT: Client sends Authgear token in Authorization header
  2. Verifies Token: Validates JWT signature using Authgear's public keys (JWKS)
  3. Extracts Claims: Gets user information (sub, email, etc.) from verified token
  4. Signs Supabase JWT: Creates new JWT with:
    • role: "authenticated" (required by Supabase RLS)
    • sub: User ID from Authgear (used in RLS policies)
    • Other claims from original token
  5. Returns Token: Client uses this token for Supabase API calls

Function Location: supabase/functions/exchange-jwt/index.ts

Key Code Sections:

// Verify Authgear token with public key
const verified = await verifyToken(token);

// Sign new Supabase JWT
payload.role = "authenticated"; // Required for RLS
const supabaseJwt = await new jose.SignJWT(payload)
  .setProtectedHeader({ alg: "HS256", typ: "JWT" })
  .setIssuer("supabase")
  .sign(supabaseSecret);

4. Configure Authgear

  1. Go to Authgear Portal
  2. Create a new application (type: Single Page Application)
  3. Configure OAuth:
    • Redirect URIs: http://localhost:5173/auth-redirect
    • Post Logout Redirect URIs: http://localhost:5173/
  4. Note your:
    • Authgear Endpoint (e.g., https://your-project.authgear.cloud)
    • Client ID

5. Configure React App

Create my-app/.env.local:

cp my-app/.env.example my-app/.env.local

Update with your credentials:

# Supabase Configuration
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-supabase-anon-key

# Authgear Configuration
VITE_AUTHGEAR_CLIENT_ID=your-authgear-client-id
VITE_AUTHGEAR_ENDPOINT=https://your-project.authgear.cloud
VITE_AUTHGEAR_REDIRECT_URL=http://localhost:5173/auth-redirect
VITE_AUTHGEAR_LOGOUT_REDIRECT_URL=http://localhost:5173/

6. Run the Application

cd my-app
npm run dev

Visit http://localhost:5173

How It Works

Authentication Flow

  1. User Clicks Login

    • App calls authgear.startAuthentication()
    • User redirects to Authgear login page
  2. User Logs In

    • Authgear authenticates user
    • Redirects to /auth-redirect with auth code
  3. Complete Authentication

    • AuthRedirect component calls authgear.finishAuthentication()
    • Exchanges auth code for access token
    • Stores token in authgear SDK
    • Redirects to home page
  4. Session State Management

    • UserProvider monitors authgear.sessionState
    • Uses onSessionStateChange delegate for real-time updates
    • Fetches user info when authenticated

Data Access Flow

  1. User Requests Data

    • App calls supabase.from('instruments').select()
  2. Automatic Token Exchange (via accessToken callback)

    • Supabase client intercepts request
    • Calls custom accessToken() function
    • Function gets Authgear token and exchanges it:
      const response = await fetch(`${SUPABASE_URL}/functions/v1/exchange-jwt`, {
        headers: { Authorization: `Bearer ${authgearToken}` }
      });
      const { supabaseJwt } = await response.json();
    • Returns Supabase JWT to client
  3. Database Query with RLS

    • Request uses Supabase JWT in Authorization header
    • Supabase verifies JWT signature
    • RLS policies check auth.jwt() ->> 'sub' matches user_id
    • Returns only user's data

Supabase Client Configuration

The Supabase client is configured with a custom accessToken callback that automatically handles token exchange:

Location: my-app/src/lib/supabase.js

export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    autoRefreshToken: false,  // We manage tokens via Authgear
    persistSession: false,     // Session managed by Authgear
  },
  accessToken: async () => {
    // This callback runs before every Supabase API call
    await authgear.refreshAccessTokenIfNeeded();
    const authgearToken = authgear.accessToken;
    
    if (!authgearToken) return null;
    
    // Exchange Authgear token for Supabase token
    const supabaseJwt = await exchangeToken(authgearToken);
    return supabaseJwt;
  },
});

How it works:

  1. Every Supabase query (SELECT, INSERT, UPDATE, DELETE) triggers the accessToken callback
  2. The callback refreshes the Authgear token if needed
  3. It exchanges the Authgear token for a Supabase JWT via the Edge Function
  4. The Supabase JWT is automatically added to the request's Authorization header
  5. No manual token management needed in components!

Token Caching

The app caches exchanged tokens to avoid unnecessary API calls:

// Only exchange if Authgear token changed
if (authgearToken === lastAuthgearAccessToken && cachedSupabaseJwt) {
  return cachedSupabaseJwt;
}

This means the exchange function is only called when:

  • The user first logs in
  • The Authgear token is refreshed
  • The cache is cleared on logout

Project Structure

authgear-supabase-demo/
├── my-app/                          # React frontend
│   ├── src/
│   │   ├── components/
│   │   │   ├── Home.jsx            # Main app with CRUD
│   │   │   └── AuthRedirect.jsx    # OAuth callback handler
│   │   ├── context/
│   │   │   └── UserProvider.jsx    # Auth state management
│   │   ├── lib/
│   │   │   └── supabase.js         # Supabase client + token exchange
│   │   ├── config/
│   │   │   └── constants.js        # Environment variables
│   │   ├── App.jsx                 # Router setup
│   │   └── main.jsx                # Authgear initialization
│   ├── .env.example                # Environment template
│   └── package.json
│
└── supabase/                        # Supabase backend
    └── functions/
        └── exchange-jwt/            # JWT exchange function
            ├── index.ts             # Edge Function code
            └── .env.example         # Function environment template

Resources

License

MIT