# PTA V7.12 — Second facteur pour les comptes Service Enfance et DSI ## Le problème traité La V7.11 a remplacé le sélecteur de rôle par un jeton personnel. C'était un progrès décisif, mais un jeton reste **un secret qui circule** : il est produit par la DSI, transmis par courriel ou sur papier, parfois recopié dans un carnet. Un jeton intercepté suffisait à valider les plannings de toute la collectivité (Service Enfance), voire à distribuer de nouveaux accès (DSI). Le risque n'est pas le même selon le compte. Un agent ne peut que lire son propre planning ; un responsable ne touche qu'aux brouillons de son lieu. Ces deux rôles-là restent à un seul facteur — alourdir leur connexion coûterait plus qu'il ne protégerait. ## Ce qui change ### 1. Deux facteurs, mais seulement là où c'est justifié | Rôle | Jeton | Mot de passe | | --- | --- | --- | | Agent | oui | — | | Responsable de structure | oui | — | | **Service Enfance** | oui | **oui** | | **DSI** | oui | **oui** | La règle vit **en base**, dans la vue `v_acces_compte` (`mot_de_passe_requis`), comme le reste des droits depuis la V7.8 : étendre le second facteur à un autre rôle est une modification SQL, pas un redéploiement. Les deux secrets sont volontairement de nature différente : le jeton est **remis** par la DSI, le mot de passe est **choisi** par le titulaire et n'est jamais affiché nulle part. ### 2. Le mot de passe se choisit à la première connexion La DSI ne pose aucun mot de passe initial, et ne peut donc en connaître aucun. Le parcours : 1. la DSI crée le compte et remet le jeton ; 2. à la première connexion, le jeton est reconnu et l'écran **impose de choisir un mot de passe** ; 3. aux connexions suivantes : jeton, puis mot de passe. Entre les deux étapes, **rien n'est ouvert** : la session ne porte qu'une attente horodatée, périmée au bout de cinq minutes. Un jeton validé sans mot de passe ne donne accès à aucune page — vérifié : `dsi.php` renvoie alors vers l'écran de connexion. Le compte est **relu en base entre les deux étapes** : si la DSI révoque le compte pendant que l'utilisateur tape son mot de passe, la session ne s'ouvre pas. ### 3. Politique : la longueur, et rien d'autre - **12 caractères minimum**, 72 maximum. Aucune règle de composition : exiger une majuscule et un chiffre produit `Motdepasse1`, pas un secret. Une phrase dont on se souvient vaut mieux. - La borne haute n'est pas arbitraire : `password_hash()` utilise bcrypt, qui **ignore silencieusement** tout ce qui dépasse 72 octets. Mieux vaut refuser que laisser croire. - Le jeton lui-même est refusé comme mot de passe : le recopier ramènerait les deux facteurs à un seul secret. - Hachage par `password_hash()` (bcrypt), avec remise à niveau automatique si PHP change d'algorithme par défaut. C'est un secret choisi par un humain, donc attaquable par dictionnaire : contrairement au jeton, le hachage lent a ici tout son sens. ### 4. Changement et oubli - **Changer son mot de passe** : bouton « Mot de passe » dans l'en-tête, visible des seuls comptes concernés. L'ancien mot de passe est exigé — sans quoi un poste laissé ouvert suffirait à confisquer un compte. - **Oubli** : la DSI **efface** le mot de passe depuis sa page (bouton « Effacer le mot de passe »). Elle ne le remplace pas : le titulaire en choisit un nouveau à sa connexion suivante, avec son jeton inchangé. La DSI ne connaît donc jamais le mot de passe de personne, pas même à l'instant de la réinitialisation. - La colonne « État » de la liste des comptes affiche « Mot de passe défini » ou « Mot de passe à définir ». Le second n'est pas une anomalie : c'est l'état normal d'un compte neuf. ### 5. Le frein anti-force-brute couvre les deux facteurs Le compteur du journal (`JournalModel::echecsRecents()`) additionne désormais `JETON_REFUSE` et `MOT_DE_PASSE_REFUSE` : essayer mille mots de passe derrière un jeton volé bute sur le même mur qu'essayer mille jetons — 10 échecs en 15 minutes par adresse IP, puis plus rien n'est comparé. ### 6. Ce que le journal enregistre en plus | Action | Catégorie | Quand | | --- | --- | --- | | `JETON_ACCEPTE_ATTENTE_MDP` | `CONNEXION` | Jeton reconnu, mot de passe attendu | | `MOT_DE_PASSE_REFUSE` | `CONNEXION` | Mot de passe incorrect derrière un jeton valide | | `MOT_DE_PASSE_DEFINI` | `ECRITURE` | Premier choix par le titulaire | | `MOT_DE_PASSE_MODIFIE` | `ECRITURE` | Changement par le titulaire | | `MOT_DE_PASSE_REINITIALISE` | `ADMINISTRATION` | Effacement par la DSI | `CONNEXION_REUSSIE` porte en contexte les facteurs réellement présentés (`["jeton"]` ou `["jeton","mot_de_passe"]`) : le journal distingue donc une connexion à un facteur d'une connexion à deux. ## Mise en service Rien à faire hors la migration `database/migration_v7_9f_mot_de_passe_admin.sql`, appliquée automatiquement au démarrage. **Les comptes Service Enfance et DSI existants n'ont pas de mot de passe** : leur prochaine connexion, jeton en main, leur en fera choisir un. Aucun jeton n'est invalidé, aucun compte n'est bloqué. Prévenez les titulaires concernés : ils verront un écran qu'ils n'attendent pas. ### Si le dernier accès DSI perd son mot de passe Le cas ne se résout pas dans l'interface, puisqu'il faudrait être connecté pour le faire. En SQL : ```sql UPDATE compte_acces SET mot_de_passe_hash = NULL, date_mot_de_passe = NULL WHERE id_compte = 1; ``` La connexion suivante, avec le jeton, redemandera d'en définir un. Si le jeton lui-même est perdu, voir la procédure de `README_V7_11.md`. ## Fichiers | Fichier | Rôle | | --- | --- | | `database/migration_v7_9f_mot_de_passe_admin.sql` | Colonnes `mot_de_passe_hash` / `date_mot_de_passe`, vue `v_acces_compte` enrichie de `mot_de_passe_requis` et `mot_de_passe_defini` | | `app/Controllers/RoleController.php` | Connexion en deux étapes, définition, vérification, changement | | `app/Models/CompteModel.php` | Politique, hachage, vérification, réinitialisation | | `app/Models/JournalModel.php` | Frein étendu au second facteur | | `app/Controllers/DsiController.php`, `api/dsi_compte_mot_de_passe.php` | Effacement par la DSI | | `app/Views/pages/role.php` | Les quatre états de l'écran de connexion | | `app/Views/layouts/header.php` | Lien « Mot de passe », pour les seuls comptes concernés | ## Ce que la V7.12 ne fait pas - **Pas de verrouillage du compte** après N échecs : seul le frein par adresse IP s'applique. Un verrouillage par compte permettrait à quiconque connaît un jeton de bloquer volontairement un administrateur. - **Pas d'expiration ni d'historique des mots de passe** : forcer un changement tous les trois mois produit `Motdepasse-Mars`, `Motdepasse-Juin`. Le mot de passe se change quand il y a une raison. - **Pas de réinitialisation par courriel** : il n'y a pas d'envoi de courriel dans PTA, et la boîte aux lettres deviendrait le vrai facteur d'authentification. - **Pas de TOTP** : le jeton joue déjà le rôle du « quelque chose que l'on détient ». Un authentificateur mobile serait le prolongement naturel si le besoin s'en faisait sentir.