Authentication Implementation¶
This document describes the OAuth-based authentication system implementation for the Musingly microlearning platform.
Overview¶
The authentication system provides OAuth-based authentication using Google and GitHub as identity providers. It implements:
- OAuth 2.0 flows for Google and GitHub
- Session management with secure HttpOnly cookies
- Token refresh with rotation for security
- User profile creation on first signup
- Integration with Supabase Auth for identity management
Architecture¶
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │──────▶│ Auth Edge │──────▶│ Supabase │
│ (Web/Mobile)│◀──────│ Function │◀──────│ Auth │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌──────────────┐
│ PostgreSQL │
│ - sessions │
│ - profiles │
└──────────────┘
API Endpoints¶
Base URL¶
- Local:
http://127.0.0.1:54321/functions/v1/auth - Production:
https://<project-ref>.supabase.co/functions/v1/auth
Endpoints¶
1. GET /auth/login¶
Initiates OAuth flow with specified provider.
Query Parameters:
provider(required):google|githubredirect(optional): URL to redirect after successful auth
Response: 302 Redirect to OAuth provider
Example:
2. GET /auth/callback¶
Handles OAuth provider callback after user authorizes.
Query Parameters:
code(required): Authorization code from OAuth providerstate(optional): CSRF state token for validation
Response: 200 OK
{
"user": {
"id": "uuid",
"email": "user@example.com",
"display_name": "John Doe",
"provider": "google"
},
"session": {
"access_token": "jwt-token",
"refresh_token": "refresh-token",
"expires_at": "2026-02-15T12:00:00Z",
"expires_in": 3600
},
"is_new_user": true,
"redirect_url": "http://localhost:3000/dashboard"
}
Cookies Set:
sb-access-token(HttpOnly, Secure, SameSite=Lax)sb-refresh-token(HttpOnly, Secure, SameSite=Lax)
3. GET /auth/session¶
Returns current authentication state.
Response: 200 OK
Authenticated:
{
"authenticated": true,
"user": {
"id": "uuid",
"email": "user@example.com",
"display_name": "John Doe",
"avatar_url": "https://...",
"provider": "google"
},
"session": {
"id": "session-uuid",
"provider": "google",
"expires_at": "2026-02-15T12:00:00Z",
"created_at": "2026-02-15T10:00:00Z"
}
}
Anonymous:
4. POST /auth/logout¶
Ends user session and invalidates tokens.
Headers:
Authorization: Bearer <access-token>(required)
Response: 200 OK
Cookies Cleared:
sb-access-tokensb-refresh-token
5. POST /auth/refresh¶
Refreshes access token using refresh token.
Request Body (optional):
Or provide refresh token via sb-refresh-token cookie.
Response: 200 OK
{
"access_token": "new-jwt-token",
"refresh_token": "new-refresh-token",
"expires_at": "2026-02-15T13:00:00Z",
"expires_in": 3600
}
Cookies Set:
sb-access-token(new token)sb-refresh-token(new token, rotated)
Database Schema¶
sessions Table¶
Stores active user authentication sessions.
CREATE TABLE sessions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
provider oauth_provider NOT NULL,
access_token_hash VARCHAR(255) NOT NULL,
refresh_token_hash VARCHAR(255),
access_token_expires_at TIMESTAMPTZ NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
last_activity_at TIMESTAMPTZ NOT NULL,
user_agent TEXT,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
Key Features:
- Stores SHA-256 hashes of tokens (never plain text)
- Tracks session expiration and activity
- Supports session invalidation
- RLS policies ensure users only see their own sessions
user_profiles Enhancements¶
Added OAuth provider tracking:
Security Features¶
1. CSRF Protection¶
- OAuth state parameter used for CSRF protection
- State token validated in callback
2. Token Storage¶
- HttpOnly cookies: Prevents XSS attacks
- Secure flag: HTTPS-only transmission
- SameSite=Lax: CSRF protection
- Token hashing: SHA-256 hashes stored in database
3. Token Rotation¶
- New refresh token generated on each refresh
- Old refresh token invalidated
- Detects token reuse (potential attack)
4. Session Management¶
- Automatic expiration based on token lifetime
- Manual invalidation via logout
- Cleanup function for expired sessions
5. Input Validation¶
- Zod schema validation for all inputs
- Sanitization prevents SQL injection
- Redirect URL validation prevents open redirects
OAuth Provider Setup¶
Google OAuth¶
- Create OAuth credentials in Google Cloud Console
- Add authorized redirect URIs:
- Local:
http://localhost:54321/auth/v1/callback -
Production:
https://<project-ref>.supabase.co/auth/v1/callback -
Set environment variables:
GitHub OAuth¶
- Create OAuth App in GitHub Settings
- Add authorization callback URL:
- Local:
http://localhost:54321/auth/v1/callback -
Production:
https://<project-ref>.supabase.co/auth/v1/callback -
Set environment variables:
Usage Examples¶
Client-Side OAuth Flow¶
// 1. Initiate OAuth
window.location.href = "/auth/login?provider=google&redirect=/dashboard";
// 2. After redirect, callback is handled automatically
// User lands on /dashboard with session cookies set
// 3. Check session state
const response = await fetch("/auth/session", {
credentials: "include", // Include cookies
});
const { authenticated, user, session } = await response.json();
if (authenticated) {
console.log("User:", user);
console.log("Session expires:", session.expires_at);
}
// 4. Refresh token when needed
const refreshResponse = await fetch("/auth/refresh", {
method: "POST",
credentials: "include",
});
const { access_token, expires_at } = await refreshResponse.json();
// 5. Logout
await fetch("/auth/logout", {
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
},
credentials: "include",
});
Server-Side (Supabase Admin)¶
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY,
);
// Get all active sessions for a user
const { data: sessions } = await supabase
.from("sessions")
.select("*")
.eq("user_id", userId)
.eq("is_active", true);
// Invalidate specific session
await supabase
.from("sessions")
.update({ is_active: false })
.eq("id", sessionId);
// Clean up expired sessions (run periodically)
await supabase.rpc("invalidate_expired_sessions");
Performance Considerations¶
Token Validation¶
- JWKS caching: Supabase public keys cached for 24 hours
- Session lookup: Indexed on
user_idandis_active - Single query: User + session fetched in one call
Performance Targets¶
- GET /auth/login: p95 < 100ms
- GET /auth/callback: p95 < 500ms
- POST /auth/logout: p95 < 200ms
- GET /auth/session: p95 < 50ms
- POST /auth/refresh: p95 < 300ms
Optimization Tips¶
- Use connection pooling (built into Supabase)
- Cache JWKS public keys
- Minimize database queries per request
- Use RPC functions for complex queries
Error Handling¶
Error Response Format¶
Common Error Codes¶
| Code | Status | Description |
|---|---|---|
UNAUTHORIZED |
401 | Authentication required or invalid token |
VALIDATION_ERROR |
400 | Invalid input parameters |
NOT_FOUND |
404 | Resource not found |
INTERNAL_ERROR |
500 | Server error |
MISSING_REFRESH_TOKEN |
401 | Refresh token not provided |
Testing¶
Local Testing¶
-
Start Supabase:
-
Serve auth function:
-
Test OAuth flow:
-
Run E2E tests:
Manual Testing with HTTP Files¶
See http/auth.http for comprehensive manual test cases.
Deployment¶
Deploy Auth Function¶
Environment Variables¶
Ensure these are set in Supabase dashboard:
GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGITHUB_CLIENT_IDGITHUB_CLIENT_SECRET
Troubleshooting¶
Issue: "OAuth failed"¶
- Cause: Invalid client ID/secret or redirect URI mismatch
- Solution: Verify OAuth credentials in provider dashboard
Issue: "Refresh token expired"¶
- Cause: Token rotation failed or token was revoked
- Solution: User must re-authenticate via /auth/login
Issue: "Session not found"¶
- Cause: Session expired or was invalidated
- Solution: Check
access_token_expires_atand invalidate_expired_sessions
Issue: "CSRF state mismatch"¶
- Cause: State parameter doesn't match original request
- Solution: Ensure state is preserved during OAuth flow
Service-to-Service Authentication¶
Overview¶
The platform supports service-to-service (S2S) authentication for backend
services like Cloudflare Workers performing ISR/SSR. Services authenticate via
the X-Service-Auth header with a shared secret, without needing a user JWT.
See the dedicated Service-to-Service Guide for integration examples and full documentation.
Middleware Resolution Order¶
The protection middleware (src/middleware/protection.ts) resolves user context
in this priority order:
- Dev bypass (local dev only): If
DEV_BYPASS_AUTH=trueand on localhost, create an admin context usingDEV_USER_ID - JWT resolution: If
Authorization: Bearer <token>is present, decode JWT and resolve role from custom claims (user_role,subscription_plan) - Service auth fallback: If JWT resolution yields
ANONYMOUSAND the request has a validX-Service-Authheader, create aSERVICEcontext
// In protectionMiddleware():
userContext = resolveUserContextFromHeader(authHeader);
// Service-to-service auth fallback
if (userContext.role === UserRole.ANONYMOUS && hasValidServiceAuth(c)) {
userContext = createUserContext(UserRole.SERVICE, "service");
}
SERVICE Role Permissions¶
The SERVICE role (shared/src/protection/roles.ts) has carefully scoped
permissions:
Granted:
READ_PUBLIC_CONTENT— Browse published contentREAD_PREVIEW_CONTENT— Read preview contentREAD_FULL_CONTENT— Read full contentREAD_PREMIUM_CONTENT— Read premium contentUNLIMITED_SEARCH— Search without limitsBYPASS_RATE_LIMITS— No rate limiting
Not granted:
TRACK_PROGRESS— Cannot access/me/*routesCREATE_JOURNEY— Cannot create journeysMANAGE_CONTENT— Cannot modify contentMANAGE_USERS— Cannot manage users
Database Access¶
When a service authenticates via X-Service-Auth (no JWT):
- No
Authorizationheader is forwarded to Supabase getSupabaseClient()creates a client with the anon key- RLS policies for the
anonrole apply — published content is readable - User-specific tables (
journeys,learning_events) are inaccessible - No RLS bypass — this approach respects all Row Level Security policies
Security Considerations¶
SERVICE_AUTH_SECRETmust be a strong random string (32+ characters)- Only share with trusted backend services (Cloudflare Workers, build systems)
- User JWT always takes precedence over service auth
- Service auth only grants read access, never write access
- All service requests are logged with
userId: "service"in audit trails
Future Enhancements¶
Planned Features¶
- Email/password authentication
- Multi-factor authentication (MFA)
- Social login with more providers
- Device tracking and management
- Session history and audit log
- Domain events (user_registered, user_authenticated, user_logged_out)
- Event consumption (subscription tier updates)
Event Publishing (TODO)¶
Once event infrastructure is ready, publish these domain events:
- identity.user_registered - New user completes OAuth signup
- identity.user_authenticated - Existing user logs in
- identity.user_logged_out - User logs out