# 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 `