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
This commit is contained in:
co-authored by
Claude Opus 5
parent
c110340628
commit
998d5867df
@@ -3,7 +3,12 @@
|
||||
from fastapi import APIRouter, Request, Response, status
|
||||
|
||||
from app.api.deps import CurrentUser, DbSession
|
||||
from app.core.cookies import REFRESH_COOKIE, clear_auth_cookies, set_auth_cookies
|
||||
from app.core.cookies import (
|
||||
REFRESH_COOKIE,
|
||||
clear_auth_cookies,
|
||||
set_auth_cookies,
|
||||
warn_if_cookie_unusable,
|
||||
)
|
||||
from app.core.errors import AuthError
|
||||
from app.schemas.auth import ChangePasswordRequest, LoginRequest, UserOut
|
||||
from app.schemas.common import ErrorResponse, MessageResponse
|
||||
@@ -29,7 +34,12 @@ UNAUTHORIZED = {status.HTTP_401_UNAUTHORIZED: {"model": ErrorResponse}}
|
||||
description="Prüft die Zugangsdaten und legt Access- und Refresh-Token als "
|
||||
"httpOnly-Cookies ab.",
|
||||
)
|
||||
async def login(payload: LoginRequest, response: Response, session: DbSession) -> UserOut:
|
||||
async def login(
|
||||
payload: LoginRequest, request: Request, response: Response, session: DbSession
|
||||
) -> UserOut:
|
||||
# Meldet die Kombination, bei der der Browser das Cookie stillschweigend verwirft.
|
||||
warn_if_cookie_unusable(request)
|
||||
|
||||
provider = get_auth_provider("local")
|
||||
user = await provider.authenticate(
|
||||
session, Credentials(username=payload.username, password=payload.password)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Setzen und Löschen der Authentifizierungs-Cookies."""
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import Response
|
||||
from fastapi import Request, Response
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCESS_COOKIE = "moneyfy_access"
|
||||
REFRESH_COOKIE = "moneyfy_refresh"
|
||||
|
||||
@@ -13,6 +16,38 @@ REFRESH_COOKIE = "moneyfy_refresh"
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user