Files
blackcanyontickets/AUTH.md
dzinesco 425dfc9348 fix: Remove client-side auth redirects causing dashboard flashing
- 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>
2025-07-12 20:40:11 -06:00

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

  1. Universal Compatibility: Works with both Request objects (API routes) and AstroCookies (Astro pages)
  2. SSR Support: Full server-side rendering support with proper cookie handling
  3. Bearer Token Fallback: Supports Authorization header for API clients
  4. Security Hardening: Built-in CSRF protection, rate limiting, and security logging
  5. 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

  1. Login: User submits credentials → /api/auth/login → Supabase sets cookies → Redirect to dashboard
  2. Session Check: Every request → verifyAuth() checks cookies → Returns auth context or null
  3. Protected Routes: Page/API checks auth → Redirects to login or returns 401 if not authenticated
  4. 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

  1. Update imports:

    // Old
    import { verifyAuth } from '../lib/auth';
    
    // New (but old path still works via proxy)
    import { verifyAuth } from '../lib/auth-unified';
    
  2. Use Astro.cookies instead of Astro.request:

    // Old
    const auth = await verifyAuth(Astro.request);
    
    // New (better SSR support)
    const auth = await verifyAuth(Astro.cookies);
    
  3. 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:

  1. Cookie Domain: Automatically handles cookie domain settings
  2. Secure Cookies: Uses secure cookies in production
  3. 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

  1. "No session found" after login

    • Check cookie settings in browser
    • Verify Supabase URL and keys are correct
    • Ensure cookies are not blocked
  2. Dashboard flashing before redirect

    • Ensure using Astro.cookies not Astro.request
    • Verify prerender = false is set
    • Check server-side auth is enabled
  3. Auth works locally but not in Docker

    • Check COOKIE_DOMAIN environment variable
    • Verify proxy headers are being forwarded
    • Ensure secure cookie settings match environment

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

  1. Always use the unified auth module - Don't create separate auth implementations
  2. Use Astro.cookies for pages - Better SSR support than Astro.request
  3. Handle errors gracefully - Show user-friendly messages, not technical errors
  4. Test auth flows regularly - Use /auth-test-unified to verify functionality
  5. 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