Files
moneyfy/backend/app/api/routes/auth.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

117 lines
3.5 KiB
Python

"""Anmeldung, Abmeldung, Token-Erneuerung und Passwortwechsel."""
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,
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
from app.services.auth import (
Credentials,
change_password,
get_auth_provider,
issue_tokens,
revoke_refresh_token,
rotate_tokens,
)
router = APIRouter(prefix="/auth", tags=["auth"])
UNAUTHORIZED = {status.HTTP_401_UNAUTHORIZED: {"model": ErrorResponse}}
@router.post(
"/login",
response_model=UserOut,
responses=UNAUTHORIZED,
summary="Anmelden",
description="Prüft die Zugangsdaten und legt Access- und Refresh-Token als "
"httpOnly-Cookies ab.",
)
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)
)
tokens = await issue_tokens(session, user)
await session.commit()
set_auth_cookies(
response,
tokens.access_token,
tokens.access_expires_at,
tokens.refresh_token,
tokens.refresh_expires_at,
)
return UserOut.model_validate(user)
@router.post(
"/refresh",
response_model=UserOut,
responses=UNAUTHORIZED,
summary="Sitzung erneuern",
description="Tauscht das Refresh-Token gegen ein neues Paar. Das alte Token "
"wird dabei gesperrt (Rotation).",
)
async def refresh(request: Request, response: Response, session: DbSession) -> UserOut:
token = request.cookies.get(REFRESH_COOKIE)
if not token:
clear_auth_cookies(response)
raise AuthError("Nicht angemeldet.", code="not_authenticated")
user, tokens = await rotate_tokens(session, token)
await session.commit()
set_auth_cookies(
response,
tokens.access_token,
tokens.access_expires_at,
tokens.refresh_token,
tokens.refresh_expires_at,
)
return UserOut.model_validate(user)
@router.post(
"/logout",
response_model=MessageResponse,
summary="Abmelden",
description="Sperrt das aktuelle Refresh-Token und löscht beide Cookies.",
)
async def logout(request: Request, response: Response, session: DbSession) -> MessageResponse:
await revoke_refresh_token(session, request.cookies.get(REFRESH_COOKIE))
await session.commit()
clear_auth_cookies(response)
return MessageResponse(detail="Abgemeldet.")
@router.post(
"/change-password",
response_model=MessageResponse,
responses=UNAUTHORIZED,
summary="Passwort ändern",
description="Ändert das Passwort und beendet dabei alle bestehenden Sitzungen.",
)
async def change_own_password(
payload: ChangePasswordRequest,
response: Response,
user: CurrentUser,
session: DbSession,
) -> MessageResponse:
await change_password(session, user, payload.current_password, payload.new_password)
await session.commit()
clear_auth_cookies(response)
return MessageResponse(detail="Passwort geändert. Bitte neu anmelden.")