Files
moneyfy/backend/app/core/cookies.py
T
Jonas MenzelandClaude Opus 5 998d5867df
CI / backend (push) Successful in 2m33s
Images bauen / build (backend) (push) Successful in 3m45s
Images bauen / build (frontend) (push) Successful in 4m23s
CI / frontend (push) Successful in 6m38s
fix: stille Anmeldeschleife bei Secure-Cookie über HTTP erklären
Bei COOKIE_SECURE=true und Zugriff über HTTP verwirft der Browser das
Sitzungs-Cookie. Die Anmeldung selbst meldet Erfolg, die nächste Anfrage gilt
aber als nicht angemeldet – der Nutzer landet ohne jede Meldung wieder auf der
Anmeldemaske und hält es für ein falsches Passwort.

- Das Frontend prüft die Sitzung unmittelbar nach der Anmeldung und nennt bei
  einem Fehlschlag die Ursache samt beider Auswege
- Das Backend protokolliert dieselbe Kombination als Warnung und wertet dabei
  X-Forwarded-Proto aus, weil hinter einem Reverse Proxy immer http ankommt
- docs/deployment.md beschreibt das Symptom wörtlich

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
2026-09-10 00:00:29 +02:00

100 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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",
)