This commit is contained in:
PTA
2026-08-20 17:00:06 +02:00
parent 5d8c0993a6
commit 94e7f09eb4
58 changed files with 5605 additions and 615 deletions

133
docs/README_V7_12.md Normal file
View File

@@ -0,0 +1,133 @@
# 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.