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
117 lines
3.5 KiB
Python
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.")
|