""" Signierte, serverseitig invalidierbare Session-Cookies. Kein separates Session-Store noetig: Das Cookie traegt user_id, session_version (fuer harte Invalidierung, z.B. bei Passwortwechsel) und zwei Zeitstempel (Login-Zeit fuer den absoluten Timeout, Last-Seen fuer den gleitenden Idle-Timeout). Signatur ueber einen vom KEK getrennten Secret (Schluesseltrennung, Konzept 6.2). """ from __future__ import annotations import time from dataclasses import dataclass from itsdangerous import BadSignature, URLSafeSerializer from app.config import settings _serializer = URLSafeSerializer(settings.session_secret.hex(), salt="jumphost-session") SESSION_COOKIE_NAME = "jh_session" @dataclass class SessionPayload: user_id: int session_version: int login_ts: float last_seen_ts: float def create_session_token(user_id: int, session_version: int) -> str: now = time.time() payload = {"uid": user_id, "sv": session_version, "iat": now, "seen": now} return _serializer.dumps(payload) def refresh_session_token(payload: SessionPayload) -> str: data = { "uid": payload.user_id, "sv": payload.session_version, "iat": payload.login_ts, "seen": time.time(), } return _serializer.dumps(data) def decode_session_token(token: str) -> SessionPayload | None: try: data = _serializer.loads(token) except BadSignature: return None try: return SessionPayload( user_id=int(data["uid"]), session_version=int(data["sv"]), login_ts=float(data["iat"]), last_seen_ts=float(data["seen"]), ) except (KeyError, TypeError, ValueError): return None def is_expired(payload: SessionPayload) -> bool: now = time.time() if now - payload.last_seen_ts > settings.session_idle_timeout_s: return True if now - payload.login_ts > settings.session_absolute_timeout_s: return True return False