Skip to main content

Authentication Flow

This document describes the authentication architecture for Traceo.

Overview

Traceo uses Supabase Auth for authentication across all services:
  • Web Client: OAuth (Google, GitHub) and email/password login
  • MCP Server: JWT validation middleware
  • Engine: JWT validation middleware

Architecture

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Web App    │     │  MCP Server  │     │    Engine    │
│   (Next.js)  │     │   (Python)   │     │   (Python)   │
└──────┬───────┘     └──────┬───────┘     └──────┬───────┘
       │                    │                    │
       │ OAuth/Email        │ JWT                │ JWT
       │                    │                    │
       └────────────────────┴────────────────────┘

                    ┌───────┴───────┐
                    │   Supabase    │
                    │     Auth      │
                    └───────────────┘

Web Client Authentication

Environment Variables

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Auth Flow

  1. Login (/login):
    • User clicks OAuth provider (Google/GitHub) or enters email/password
    • Supabase handles OAuth redirect flow
    • On success, redirects to /callback
  2. Callback (/callback):
    • Exchanges auth code for session
    • Checks if user needs onboarding (no workspace)
    • Redirects to /onboarding or /sessions
  3. Middleware (src/middleware.ts):
    • Protects all routes except public paths
    • Refreshes session if expired
    • Redirects unauthenticated users to /login

Usage in Components

import { useAuth } from "@/shared/state/auth/AuthContext";

function MyComponent() {
  const { user, isAuthenticated, signOut } = useAuth();
  
  if (!isAuthenticated) {
    return <div>Please log in</div>;
  }
  
  return (
    <div>
      Welcome, {user?.email}
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}

Getting Access Token for API Calls

import { useAccessToken } from "@/shared/state/auth/AuthContext";

function ApiComponent() {
  const token = useAccessToken();
  
  const fetchData = async () => {
    const response = await fetch("/api/data", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  };
}

MCP Server Authentication

Environment Variables

SUPABASE_JWT_SECRET=your-jwt-secret  # Required
SUPABASE_URL=https://your-project.supabase.co  # Optional, for refresh
SUPABASE_ANON_KEY=your-anon-key  # Optional, for refresh

Installation

pip install traceo-mcp-server[auth]

Middleware Usage

from traceo_mcp_server.auth import AuthMiddleware, get_current_user, require_auth

# Add middleware to ASGI app
app.add_middleware(
    AuthMiddleware,
    exclude_paths=["/health", "/metrics"],
    allow_anonymous=False,
)

# Use decorator for route protection
@require_auth
def my_protected_function():
    user = get_current_user()
    print(f"User: {user.email}, Role: {user.role}")

Role-Based Access

from traceo_mcp_server.auth import require_role

@require_role("editor")
def create_requirement():
    # Only editors, admins, and owners can access
    pass

@require_role("admin")
def delete_requirement():
    # Only admins and owners can access
    pass

User Context

from traceo_mcp_server.auth import UserContext, get_current_user, require_user

# Get optional user (may be None)
user = get_current_user()

# Get required user (raises if not authenticated)
user = require_user()

# Check permissions
if user.is_admin:
    # Admin-only logic
    pass

if user.has_permission("editor"):
    # Editor or higher
    pass

Engine Authentication

Same pattern as MCP Server:
from engine.auth import AuthMiddleware, get_current_user, require_auth

# FastAPI integration
app = FastAPI()
app.add_middleware(AuthMiddleware)

@app.post("/ingest")
@require_auth
async def ingest_document(request: IngestRequest):
    user = get_current_user()
    # Process ingestion with user context

JWT Structure

Supabase JWTs contain:
{
  "sub": "user-uuid",
  "email": "user@example.com",
  "aud": "authenticated",
  "iss": "https://your-project.supabase.co/auth/v1",
  "iat": 1234567890,
  "exp": 1234571490,
  "app_metadata": {
    "workspace_id": "workspace-uuid",
    "role": "editor"
  },
  "user_metadata": {
    "full_name": "John Doe"
  }
}

Role Hierarchy

RoleLevelPermissions
viewer0Read requirements
editor1Create/update requirements
admin2Delete requirements, view audit logs, manage settings
owner3All permissions, manage users

Security Considerations

  1. JWT Secret: Keep SUPABASE_JWT_SECRET secure and never commit to git
  2. Token Expiry: Access tokens expire in 1 hour by default
  3. Session Refresh: Web middleware auto-refreshes expired sessions
  4. RLS: Database Row Level Security uses JWT claims for tenant isolation

Troubleshooting

”JWT secret not found”

Ensure SUPABASE_JWT_SECRET environment variable is set. Get it from: Supabase Dashboard → Settings → API → JWT Secret

”Token expired”

The web middleware should auto-refresh. If issues persist:
  1. Check browser cookies aren’t blocked
  2. Clear cookies and re-login
  3. Verify Supabase project is active

”Invalid signature”

  1. Verify JWT secret matches Supabase project
  2. Ensure token wasn’t modified in transit
  3. Check for encoding issues (Base64 vs raw)