Files
timemaster/backend/app/core/security.py
T
patrickandClaude Sonnet 5 edb727e09a
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled
fix(security): python-jose CVE-2024-33663 durch pyjwt ersetzt, Deps gepinnt
- security.py/dependencies.py/auth.py: jose -> pyjwt (unmaintained,
  Algorithm-Confusion-CVE). API-kompatibel (jwt.encode/decode gleich).
- requirements.txt: alle Versionen gepinnt (waren >=, jetzt == anhand
  aktueller 137-Installation) fuer reproduzierbare Deploys.
- nginx.conf: /docs + /openapi.json nur noch aus LAN erreichbar (waren
  oeffentlich, API-Struktur-Leak).

fail2ban auf 137+164 installiert (sshd + nginx-badbots + timemaster-auth
Jails), Configs nicht im Repo (Server-only, /etc/fail2ban/).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
2026-08-05 19:10:28 +02:00

83 lines
3.3 KiB
Python

from datetime import datetime, timedelta, timezone
from typing import Any
import secrets
import hashlib
import bcrypt
import jwt
from jwt import PyJWTError as JWTError
from app.core.config import settings
# ── Password ────────────────────────────────────────────────────────────────
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()
def verify_password(plain_password: str, hashed_password: str) -> bool:
return bcrypt.checkpw(plain_password.encode(), hashed_password.encode())
def hash_token(token: str) -> str:
"""SHA-256 hash for storing tokens (refresh, invite, reset) in DB."""
return hashlib.sha256(token.encode()).hexdigest()
# ── JWT ─────────────────────────────────────────────────────────────────────
def create_access_token(subject: str, extra: dict[str, Any] | None = None) -> str:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.access_token_expire_minutes
)
payload = {"sub": subject, "exp": expire, "type": "access"}
if extra:
payload.update(extra)
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
def create_refresh_token() -> tuple[str, str]:
"""Returns (raw_token, hashed_token). Store hash in DB, send raw to client."""
raw = secrets.token_urlsafe(64)
return raw, hash_token(raw)
def decode_access_token(token: str) -> dict[str, Any]:
"""Raises JWTError on invalid/expired token."""
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
if payload.get("type") != "access":
raise JWTError("Invalid token type")
return payload
# ── Partial token (TOTP pending) ─────────────────────────────────────────────
def create_partial_token(user_id: str) -> str:
"""Short-lived token issued after password-OK but before TOTP verification. Valid 5 min."""
expire = datetime.now(timezone.utc) + timedelta(minutes=5)
payload = {"sub": user_id, "exp": expire, "type": "partial"}
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
def decode_partial_token(token: str) -> str:
"""Returns user_id (sub). Raises JWTError on invalid/expired/wrong-type token."""
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
if payload.get("type") != "partial":
raise JWTError("Invalid token type")
return payload["sub"]
# ── One-time tokens ──────────────────────────────────────────────────────────
def generate_invite_token() -> tuple[str, str]:
"""Returns (raw, hashed). Invite valid for 7 days."""
raw = secrets.token_urlsafe(32)
return raw, hash_token(raw)
def generate_reset_token() -> tuple[str, str]:
"""Returns (raw, hashed). Reset valid for 1 hour."""
raw = secrets.token_urlsafe(32)
return raw, hash_token(raw)