- Removed checkAuth() function and redirects from dashboard.astro - Removed checkAuth() function and redirects from events/new.astro - Updated to use Astro.cookies for better SSR compatibility - Client-side code now focuses on data loading, not authentication - Server-side unified auth system handles all protection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
7.8 KiB
Authentication System Documentation
Overview
Black Canyon Tickets uses a unified authentication system built on top of Supabase Auth with server-side rendering (SSR) support. All authentication functionality is centralized in a single module (src/lib/auth-unified.ts) to ensure consistency and maintainability.
Architecture
Core Module: auth-unified.ts
The unified auth module is the single source of truth for all authentication in the application. It provides:
- 🔐 Session-based authentication using Supabase cookies
- 🎭 Role-based access control (User, Admin, Super Admin)
- 🏢 Organization-based access control
- 🛡️ Security logging and rate limiting
- 📝 Type-safe authentication context
- 🐳 Docker-friendly cookie handling
Key Features
- Universal Compatibility: Works with both
Requestobjects (API routes) andAstroCookies(Astro pages) - SSR Support: Full server-side rendering support with proper cookie handling
- Bearer Token Fallback: Supports Authorization header for API clients
- Security Hardening: Built-in CSRF protection, rate limiting, and security logging
- Type Safety: Full TypeScript support with proper types
Usage Guide
Basic Authentication Check
// In Astro pages (.astro files)
import { verifyAuth } from '../lib/auth-unified';
// Check if user is authenticated (returns null if not)
const auth = await verifyAuth(Astro.cookies);
// In API routes (.ts files)
const auth = await verifyAuth(request);
Requiring Authentication
// Throws AuthError if not authenticated
const auth = await requireAuth(Astro.cookies);
// The auth object contains:
// - user: Supabase user object
// - session: Session information
// - isAdmin: boolean
// - isSuperAdmin: boolean
// - organizationId: string | null
Role-Based Access Control
// Require admin access
const auth = await requireAdmin(Astro.cookies);
// Require super admin access
const auth = await requireSuperAdmin(Astro.cookies);
// Check organization access
const auth = await requireOrganizationAccess(Astro.cookies, organizationId);
Protected Page Pattern
---
import Layout from '../layouts/Layout.astro';
import { verifyAuth } from '../lib/auth-unified';
export const prerender = false; // Enable SSR
const auth = await verifyAuth(Astro.cookies);
if (!auth) {
return Astro.redirect('/login');
}
---
<Layout title="Protected Page">
<h1>Welcome, {auth.user.email}!</h1>
</Layout>
Protected API Route Pattern
import type { APIRoute } from 'astro';
import { requireAuth, createAuthResponse } from '../lib/auth-unified';
export const GET: APIRoute = async ({ request }) => {
try {
const auth = await requireAuth(request);
// Your protected logic here
const data = { userId: auth.user.id };
return createAuthResponse(data);
} catch (error) {
return createAuthResponse(
{ error: error.message },
error.statusCode || 401
);
}
};
Using the Middleware Pattern
import { withAuth } from '../lib/auth-unified';
// Wrap your handler with authentication
export const GET: APIRoute = async ({ request }) => {
return withAuth(request, async (auth) => {
// This code only runs if authenticated
return new Response(JSON.stringify({
message: `Hello ${auth.user.email}`
}));
});
};
Authentication Flow
- Login: User submits credentials →
/api/auth/login→ Supabase sets cookies → Redirect to dashboard - Session Check: Every request →
verifyAuth()checks cookies → Returns auth context or null - Protected Routes: Page/API checks auth → Redirects to login or returns 401 if not authenticated
- Logout: Clear session →
/api/auth/logout→ Supabase clears cookies → Redirect to login
Error Handling
The auth system uses a custom AuthError class with specific error codes:
try {
const auth = await requireAuth(request);
} catch (error) {
if (error instanceof AuthError) {
switch (error.code) {
case 'NO_SESSION':
// User not logged in
break;
case 'NO_PERMISSION':
// User lacks required role
break;
case 'EXPIRED':
// Session expired
break;
}
}
}
Security Features
CSRF Protection
// Generate token for forms
const csrfToken = generateCSRFToken();
// Verify token on submission
if (!verifyCSRFToken(request, sessionToken)) {
throw new Error('Invalid CSRF token');
}
Rate Limiting
// Check rate limit (10 requests per minute by default)
const identifier = `login:${email}`;
if (!checkRateLimit(identifier, 5, 60000)) {
throw new Error('Too many attempts');
}
Security Logging
All authentication events are automatically logged:
- Failed login attempts
- Successful authentications
- Permission denied events
- Rate limit violations
Testing
Visit /auth-test-unified to test the authentication system. This page shows:
- Current authentication status
- Session information
- Request headers and cookies
- Links to test protected routes
Migration Guide
From Old Auth System
-
Update imports:
// Old import { verifyAuth } from '../lib/auth'; // New (but old path still works via proxy) import { verifyAuth } from '../lib/auth-unified'; -
Use Astro.cookies instead of Astro.request:
// Old const auth = await verifyAuth(Astro.request); // New (better SSR support) const auth = await verifyAuth(Astro.cookies); -
Handle new error types:
// Old if (!auth) throw new Error('Not authenticated'); // New import { AuthError } from '../lib/auth-unified'; if (!auth) throw new AuthError('Not authenticated', 'NO_SESSION');
Docker Considerations
The unified auth system is designed to work seamlessly in Docker environments:
- Cookie Domain: Automatically handles cookie domain settings
- Secure Cookies: Uses secure cookies in production
- Proxy Support: Correctly reads IP addresses behind proxies
Environment Variables
# Required for auth to work
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key
# Optional for enhanced security
COOKIE_DOMAIN=.yourdomain.com # For subdomain support
NODE_ENV=production # Enables secure cookies
Troubleshooting
Common Issues
-
"No session found" after login
- Check cookie settings in browser
- Verify Supabase URL and keys are correct
- Ensure cookies are not blocked
-
Dashboard flashing before redirect
- Ensure using
Astro.cookiesnotAstro.request - Verify
prerender = falseis set - Check server-side auth is enabled
- Ensure using
-
Auth works locally but not in Docker
- Check
COOKIE_DOMAINenvironment variable - Verify proxy headers are being forwarded
- Ensure secure cookie settings match environment
- Check
Debug Mode
Enable debug logging in development:
// In your page or API route
import { authDebug } from '../lib/auth-unified';
authDebug.logCookies(request);
authDebug.logSession(session);
Best Practices
- Always use the unified auth module - Don't create separate auth implementations
- Use
Astro.cookiesfor pages - Better SSR support thanAstro.request - Handle errors gracefully - Show user-friendly messages, not technical errors
- Test auth flows regularly - Use
/auth-test-unifiedto verify functionality - Keep sessions secure - Use HTTPS in production, set proper cookie flags
Future Enhancements
- Refresh token rotation
- Remember me functionality
- Two-factor authentication
- Session activity tracking
- IP-based session validation