Authentification backoffice & 2FA
backend/src/controllers/auth.controller.js + frontend/src/views/backoffice/LoginView.vue + frontend/src/stores/auth.js.
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_secreten 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, permissions — id_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.
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 — mécanisme distinct, sans lien avec la 2FA backoffice — n'existe d'ailleurs lui-même plus depuis le 2026-08-24 : l'OTP de ce formulaire envoie désormais un vrai SMS avec un code aléatoire (voir OTP, SMS & vérification SBA).
3. Deux chemins de création de compte
POST /api/users(verifyToken+isSuperAdmin, voir Utilisateurs & rôles) : crée l'utilisateur avecpassword_hash: null(compte inutilisable tant que le mot de passe n'est pas défini), génèresetup_token(32 octets aléatoires,crypto.randomBytes) valable 24h, envoie un email d'invitation. C'est le chemin utilisé parUsersView.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)
mailer.sendInvitationEmail(backend/src/utils/mailer.js) construit le lien à partir deprocess.env.FRONTEND_URL(repli surhttp://localhost:5173si absent — corrigé le 2026-08-13, l'URL était auparavant codée en dur).SetupPasswordView.vuelittokendepuis 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_atdépassé).- À la validation,
setup_token/setup_token_expires_atsont effacés — le lien ne peut servir qu'une fois.
4. "Mot de passe oublié" — ajouté le 2026-08-24 des deux côtés, mais toujours cassé (deux URL codées en dur)
LoginView.vue a un lien "Oublié ?" vers ForgotPasswordView.vue (/admin/forgot-password), qui envoie vers ResetPasswordView.vue (/admin/reset-password) — un vrai flux UI (formulaire email → lien de réinitialisation → nouveau mot de passe) existe côté frontend.
Le backend a rattrapé son retard le même jour (cdde188, 2026-08-20, mais visible seulement après un second git pull du dépôt backend le 2026-08-24) : POST /api/auth/forgot-password/POST /api/auth/reset-password existent désormais (auth.controller.js), avec de bonnes pratiques — ne révèle jamais si l'email existe en base, token crypto.randomBytes(32) unique (users.reset_token, expire en 15 min), regex de mot de passe fort exigée à la réinitialisation.
- Backend :
services/mailer.js::sendResetPasswordEmailconstruit le lien de réinitialisation avec`http://localhost:5173/admin/reset-password?token=${token}`codé en dur, alors queprocess.env.FRONTEND_URLexiste déjà dans.env.exampleet sert déjà exactement à ça pour l'email d'invitation (utils/mailer.js, corrigé le 2026-08-13 pour la même raison — voir Dette technique connue). - Frontend :
ForgotPasswordView.vue/ResetPasswordView.vueappellent une URL codée en dur (fetch('http://localhost:5000/api/auth/forgot-password', ...)) au lieu de passer par l'instanceapi(axios,src/api/axios.js) utilisée partout ailleurs dans l'app avec unebaseURLconfigurable.
En pratique, un déploiement hors localhost casse silencieusement des deux côtés : l'email de réinitialisation pointe vers un lien injoignable, et même en le corrigeant à la main, le formulaire d'origine ne contacterait de toute façon pas le bon backend.
Voir aussi Bugs & failles actives connues.
5. Pièges connus pour un futur développeur
— corrigé le 2026-08-13, voir Utilisateurs & rôles.POST /api/userssans restriction de rôle- La doc Swagger de
users.routes.jsest obsolète : elle indiquepasswordcomme champ requis du body de création, alors quecreate()n'utilise jamais de mot de passe (le flux passe par le token d'invitation). LoginView.vuecontient un tableauquickLoginsavec des identifiants en clair par rôle et une fonctionquickLogin()— code mort, jamais rendu dans le template. Ne pas supposer que ce raccourci est utilisable en l'état.- 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. - "Mot de passe oublié" est cassé de bout en bout (section 4) — ne pas supposer que ce lien fonctionne avant d'avoir vérifié l'état du backend et l'URL codée en dur.
- Voir aussi Utilisateurs & rôles pour la suite (gestion des rôles/permissions, journalisation).