Authentification admin¶
L'authentification admin contrôle l'accès aux interfaces formateur de PerfShop : panneau chaos-admin, monitoring (connexion admin), scripts-ui, jmeter-ui, et la gestion des comptes. Elle est distincte de l'authentification des utilisateurs du shop e-commerce (qui passe par AuthController) et du portail de sécurité vulnérable (AdminPortalController, réservé au Chaos Sécurité Master).
Sources
backend/src/main/java/com/perfshop/controller/AdminAuth.java, AdminController.java, service/AdminUserService.java, entity/AdminUser.java, backend/src/main/resources/db/migration-fr/V1__schema.sql (table admin_users)
Modèle de compte¶
Un compte admin est représenté par l'entité AdminUser persistée dans la table admin_users créée par V1__schema.sql. Ses champs principaux :
| Colonne | Type | Rôle |
|---|---|---|
id |
BIGINT AUTO_INCREMENT | Identifiant unique |
email |
VARCHAR(100) UNIQUE | Identifiant de login |
password_hash |
VARCHAR(255) | Hash BCrypt strength 10 ($2b$10$...) |
is_superadmin |
BOOLEAN | true pour l'unique compte superadmin |
can_access_chaos |
BOOLEAN | Accès à chaos-admin et aux endpoints /api/chaos/* |
can_access_monitoring |
BOOLEAN | Accès au dashboard monitoring admin |
can_access_admin |
BOOLEAN | Accès au backoffice /api/admin/* |
can_access_jmeter |
BOOLEAN | Accès à jmeter-ui |
can_access_scripts |
BOOLEAN | Accès à scripts-ui |
created_at |
DATETIME | Date de création |
Hachage des mots de passe¶
Les mots de passe sont stockés hachés avec BCrypt strength 10. AdminUserService expose un BCryptPasswordEncoder statique :
La strength 10 signifie 2¹⁰ = 1024 itérations internes — un compromis raisonnable entre sécurité et latence de login. Le hash résultant commence par $2b$10$ et fait 60 caractères. La méthode utilitaire hashPassword(plain) est exposée en static pour être appelée depuis d'autres contrôleurs.
La vérification au login passe par passwordEncoder.matches(password, admin.getPasswordHash()) — BCrypt intègre son propre salt dans le hash, aucun sel externe n'est nécessaire.
Bootstrap du superadmin¶
Au démarrage de Spring Boot, AdminUserService écoute l'événement ApplicationReadyEvent et exécute bootstrapSuperAdmin() :
flowchart TD
Start([ApplicationReadyEvent]) --> Check{Compte<br/>superadmin<br/>existe ?}
Check -- non --> Create[INSERT avec BCrypt du<br/>PERFSHOP_ADMIN_PASSWORD]
Check -- oui --> Verify{Hash correspond<br/>au .env ?}
Verify -- oui --> Done([✅ Superadmin prêt])
Verify -- non --> UpdatePwd[UPDATE password_hash]
UpdatePwd --> Done
Create --> Done
Deux cas particuliers sont gérés :
-
Changement de mot de passe via
.env— siPERFSHOP_ADMIN_PASSWORDa été modifié depuis la dernière exécution, le hash stocké ne correspond plus au mot de passe demandé.AdminUserServicedétecte ce cas viapasswordEncoder.matches(...)et met à jour le hash. C'est une voie de secours si l'administrateur perd son mot de passe : il peut simplement le réinitialiser dans.envet redémarrer le conteneur. -
Rattrapage des droits JMeter/Scripts — pour les instances migrées depuis un schéma historique qui ne contenait pas encore ces deux drapeaux, le bootstrap les ajoute rétroactivement sur le compte superadmin.
Les valeurs par défaut utilisées à la création sont :
| Variable | Défaut |
|---|---|
perfshop.admin.email |
admin@perfshop.fr |
perfshop.admin.password |
perfshop |
Ces valeurs conviennent pour un environnement pédagogique isolé. Elles doivent être changées pour toute exposition publique.
Flux de login¶
Le flux de login formateur suit cette séquence :
sequenceDiagram
autonumber
participant C as chaos-admin login.html
participant B as AdminController
participant S as AdminUserService
participant DB as MySQL
participant Sess as HttpSession
C->>B: POST /api/admin/login { email, password }
B->>S: authenticate(email, password)
S->>DB: SELECT * FROM admin_users WHERE email=?
DB-->>S: AdminUser
S->>S: passwordEncoder.matches(password, hash)
alt credentials OK
S-->>B: Optional<AdminUser>.of(admin)
B->>Sess: setAttribute("admin_logged_in", true)
B->>B: generateAdminToken() + store
B-->>C: 200 { email, adminToken, isSuperAdmin, canAccessChaos, canAccessMonitoring, canAccessAdmin }
C->>C: sessionStorage.setItem('chaos_auth', 'true')
C->>C: sessionStorage.setItem('chaos_token', adminToken)
C->>C: redirect /admin/
else credentials KO
S-->>B: Optional.empty()
B-->>C: 401 { error: "credentials invalid" }
end
Point important : le backend refuse le login avec HTTP 402 si aucune licence n'est active, grâce à LicenseInterceptor qui bloque /api/admin/*. Or le login admin lui-même est sur /api/admin/login, donc l'interceptor le bloque bien. Conséquence : une instance sans licence ne peut pas être administrée via le panneau formateur. Voir Système de licence.
Les deux modes d'authentification¶
L'utilitaire AdminAuth.isAdmin(session, adminToken) accepte deux voies pour reconnaître un admin :
public static boolean isAdmin(HttpSession session, String adminToken) {
if (Boolean.TRUE.equals(session.getAttribute(ADMIN_SESSION_KEY))) return true;
return adminToken != null && !adminToken.isBlank()
&& AdminController.isValidAdminToken(adminToken);
}
Mode 1 — Session HTTP¶
Après un login réussi, AdminController pose l'attribut admin_logged_in = true sur la session HTTP. Tant que la session persiste (cookie JSESSIONID + SameSite=lax), les appels suivants sont automatiquement authentifiés. C'est le mode standard pour les pages HTML servies depuis le même domaine (Docker Desktop, réseau local simple).
Mode 2 — Header X-Admin-Token¶
En complément, AdminController génère un token opaque à chaque login et le retourne dans la réponse. Les clients peuvent ensuite envoyer ce token dans le header X-Admin-Token sur chaque requête :
PUT /api/admin/accounts/42/rights HTTP/1.1
X-Admin-Token: abc123def456...
Content-Type: application/json
{ "canAccessChaos": true, ... }
Cette voie est nécessaire dans plusieurs cas de figure :
- Déploiement cross-origin — le panneau chaos-admin et le backend sont sur des domaines différents, donc le cookie de session ne suit pas les requêtes CORS
- Déploiement mixte HTTP/HTTPS — cookies
Securesur HTTPS, mais le client HTTP n'y a pas accès - Clients programmatiques — scripts curl, Postman, JMeter qui n'ont pas de cookie jar persistant
Le panneau chaos-admin utilise un wrapper adminFetch() qui injecte automatiquement X-Admin-Token depuis sessionStorage.chaos_token dans toutes les requêtes :
function adminFetch(url, opts = {}) {
const token = sessionStorage.getItem('chaos_token');
const headers = { ...(opts.headers || {}) };
if (token) headers['X-Admin-Token'] = token;
return fetch(url, { ...opts, credentials: 'include', headers });
}
Les deux voies cohabitent sans conflit : si l'une fonctionne, l'accès est accordé.
Protection du superadmin¶
Le superadmin (is_superadmin = true) bénéficie de trois protections codées en dur dans AdminUserService :
- Non-supprimable —
deleteAdmin(id)lèveIllegalStateExceptionsi le compte est superadmin - Droits figés —
updateRights(id, ...)lèveIllegalStateException— les droits du superadmin sont tous àtrueen permanence - Mot de passe modifiable —
updatePassword(id, newPassword)fonctionne pour tous les comptes, y compris le superadmin
Cette asymétrie garantit qu'il existe toujours un compte capable d'administrer la plateforme. Si un bug ou une mauvaise manipulation casse tous les autres comptes, le superadmin reste fonctionnel.
CRUD des comptes — AdminUserService¶
| Méthode | Rôle | Contrainte |
|---|---|---|
findAll() |
Lister tous les comptes | — |
findById(id) |
Rechercher par ID | — |
findByEmail(email) |
Rechercher par email | — |
authenticate(email, password) |
Vérifier credentials, retourne Optional<AdminUser> |
— |
createAdmin(...) |
Créer un compte | Email doit être unique |
deleteAdmin(id) |
Supprimer | Le superadmin est non-supprimable |
updatePassword(id, newPassword) |
Changer le mot de passe | Minimum 6 caractères |
updateRights(id, ...) |
Modifier les droits d'accès | Le superadmin a des droits fixes |
Les endpoints HTTP correspondants sont exposés par AdminController (voir API Administration).
Page mon-compte¶
Chaque admin connecté peut changer son propre mot de passe depuis chaos-admin/public/admin/mon-compte.html. Le script mon-compte.js appelle PUT /api/admin/accounts/me avec le nouveau mot de passe après vérification côté client de la confirmation. Le backend vérifie la longueur minimale (6 caractères) et met à jour le hash BCrypt via AdminUserService.updatePassword(). Voir Chaos admin (formateur).
Variables d'environnement¶
| Variable | Usage | Défaut |
|---|---|---|
PERFSHOP_ADMIN_EMAIL |
Email du superadmin (bootstrap) | admin@perfshop.fr |
PERFSHOP_ADMIN_PASSWORD |
Mot de passe du superadmin (bootstrap) | perfshop |
SESSION_COOKIE_SECURE |
Cookie Secure flag |
false |
SESSION_COOKIE_SAME_SITE |
Cookie SameSite |
lax |
Les deux valeurs par défaut doivent être changées en production via .env ou via la page mon compte après le premier login.