Files
webmail-client/backend/app/identity/cookies.py
T
lukasandClaude Opus 4.8 a5ec0d21cb Initial commit: webmail client (M0/M1)
Three-pane webmail client.
- Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP
  sync, threading, search, drafts/outbox with undo, attachments, 2FA.
- Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane
  UI with rich-text composer, light/dark themes, resizable panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:20:34 +02:00

77 lines
2.4 KiB
Python

"""Session and CSRF cookie handling (SPEC §6.1, §10.3).
Over HTTPS we emit the spec-mandated ``__Host-session`` prefix with ``Secure``.
Over plain HTTP (dev only) we fall back to a plain ``session`` name without
``Secure`` so login works behind the Vite HTTP proxy. See docs/open-questions.md
OQ-3. ``SameSite=Lax``, ``HttpOnly`` (session), ``Path=/`` are always applied.
"""
from __future__ import annotations
import secrets
from fastapi import Request, Response
SESSION_COOKIE_SECURE = "__Host-session"
SESSION_COOKIE_INSECURE = "session"
CSRF_COOKIE_SECURE = "__Host-csrf"
CSRF_COOKIE_INSECURE = "csrf"
CSRF_HEADER = "X-CSRF"
def _is_secure(request: Request) -> bool:
return request.url.scheme == "https"
def session_cookie_name(request: Request) -> str:
"""Return the session cookie name appropriate to the request scheme."""
return SESSION_COOKIE_SECURE if _is_secure(request) else SESSION_COOKIE_INSECURE
def csrf_cookie_name(request: Request) -> str:
"""Return the CSRF cookie name appropriate to the request scheme."""
return CSRF_COOKIE_SECURE if _is_secure(request) else CSRF_COOKIE_INSECURE
def read_session_token(request: Request) -> str:
"""Read the session token from either cookie name (whichever is present)."""
return request.cookies.get(SESSION_COOKIE_SECURE) or request.cookies.get(
SESSION_COOKIE_INSECURE, ""
)
def new_csrf_token() -> str:
"""Return a fresh CSRF token for the double-submit pattern."""
return secrets.token_urlsafe(32)
def set_auth_cookies(
request: Request, response: Response, session_token: str, csrf_token: str, max_age: int
) -> None:
"""Set the session (HttpOnly) and CSRF (JS-readable) cookies."""
secure = _is_secure(request)
response.set_cookie(
session_cookie_name(request),
session_token,
max_age=max_age,
httponly=True,
secure=secure,
samesite="lax",
path="/",
)
response.set_cookie(
csrf_cookie_name(request),
csrf_token,
max_age=max_age,
httponly=False, # double-submit: client JS echoes it in the X-CSRF header
secure=secure,
samesite="lax",
path="/",
)
def clear_auth_cookies(request: Request, response: Response) -> None:
"""Clear both auth cookies (logout)."""
for name in (session_cookie_name(request), csrf_cookie_name(request)):
response.delete_cookie(name, path="/")