Skip to content

feat(admin): confirmer la connexion avant une action sensible - #550

Merged
PaulThiberville merged 2 commits into
mainfrom
issue/528-recent-second-factor
Oct 21, 2026
Merged

PaulThiberville merged 2 commits into
mainfrom
issue/528-recent-second-factor

Conversation

@Nascimento95

@Nascimento95 Nascimento95 commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Résultat et problème

Une session d'admin restait valable 12 h pour changer un rôle, suspendre ou supprimer un compte : un poste laissé ouvert suffisait. Après 10 minutes sans nouvelle connexion avec le code de l'application d'authentification, ces trois actions demandent maintenant de se reconnecter.

  • API : une garde requireRecentSignIn compare auth_time du jeton (la dernière connexion, avec son second facteur) à un délai, et répond 403 recent_sign_in_required au-delà. Elle protège PATCH /admin/users/:id/role, POST /admin/users/:id/suspend et DELETE /admin/users/:id. Lire une fiche et réactiver un compte restent libres. Le délai vient de ADMIN_RECENT_SIGN_IN_MINUTES : 10 par défaut (la règle du wiki), entre 1 et 720 ; plus court seulement pour essayer en local. Avec l'émulateur Firebase local, qui n'a pas de second facteur, la règle est ignorée, comme pour mfa_required.
  • Admin : à la réponse recent_sign_in_required, la fiche du compte ouvre une fenêtre « Reconnexion nécessaire » au lieu du message « Votre rôle ne permet pas cette action » (qui était faux : le rôle est bon). Elle explique que l'action demande une connexion récente avec le code de l'application d'authentification, et propose « Se reconnecter » ou « Annuler ». « Se reconnecter » ferme la session ; la page de connexion garde l'adresse, la personne revient sur la même page et recommence l'action.

Choix à relire : une fenêtre qui renvoie à la connexion, pas une saisie du code seul. Le ticket demande de « redemander le code TOTP ». Le SDK Firebase du navigateur ne sait pas redemander le second facteur seul : rafraîchir auth_time demande de se ré-authentifier en entier (fenêtre Google, puis le code). La fenêtre de reconnexion est donc le moyen le plus proche du ticket que l'API permet. Décision de l'opérateur (Marco, 2026-10-20) ; une saisie du code dans une fenêtre, sans repasser par Google, demanderait un autre mécanisme d'authentification côté API.

Limites :

Fixes #528
Écrans : A-06 (fiche d'un compte)
Dépendance : PR empilée sur #501 (ticket #344), elle-même sur #500, #499, #479, #398, #365 et #363 (base issue/344-affiliation).

Comment vérifier

Montage local de la pile, base locale migrée, compte admin connecté avec un second facteur (Firebase de staging), et dans apps/api/.env : ADMIN_RECENT_SIGN_IN_MINUTES=5 (pour ne pas attendre 10 minutes).

  1. Se connecter en admin et noter l'heure. Utilisateurs, ouvrir un compte, changer son rôle : l'action marche.
  2. Attendre 5 minutes sans se reconnecter.
  3. « Suspendre » un autre compte, avec un motif : aucun message sur le rôle ; la fenêtre « Reconnexion nécessaire » s'ouvre, le compte n'est pas suspendu.
  4. « Annuler » : la fenêtre se ferme, la session reste ouverte.
  5. Recommencer, « Se reconnecter » : la page de connexion s'ouvre, puis, après le code, la page des utilisateurs revient. Suspendre à nouveau : l'action marche.
  6. Dans le journal des actions (A-12), la suspension et le changement de rôle sont présents ; la tentative refusée n'a laissé aucune ligne.

Tests : 3 tests d'API (admin-users.test.ts) : les trois actions refusées après le délai, sans rien écrire (journal et ligne du compte inchangés) ; la même action acceptée avec une connexion récente ; la lecture d'une fiche et la réactivation toujours libres avec une connexion ancienne. Un test de configuration (délai : défaut 10, valeur 5, refus de 0). Un test admin pour reconnaître ce refus-là et aucun autre 403.

Testé par Marco le 2026-10-20, en local, sur le commit 256a22d (Firebase de staging, base locale, délai à 5 minutes) : le changement de rôle avant le délai, puis, après le délai, la fenêtre « Reconnexion nécessaire » à la place du message sur le rôle, « Annuler », et « Se reconnecter » jusqu'au retour sur la page des utilisateurs avec l'action qui passe. Rien n'a été testé sur un environnement déployé, ni avec un compte connecté par lien e-mail (la reconnexion passe par la même page de connexion).

  • pnpm lint && pnpm typecheck && pnpm test verts en local

Formatage mécanique : non

Risque

Niveau : 3 — authentification et session de l'admin : une garde de plus sur le changement de rôle, la suspension et la suppression d'un compte.

Taille : 95 lignes ajoutées côté API (tests compris) et une soixantaine côté admin ; aucun commit au-dessus de 400 lignes. Aucun fichier généré.

Revue OWASP (Top 10, édition en cours)

  • A07 Échecs d'identification et d'authentification : une action sensible exige une authentification récente avec le second facteur ; le délai part de auth_time du jeton vérifié par Firebase (jamais d'une valeur envoyée par le client). Une session ancienne ne peut plus, seule, changer un rôle, suspendre ou supprimer un compte.
  • A01 Contrôle d'accès : la garde vient après requireAdminSection (rôle, session de 12 h, second facteur) ; les règles de allowedUserActions et les verrous de chaque écriture sont inchangés. Un refus pour connexion ancienne n'écrit rien (testé : journal et compte inchangés).
  • A05 Mauvaise configuration : ADMIN_RECENT_SIGN_IN_MINUTES est borné (1 à 720) et validé au démarrage ; sa valeur par défaut est celle du wiki. Il n'y a pas de moyen de désactiver la garde, sauf l'émulateur Firebase local, déjà refusé en production par config.ts.
  • A09 Journalisation : un refus ne laisse pas de ligne dans admin_actions ; les actions faites après reconnexion s'y écrivent comme avant.

Nouvelle dépendance : aucune.

Retour arrière

Le revert de la PR suffit : aucune migration, aucune donnée créée. La variable ADMIN_RECENT_SIGN_IN_MINUTES est facultative ; sans le code qui la lit, elle est ignorée.

🤖 Generated with Claude Code

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche remise sur la base de la PR #398 corrigée (test instable de admin-catalog-places). Aucun commit n'est ajouté ni modifié ici : les commits sont les mêmes, seuls leurs identifiants changent. Lint, types et tests verts en local.

@PaulThiberville

Copy link
Copy Markdown
Collaborator

📋 MANAGER, 2026-10-21 — relecture de la pile admin pour Paul (tête 256a22d). Récapitulatif : Inbox.

Verdict : A, fusionnable telle quelle. Niveau 3, cohérent (session et authentification de l'admin). La fusion attend une demande explicite de Paul.

Vérifié :

  • requireRecentSignIn lit auth_time du jeton vérifié, jamais une valeur du client. Il vient après le crochet de section et protège le rôle, la suspension et la suppression.
  • Un refus n'écrit rien (testé). Le délai est borné de 1 à 720 minutes, 10 par défaut.

Choix à valider par Paul : #528 demande de « redemander le code TOTP ». La PR renvoie plutôt à une reconnexion complète (Google puis le code), parce que le SDK web de Firebase ne sait pas redemander le second facteur seul. Ce choix a été pris par Marco comme opérateur de sa session. Il est conforme à la sécurité visée, mais le critère est rempli au sens large.

Les réglages, que le wiki range aussi parmi les actions sensibles, sont dans #535. Labels type:feature et area:admin absents.

Base automatically changed from issue/344-affiliation to main October 21, 2026 16:15
Nascimento95 and others added 2 commits October 21, 2026 18:15
…a deletion

A sensitive action of the admin on an account is refused with 403
recent_sign_in_required when the last sign-in, with its second factor, is older
than ADMIN_RECENT_SIGN_IN_MINUTES (10 by default). Reading a card and lifting a
suspension stay free. The local Auth emulator, which has no second factor, is
left alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…for an old sign-in

When the API answers 403 recent_sign_in_required to a role, a suspension or a
deletion, the card of the account opens a window instead of the toast « Votre rôle
ne permet pas cette action »: it explains that the action needs a recent sign-in
with the authenticator code, and offers to sign in again, back to the same page.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@PaulThiberville
PaulThiberville force-pushed the issue/528-recent-second-factor branch from 256a22d to aa4e44a Compare October 21, 2026 16:17
@PaulThiberville
PaulThiberville merged commit f2963f4 into main Oct 21, 2026
5 checks passed
@PaulThiberville
PaulThiberville deleted the issue/528-recent-second-factor branch October 21, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:3 Risque niveau 3 (SECURITY.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin : redemander le code TOTP avant une action sensible si la dernière saisie date de plus de 10 minutes

2 participants