# Cookie-Based Authentication Implementation **Date**: 2025-11-27 **Status**: Complete This document captures the full implementation of HttpOnly cookie-based authentication, including all issues encountered and their solutions. --- ## Architecture Overview ``` Browser → Nginx Proxy Manager → Nuxt SSR → Backend API (gameplay-demo.manticorum.com) ``` ### Authentication Flow 1. User clicks "Continue with Discord" (anchor tag, NOT JavaScript) 2. Browser navigates to `/api/auth/discord/login` 3. Backend creates state in Redis, redirects to Discord OAuth 4. User authorizes on Discord 5. Discord redirects to `/api/auth/discord/callback/server` 6. Backend validates, creates JWT, sets HttpOnly cookies with `path="/"` 7. Backend redirects to frontend `/games` 8. Nuxt SSR middleware forwards cookies to `/api/auth/me` 9. Backend validates cookie, returns user info 10. Page renders with authenticated state --- ## Critical Implementation Details ### 1. Cookie Configuration (Backend) **File**: `backend/app/utils/cookies.py` ```python response.set_cookie( key="pd_access_token", value=access_token, max_age=3600, # 1 hour httponly=True, secure=is_secure_context(), # True for HTTPS even in dev samesite="lax", path="/", # CRITICAL: Must be "/" not "/api" ) ``` **Why `is_secure_context()` instead of `is_production()`?** - Safari/iOS requires `Secure=true` for cookies over HTTPS connections - Even in development, if accessed via HTTPS (through proxy), cookies need `Secure=true` - `is_secure_context()` returns `True` if `APP_ENV=production` OR `FRONTEND_URL` starts with `https://` **Why `path="/"`?** - Cookies with `path="/api"` are only sent to `/api/*` routes - When browser requests `/games`, cookie isn't sent - SSR server receives no cookie to forward - Auth check fails → redirect loop ### 2. Login Button (Frontend) **File**: `frontend-sba/pages/auth/login.vue` **MUST use anchor tag, NOT JavaScript button:** ```vue Continue with Discord ``` **Why?** - iPad Safari blocks async JavaScript on certain pages - Nginx Proxy Manager may interfere with JS execution - Anchor tags work reliably in all scenarios ### 3. Auth Middleware (Frontend) **File**: `frontend-sba/middleware/auth.ts` ```typescript export default defineNuxtRouteMiddleware(async (to, from) => { const authStore = useAuthStore() if (authStore.isAuthenticated) { return // Already authenticated } // Call backend to verify cookies const isAuthed = await authStore.checkAuth() if (!isAuthed) { return navigateTo('/auth/login') } }) ``` ### 4. Auth Store - Cookie Forwarding for SSR **File**: `frontend-sba/store/auth.ts` ```typescript async function checkAuth(): Promise { const headers: Record = {} // SSR: Forward cookies from incoming request if (import.meta.server) { const event = useRequestEvent() const cookieHeader = event?.node.req.headers.cookie if (cookieHeader) { headers['Cookie'] = cookieHeader } } const response = await $fetch('/api/auth/me', { credentials: 'include', // Client-side headers, // Server-side cookie forwarding }) // ... handle response } ``` ### 5. API Calls - Use Cookies, Not Tokens **All API calls must use `credentials: 'include'`:** ```typescript // CORRECT const response = await $fetch('/api/games/', { credentials: 'include', }) // WRONG - authStore.token no longer exists const response = await $fetch('/api/games/', { headers: { Authorization: `Bearer ${authStore.token}` } }) ``` --- ## Common Issues & Solutions ### Issue 1: "Continue with Discord" Does Nothing **Symptom**: Clicking login button has no effect **Cause**: Using `