Sécurise le cookie de session par signature HMAC

Le cookie user_id était lu tel quel depuis la requête, ce qui permettait à
n'importe qui de forger `user_id=victime` dans DevTools/curl et d'accéder à
toutes les routes "authentifiées" (lecture/écriture des pointages, export,
régénération des tokens Tasker, etc.). Le mot de passe bcrypt ne servait
donc à rien.

Le cookie est maintenant au format `<user_id>.<hmac_sha256_hex>` signé avec
une clé persistée dans /data/secret.key (auto-générée, stable entre
redémarrages). La vérification utilise hmac.compare_digest pour éviter les
fuites par timing. Toutes les routes appellent _get_user qui valide la
signature à chaque requête.

Les tests de test_logs.py récupèrent désormais le cookie réellement signé
par le serveur, et un nouveau module test_security.py couvre le rejet des
cookies forgés/tamponnés/rejoués.

Le DATA_DIR temporaire des tests est centralisé dans conftest.py pour éviter
les collisions entre modules de test.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-07-20 00:20:50 +02:00
parent 55efac9cdd
commit 8dc2a3fa4c
5 changed files with 171 additions and 18 deletions

View File

@ -1,16 +1,9 @@
"""Smoke tests pour les nouvelles routes /logs et /aide + journalisation présence."""
import sys
import tempfile
from pathlib import Path
# Prépare un DATA_DIR temporaire AVANT l'import de models
_TMP = Path(tempfile.mkdtemp())
import models
models.DATA_DIR = _TMP
models.CONFIG_FILE = models.DATA_DIR / "config.json"
models.CONFIG_FILE.write_text('{"heures_jour": 7.8, "allowed_email_domain": "x.fr"}')
# DATA_DIR temporaire est positionné par conftest.py avant l'import de models.
import models # noqa: F401 (réexport pratique pour les tests ci-dessous)
# Recharge main dans un état propre
sys.path.insert(0, ".")
import main
from fastapi.testclient import TestClient
@ -19,14 +12,15 @@ client = TestClient(main.app)
def _login_as(uid: str):
"""Crée un user avec mdp puis se connecte (renvoie les cookies)."""
"""Crée un user avec mdp puis se connecte (renvoie les cookies signés)."""
import bcrypt
models.save_auth(uid, email=f"{uid}@x.fr",
password_hash=bcrypt.hashpw(b"x" * 12, bcrypt.gensalt()).decode())
r = client.post("/login/password", data={"email": f"{uid}@x.fr", "password": "x" * 12},
follow_redirects=False)
assert r.status_code == 302, r.text
return {"user_id": uid}
# Récupère le cookie user_id réellement signé par le serveur
return {"user_id": r.cookies.get("user_id")}
COOKIES = _login_as("alice")