test(backend): éprouve le parcours d'authentification contre la vraie base

Six scénarios bout en bout, sans serveur ni port ouvert : connexion,
rotation, déconnexion, rejeu d'un cookie déjà tourné, révocation
immédiate et enregistrement d'une tentative sur adresse inconnue.

Le scénario du rejeu vérifie aussi que la session encore vivante tombe
avec sa famille : c'est la propriété qui distingue la détection de la
simple rotation, et elle ne se démontre pas sur un double.

Corrige un défaut que ce parcours a révélé : `iat` est une date JWT, donc
en secondes entières, et `datetime.fromtimestamp` tronque. Tout jeton
émis dans la même seconde que `credentials_changed_at` était rejeté, ce
qui aurait déconnecté l'appareil courant à chaque changement de mot de
passe, exactement l'inverse de ce que `/auth/password` promet.
This commit is contained in:
Johan LEROY
2026-09-15 14:57:42 +02:00
parent e8f22bf427
commit cb2ac8c2c2
2 changed files with 166 additions and 1 deletions
+4 -1
View File
@@ -149,7 +149,10 @@ async def get_current_principal(
compte = await UserRepository(session).get_by_id(claims.subject)
if compte is None or not compte.is_active:
raise _non_authentifie("invalid_token")
if claims.issued_at < compte.credentials_changed_at:
# Piège : `iat` est une date JWT, donc en secondes entières. Comparer sans tronquer le
# marqueur rejetterait tout jeton émis dans la même seconde que le changement, c'est-à-dire
# celui que `/auth/password` vient de rendre pour garder l'appareil courant connecté.
if int(claims.issued_at.timestamp()) < int(compte.credentials_changed_at.timestamp()):
raise _non_authentifie("token_stale")
if claims.role != compte.role:
raise _non_authentifie("token_stale")