Aller au contenu

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 :

private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);

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 :

  1. Changement de mot de passe via .env — si PERFSHOP_ADMIN_PASSWORD a été modifié depuis la dernière exécution, le hash stocké ne correspond plus au mot de passe demandé. AdminUserService détecte ce cas via passwordEncoder.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 .env et redémarrer le conteneur.

  2. 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 Secure sur 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-supprimabledeleteAdmin(id) lève IllegalStateException si le compte est superadmin
  • Droits figésupdateRights(id, ...) lève IllegalStateException — les droits du superadmin sont tous à true en permanence
  • Mot de passe modifiableupdatePassword(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.