Skip to main content

Authentification backoffice & 2FA

backend/src/controllers/auth.controller.js + frontend/src/views/backoffice/LoginView.vue + frontend/src/stores/auth.js.

Dernière vérification contre le code

Vérifié ligne à ligne le 2026-08-13, auth.controller.js lu intégralement.

1. Flux de connexion

POST /api/auth/login (public) vérifie l'email et le mot de passe (bcrypt.compare). Deux issues possibles, sans jamais émettre de token à ce stade :

  • Aucun two_factor_secret en base (premier login) : un secret TOTP est généré (otplib) et sauvegardé immédiatement, un QR code est renvoyé (require2faSetup: true). Le compte a donc un secret 2FA en base dès la première tentative de connexion, avant même que l'utilisateur ait scanné quoi que ce soit.
  • Secret déjà présent : réponse require2fa: true, le code à 6 chiffres est attendu.

POST /api/auth/verify-2fa (public) vérifie authenticator.check(token, secret). C'est cet endpoint — pas un endpoint séparé de confirmation — qui bascule two_factor_enabled à true la toute première fois que le code est validé. C'est seulement après validation qu'un JWT est émis (expiresIn: '8h', contient id, role, email, id_agence, id_zone, permissionsid_zone ajouté le 2026-08-13, absent avant : sans lui, tout filtrage backend basé sur la zone d'un CHEF_ZONE était systématiquement inopérant, voir Agences, zones & GAB).

Il existe séparément POST /api/auth/setup-2fa et POST /api/auth/confirm-2fa (protégés, JWT requis) permettant de régénérer un secret 2FA une fois déjà connecté. Ce chemin sert à une reconfiguration ultérieure — LoginView.vue ne l'appelle jamais au premier login, qui passe uniquement par /login puis /verify-2fa.

Secret TOTP stocké en clair

users.two_factor_secret est une colonne String non chiffrée. Pas de champ de rotation ni de codes de secours (backup codes).

2. Le bypass 000000 n'existe pas

Il n'existe aucun code de contournement développeur pour la 2FA — verify2fa, confirm2fa et setup2fa n'appellent que authenticator.check(token, secret), aucune branche ne compare token à '000000'. Le README backend et un commentaire dans SchoolLoanView.vue affirmaient à tort le contraire — corrigés le 2026-08-13 (le README ne mentionne plus ce bypass ; le commentaire de SchoolLoanView.vue a été reformulé pour ne plus citer ce faux précédent). Le placeholder OTP 000000 du formulaire de prêt scolaire reste en place tel quel — c'est un mécanisme distinct et volontaire (en attendant l'intégration Sopra Banking Amplitude), qui n'a jamais eu besoin de ce faux parallèle pour se justifier.

3. Deux chemins de création de compte

  • POST /api/users (verifyToken + isSuperAdmin, voir Utilisateurs & rôles) : crée l'utilisateur avec password_hash: null (compte inutilisable tant que le mot de passe n'est pas défini), génère setup_token (32 octets aléatoires, crypto.randomBytes) valable 24h, envoie un email d'invitation. C'est le chemin utilisé par UsersView.vue.
  • POST /api/auth/register (réservé isSuperAdmin) : crée directement un compte avec mot de passe fourni en clair dans la requête, pas de token d'invitation, pas d'email envoyé. Chemin distinct, non utilisé par le frontend actuel.

Flux d'invitation (POST /api/users)

  1. mailer.sendInvitationEmail (backend/src/utils/mailer.js) construit le lien à partir de process.env.FRONTEND_URL (repli sur http://localhost:5173 si absent — corrigé le 2026-08-13, l'URL était auparavant codée en dur).
  2. SetupPasswordView.vue lit token depuis l'URL, poste sur /auth/setup-password — aucune vérification d'expiration côté client, l'erreur "lien invalide ou expiré" vient uniquement de la réponse backend (setup_token_expires_at dépassé).
  3. À la validation, setup_token/setup_token_expires_at sont effacés — le lien ne peut servir qu'une fois.

4. Pièges connus pour un futur développeur

  1. POST /api/users sans restriction de rôlecorrigé le 2026-08-13, voir Utilisateurs & rôles.
  2. La doc Swagger de users.routes.js est obsolète : elle indique password comme champ requis du body de création, alors que create() n'utilise jamais de mot de passe (le flux passe par le token d'invitation).
  3. LoginView.vue contient un tableau quickLogins avec des identifiants en clair par rôle et une fonction quickLogin()code mort, jamais rendu dans le template. Ne pas supposer que ce raccourci est utilisable en l'état.
  4. Le mot de passe n'est jamais loggé dans l'historique (activity-log) même en cas de modification — seul un indicateur (mot de passe modifié) apparaît dans le diff, jamais la valeur.
  5. Voir aussi Utilisateurs & rôles pour la suite (gestion des rôles/permissions, journalisation).