- 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>
93 lines
2.8 KiB
Python
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")
|