from datetime import datetime, timedelta, timezone import jwt from passlib.context import CryptContext from app.core.app_settings import settings _pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # Konstanter Dummy-Hash für Timing-Angriff-Schutz (jwt-expert-Review-Fund): # ohne existierenden Benutzer kurzschließt `benutzer is None or verify_password(...)` # den bcrypt-Vergleich, was per Antwortzeit verrät, ob ein Login existiert. _DUMMY_HASH = _pwd_context.hash("kein-echtes-passwort-nur-fuer-konstante-antwortzeit") def hash_password(password: str) -> str: return _pwd_context.hash(password) def verify_password(plain_password: str, password_hash: str) -> bool: return _pwd_context.verify(plain_password, password_hash) def verify_password_konstante_zeit(plain_password: str, password_hash: str | None) -> bool: """Wie verify_password, aber prüft immer gegen einen Hash (Dummy, falls der Benutzer nicht existiert) - verhindert Login-Enumeration per Antwortzeit.""" return _pwd_context.verify(plain_password, password_hash or _DUMMY_HASH) def create_access_token(*, subject: str) -> str: # Bewusst KEINE Rollen im Token: get_current_user liest Rollen bei jedem Request # frisch aus der DB (Rollenänderung wirkt sofort, kein Token-Refresh nötig). expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) payload = {"sub": subject, "exp": expire} return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) class InvalidTokenError(Exception): pass def decode_access_token(token: str) -> dict: try: return jwt.decode( token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm], options={"require": ["exp", "sub"]}, ) except jwt.PyJWTError as exc: raise InvalidTokenError(str(exc)) from exc