strat-gameplay-webapp/backend/app/utils/cookies.py
Cal Corum ae4a92f0e0 CLAUDE: Fix Safari/iPad WebSocket connection issues
- Changed cookie SameSite policy from Lax to None with Secure=true
  for Safari ITP compatibility
- Fixed Nuxt composable context issue: moved useRuntimeConfig() from
  connect() callback to composable setup phase (required in Nuxt 4)
- Added GET /logout endpoint for easy browser-based logout
- Improved loading overlay with clear status indicators and action
  buttons (Retry, Re-Login, Dismiss)
- Added error handling with try-catch in WebSocket connect()
- Documented issue and fixes in .claude/SAFARI_WEBSOCKET_ISSUE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 12:03:57 -06:00

93 lines
2.8 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_secure_context() -> bool:
"""
Check if cookies should use Secure flag.
Returns True ONLY if APP_ENV is 'production'.
In development, we don't set Secure flag even if FRONTEND_URL is HTTPS,
because the WebSocket may connect to localhost (HTTP) and secure cookies
won't be sent over HTTP connections.
For production deployments behind HTTPS reverse proxy, set APP_ENV=production.
"""
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: True (required for SameSite=None)
- SameSite=None: Required for Safari to send cookies with fetch/XHR requests
- Path: Limits cookie scope
Note: Safari's ITP treats SameSite=Lax cookies as "not sent" for XHR/fetch
requests even on same-origin. SameSite=None with Secure=true fixes this.
Args:
response: FastAPI Response object
access_token: JWT access token
refresh_token: JWT refresh token
"""
# Access token - short-lived, sent to all requests (needed for SSR cookie forwarding)
# Using SameSite=None for Safari compatibility (requires Secure=true)
response.set_cookie(
key=ACCESS_TOKEN_COOKIE,
value=access_token,
max_age=ACCESS_TOKEN_MAX_AGE,
httponly=True,
secure=True, # Required for SameSite=None
samesite="none", # Safari requires this for fetch() to include cookies
path="/",
)
# 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=True, # Required for SameSite=None
samesite="none", # Safari requires this for fetch() to include cookies
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="/")
response.delete_cookie(key=REFRESH_TOKEN_COOKIE, path="/api/auth")