"""Setzen und Löschen der Authentifizierungs-Cookies.""" import logging from datetime import UTC, datetime from fastapi import Request, Response from app.core.config import settings logger = logging.getLogger(__name__) ACCESS_COOKIE = "moneyfy_access" REFRESH_COOKIE = "moneyfy_refresh" # Der Refresh-Cookie wird nur an die Endpunkte geschickt, die ihn wirklich brauchen. REFRESH_COOKIE_PATH = "/api/auth" def request_is_secure(request: Request) -> bool: """Kam die Anfrage über HTTPS? Hinter einem Reverse Proxy steht das echte Schema in `X-Forwarded-Proto`; das Schema der Verbindung zum Container ist dort immer `http`. """ weitergeleitet = request.headers.get("x-forwarded-proto", "") if weitergeleitet: # Bei mehreren Proxies steht das ursprüngliche Schema vorne. return weitergeleitet.split(",")[0].strip().lower() == "https" return request.url.scheme == "https" def warn_if_cookie_unusable(request: Request) -> bool: """Meldet die Kombination, bei der der Browser das Cookie verwirft. `Secure`-Cookies über eine reine HTTP-Verbindung werden stillschweigend weggeworfen: Die Anmeldung meldet Erfolg, die nächste Anfrage gilt aber wieder als nicht angemeldet. Ohne diesen Hinweis ist das kaum zu erkennen. """ if not settings.cookie_secure or request_is_secure(request): return False logger.warning( "Anmeldung über %s ohne HTTPS bei COOKIE_SECURE=true – der Browser " "verwirft das Sitzungs-Cookie und die Anmeldemaske erscheint erneut. " "Entweder über HTTPS zugreifen oder COOKIE_SECURE=false setzen.", request.url.hostname or "unbekannt", ) return True def set_auth_cookies( response: Response, access_token: str, access_expires_at: datetime, refresh_token: str, refresh_expires_at: datetime, ) -> None: """Legt beide Cookies als httpOnly/SameSite=Lax ab.""" now = datetime.now(UTC) response.set_cookie( ACCESS_COOKIE, access_token, max_age=max(int((access_expires_at - now).total_seconds()), 0), httponly=True, secure=settings.cookie_secure, samesite="lax", path="/", domain=settings.cookie_domain, ) response.set_cookie( REFRESH_COOKIE, refresh_token, max_age=max(int((refresh_expires_at - now).total_seconds()), 0), httponly=True, secure=settings.cookie_secure, samesite="lax", path=REFRESH_COOKIE_PATH, domain=settings.cookie_domain, ) def clear_auth_cookies(response: Response) -> None: """Entfernt beide Cookies – muss dieselben Attribute wie beim Setzen verwenden.""" response.delete_cookie( ACCESS_COOKIE, path="/", domain=settings.cookie_domain, httponly=True, secure=settings.cookie_secure, samesite="lax", ) response.delete_cookie( REFRESH_COOKIE, path=REFRESH_COOKIE_PATH, domain=settings.cookie_domain, httponly=True, secure=settings.cookie_secure, samesite="lax", )