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>
79 lines
2.0 KiB
Python
79 lines
2.0 KiB
Python
"""
|
|
Cookie Utilities for Authentication
|
|
|
|
Handles HttpOnly cookie creation for JWT tokens.
|
|
Supports both access and refresh tokens with appropriate security settings.
|
|
|
|
Author: Claude (Jarvis)
|
|
Date: 2025-11-27
|
|
"""
|
|
|
|
from fastapi import Response
|
|
from app.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
# Cookie configuration
|
|
ACCESS_TOKEN_COOKIE = "pd_access_token"
|
|
REFRESH_TOKEN_COOKIE = "pd_refresh_token"
|
|
ACCESS_TOKEN_MAX_AGE = 60 * 60 # 1 hour
|
|
REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 7 # 7 days
|
|
|
|
|
|
def is_production() -> bool:
|
|
"""Check if running in production environment."""
|
|
return getattr(settings, "app_env", "development") == "production"
|
|
|
|
|
|
def set_auth_cookies(
|
|
response: Response,
|
|
access_token: str,
|
|
refresh_token: str,
|
|
) -> None:
|
|
"""
|
|
Set both access and refresh token cookies on response.
|
|
|
|
Security settings:
|
|
- HttpOnly: Prevents XSS access to tokens
|
|
- Secure: HTTPS only in production
|
|
- SameSite=Lax: CSRF protection while allowing top-level navigations
|
|
- Path: Limits cookie scope
|
|
|
|
Args:
|
|
response: FastAPI Response object
|
|
access_token: JWT access token
|
|
refresh_token: JWT refresh token
|
|
"""
|
|
# Access token - short-lived, sent to all /api endpoints
|
|
response.set_cookie(
|
|
key=ACCESS_TOKEN_COOKIE,
|
|
value=access_token,
|
|
max_age=ACCESS_TOKEN_MAX_AGE,
|
|
httponly=True,
|
|
secure=is_production(),
|
|
samesite="lax",
|
|
path="/api",
|
|
)
|
|
|
|
# Refresh token - long-lived, restricted to auth endpoints only
|
|
response.set_cookie(
|
|
key=REFRESH_TOKEN_COOKIE,
|
|
value=refresh_token,
|
|
max_age=REFRESH_TOKEN_MAX_AGE,
|
|
httponly=True,
|
|
secure=is_production(),
|
|
samesite="lax",
|
|
path="/api/auth",
|
|
)
|
|
|
|
|
|
def clear_auth_cookies(response: Response) -> None:
|
|
"""
|
|
Clear all auth cookies (logout).
|
|
|
|
Args:
|
|
response: FastAPI Response object
|
|
"""
|
|
response.delete_cookie(key=ACCESS_TOKEN_COOKIE, path="/api")
|
|
response.delete_cookie(key=REFRESH_TOKEN_COOKIE, path="/api/auth")
|