CLAUDE: Implement server-side OAuth flow with HttpOnly cookies
Fixes iPad Safari authentication issue where async JavaScript is blocked
on OAuth callback pages after cross-origin redirects (Cloudflare + Safari ITP).
**Problem**: iPad Safari blocks all async operations (Promises, setTimeout,
onMounted) on the OAuth callback page, preventing frontend token exchange.
**Solution**: Move entire OAuth flow to backend with HttpOnly cookies,
eliminating JavaScript dependency on callback page.
## Backend Changes (7 files)
### New Files
- app/services/oauth_state.py - Redis-based OAuth state management
* CSRF protection with one-time use tokens (10min TTL)
* Replaces frontend sessionStorage state validation
- app/utils/cookies.py - HttpOnly cookie utilities
* Access token: 1 hour, Path=/api
* Refresh token: 7 days, Path=/api/auth
* Security: HttpOnly, Secure (prod), SameSite=Lax
### Modified Files
- app/api/routes/auth.py
* NEW: GET /discord/login - Initiate OAuth with state creation
* NEW: GET /discord/callback/server - Server-side callback handler
* NEW: POST /logout - Clear auth cookies
* UPDATED: GET /me - Cookie + header support (backwards compatible)
* UPDATED: POST /refresh - Cookie + body support (backwards compatible)
* FIXED: exchange_code_for_token() accepts redirect_uri parameter
- app/config.py
* Added discord_server_redirect_uri config
* Added frontend_url config for post-auth redirects
- app/websocket/handlers.py
* Updated connect handler to parse cookies from environ
* Falls back to auth object for backwards compatibility
- .env.example
* Added DISCORD_SERVER_REDIRECT_URI example
* Added FRONTEND_URL example
## Frontend Changes (10 files)
### Core Auth Changes
- store/auth.ts - Complete rewrite for cookie-based auth
* Removed: token, refreshToken, tokenExpiresAt state (HttpOnly)
* Added: checkAuth() - calls /api/auth/me with credentials
* Updated: loginWithDiscord() - redirects to backend endpoint
* Updated: logout() - calls backend logout endpoint
* All $fetch calls use credentials: 'include'
- pages/auth/callback.vue - Simplified to error handler
* No JavaScript token exchange needed
* Displays errors from query params
* Verifies auth with checkAuth() on success
- plugins/auth.client.ts
* Changed from localStorage init to checkAuth() call
* Async plugin to ensure auth state before navigation
- middleware/auth.ts - Simplified
* Removed token validity checks (HttpOnly cookies)
* Simple isAuthenticated check
### Cleanup Changes
- composables/useWebSocket.ts
* Added withCredentials: true
* Removed auth object with token
* Updated canConnect to use isAuthenticated only
- layouts/default.vue, layouts/game.vue, pages/index.vue, pages/games/[id].vue
* Removed initializeAuth() calls (handled by plugin)
## Documentation
- OAUTH_IPAD_ISSUE.md - Problem analysis and investigation notes
- OAUTH_SERVER_SIDE_IMPLEMENTATION.md - Complete implementation guide
* Security improvements summary
* Discord Developer Portal setup instructions
* Testing checklist
* OAuth flow diagram
## Security Improvements
- Tokens stored in HttpOnly cookies (XSS-safe)
- OAuth state in Redis with one-time use (CSRF-safe)
- Follows OAuth 2.0 Security Best Current Practice
- Backwards compatible with Authorization header auth
## Testing
- ✅ Backend OAuth endpoints functional
- ✅ Token exchange with correct redirect_uri
- ✅ Cookie-based auth working
- ✅ WebSocket connection with cookies
- ✅ Desktop browser flow verified
- ⏳ iPad Safari testing pending Discord redirect URI config
## Next Steps
1. Add Discord redirect URI in Developer Portal:
https://gameplay-demo.manticorum.com/api/auth/discord/callback/server
2. Test complete flow on iPad Safari
3. Verify WebSocket auto-reconnection with cookies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>