Tutorial: OAuth2 with Supabase
This tutorial walks you through adding OAuth2 token validation to your Stoma gateway. We’ll use Supabase as the example provider, but the pattern works with any RFC 7662-compliant introspection endpoint (Auth0, Okta, Keycloak, etc.).
What We’re Building
Section titled “What We’re Building”A gateway that:
- Accepts requests with a Supabase access token
- Validates the token via Supabase’s introspection endpoint
- Extracts user info and forwards it to your upstream
- Returns 401 if the token is missing or invalid
Prerequisites
Section titled “Prerequisites”- A Stoma project set up (see Quick Start)
- A Supabase project (free tier works)
- Node.js 20+ or Cloudflare Workers
Step 1: Get Your Supabase Credentials
Section titled “Step 1: Get Your Supabase Credentials”In your Supabase dashboard:
- Go to Settings → API
- Copy your URL (e.g.,
https://xyzcompany.supabase.co) - Go to Settings → Authentication → Providers
- Make sure OAuth providers are configured (or use the anonymous access)
For the introspection endpoint, you’ll use Supabase’s auth REST API:
https://[YOUR_PROJECT].supabase.co/auth/v1/introspectStep 2: Configure the Gateway
Section titled “Step 2: Configure the Gateway”Here’s a complete gateway that validates OAuth2 tokens:
// OAuth2 token introspection with Supabase (or any OIDC provider).
// Validates bearer tokens via the introspection endpoint, caches
// valid tokens, and forwards user info to upstream services.
import { createGateway, oauth2, cors, requestLog } from "@homegrower-club/stoma";
import { memoryAdapter } from "@homegrower-club/stoma/adapters";
const adapter = memoryAdapter();
const gateway = createGateway({
name: "protected-api",
basePath: "/api",
adapter,
// Global policies apply to all routes
policies: [
requestLog(),
cors({ origins: ["https://your-app.com"] }),
],
routes: [
{
path: "/protected/*",
pipeline: {
policies: [
oauth2({
// Supabase introspection endpoint — works with any RFC 7662 provider
introspectionUrl: "https://xyzcompany.supabase.co/auth/v1/introspect",
clientId: "your-anon-key",
clientSecret: "your-service-role-key",
tokenLocation: "header",
headerName: "Authorization",
headerPrefix: "Bearer",
// Forward user info to upstream as headers
forwardTokenInfo: {
sub: "x-user-id",
email: "x-user-email",
role: "x-user-role",
},
// Cache valid tokens for 5 minutes to reduce introspection calls
cacheTtlSeconds: 300,
// Require specific scopes
requiredScopes: ["authenticated"],
}),
],
upstream: {
type: "url",
target: "https://your-backend.internal",
},
},
},
],
});
export default gateway;
Step 3: Understanding Each Option
Section titled “Step 3: Understanding Each Option”Let’s break down what each config option does:
| Option | What it does | Why it matters |
|---|---|---|
introspectionUrl | Where to validate tokens | Supabase’s introspection endpoint |
clientId / clientSecret | Credentials for the introspection request | Authenticates your gateway to Supabase |
tokenLocation | Where to find the token | "header" (default) looks in Authorization |
headerPrefix | Token format | "Bearer" for standard OAuth2 tokens |
forwardTokenInfo | What user data to pass upstream | Extracts claims and sets them as headers |
cacheTtlSeconds | How long to remember valid tokens | Reduces latency and API calls |
Step 4: Test It
Section titled “Step 4: Test It”With a Valid Token
Section titled “With a Valid Token”# Get a token from Supabase (you'd do this in your frontend)curl -X POST "https://xyzcompany.supabase.co/auth/v1/token?grant_type=password" \ -H "apikey: your-anon-key" \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "password": "password123"}'Then call your gateway:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ https://your-gateway.com/api/protected/users
# Your upstream receives:# x-user-id: the-supabase-user-id# x-user-email: user@example.comWithout a Token
Section titled “Without a Token”curl https://your-gateway.com/api/protected/users
# Returns:# {# "error": "unauthorized",# "message": "Missing or invalid OAuth2 token",# "statusCode": 401# }Step 5: Adding RBAC
Section titled “Step 5: Adding RBAC”Want to restrict access based on user roles? Add the rbac policy after oauth2:
import { oauth2, rbac } from "@homegrower-club/stoma";
policies: [ oauth2({ /* ... */ }), rbac({ roleHeader: "x-user-role", // Forwarded from oauth2 roles: ["authenticated"], // Allow any logged-in user // Or be more specific: // roles: ["admin", "moderator"], }),]Alternative: Use JWT Instead
Section titled “Alternative: Use JWT Instead”If you prefer validating JWTs directly (no introspection call), use jwtAuth instead:
import { jwtAuth } from "@homegrower-club/stoma";
jwtAuth({ jwksUrl: "https://xyzcompany.supabase.co/auth/v1/.well-known/jwks.json", issuer: "https://xyzcompany.supabase.co", audience: "authenticated", // Supabase includes this by default forwardClaims: { sub: "x-user-id", email: "x-user-email", },})When to use which?
| Approach | Pros | Cons |
|---|---|---|
| OAuth2 introspection | Real-time validation, can revoke tokens instantly | Extra API call on each request (unless cached) |
| JWT (jwksUrl) | No extra API call, faster | Can’t revoke tokens instantly (must wait for expiry) |
What’s Next?
Section titled “What’s Next?”- Auth Policies Overview - all authentication options
- Real-World Example - full production gateway
- Testing Guide - write tests for your auth