Files
MABEA/backend/app/api/v1/endpoints/auth.py
T
patrickandClaude Sonnet 5 ceddeb1bf4
CI / backend-tests (push) Failing after 19s
CI / frontend-build (push) Successful in 25s
Multi-Agent-Review: Race Conditions, DB-Indizes, TLS/Security-Härtung
Konsolidierte Funde aus postgres-/sql-/jwt-/owasp-top10-expert-Review:

- Race Conditions gefixt: doppelte aktive Kontrolle (SAVEPOINT + partieller
  Unique-Index), doppelte Mindermengen-Genehmigung (FOR UPDATE + Unique-Index),
  Lost-Update bei Nachfüllung (FOR UPDATE auf Fehlbestand/Objektposition).
- Migration 0007: partielle Unique-Indizes als DB-Sicherheitsnetz + fehlende
  FK-Indizes (fehlbestand.material_id, kontrolle(objekt_id,status),
  zustaendigkeit, benutzer_rolle.rolle, objektposition.ablaufdatum u.a.).
- Connection-Pool explizit begrenzt (pool_size=5, max_overflow=5) - ohne das
  könnte jeder uvicorn-Worker den Postgres max_connections-Wert sprengen.
- Timing-Angriff bei Login-Enumeration gefixt (konstante Antwortzeit über
  Dummy-Hash), JWT-Decode verlangt jetzt exp/sub-Claims.
- App-seitiges Rate-Limiting (slowapi, 5/min) auf /auth/login als Verteidigung
  in der Tiefe zusätzlich zum nginx-Limit.
- nginx: TLS mit selbstsigniertem Zertifikat (LAN-Betrieb, keine Domain),
  HSTS, Content-Security-Policy, Permissions-Policy ergänzt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-04 18:47:06 +02:00

60 lines
2.0 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.core.rate_limit import limiter
from app.core.security import create_access_token, verify_password_konstante_zeit
from app.db.session import get_db
from app.models.auth import Benutzer
router = APIRouter()
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class MeResponse(BaseModel):
id: int
name: str
login: str
rollen: list[str]
@router.post("/auth/login", response_model=TokenResponse)
@limiter.limit("5/minute")
async def login(
request: Request,
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
) -> TokenResponse:
result = await db.execute(select(Benutzer).where(Benutzer.login == form_data.username))
benutzer = result.scalar_one_or_none()
# Passwort-Vergleich läuft IMMER (auch bei unbekanntem Login), damit die
# Antwortzeit nicht verrät, ob ein Login existiert (jwt-expert-Review-Fund).
passwort_ok = verify_password_konstante_zeit(
form_data.password, benutzer.passwort_hash if benutzer else None
)
if benutzer is None or not benutzer.aktiv or not passwort_ok:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Login oder Passwort falsch",
headers={"WWW-Authenticate": "Bearer"},
)
token = create_access_token(subject=benutzer.login)
return TokenResponse(access_token=token)
@router.get("/auth/me", response_model=MeResponse)
async def me(current_user: Benutzer = Depends(get_current_user)) -> MeResponse:
return MeResponse(
id=current_user.id,
name=current_user.name,
login=current_user.login,
rollen=current_user.rollen_namen,
)