Flux d'authentification¶
PerfShop manipule trois mécanismes d'authentification distincts, chacun avec son périmètre, son support de transport et sa durée de vie. Cette page décrit chacun en détail avec un diagramme de séquence et les références au code source.
| Mécanisme | Acteur | Transport | Stockage côté serveur |
|---|---|---|---|
| Session utilisateur | Client final / étudiant jouant un rôle d'acheteur | Cookie HTTP JSESSIONID |
HttpSession Tomcat (attribut LOGGED_IN_USER) |
| Token admin | Formateur, accès programmatique aux outils admin | Header X-Admin-Token ou attribut session admin_logged_in |
Validation par AdminController.isValidAdminToken() |
| Token étudiant | Étudiant suivant un parcours pédagogique | Header X-Student-Token |
UUID stocké dans la table pedagogique_sessions |
Source de vérité
Toute cette page est tirée de :
controller/AuthController.java, service/AuthService.java, controller/AdminAuth.java, controller/AdminController.java, controller/ChaosStudentController.java, service/PedagogiqueSessionService.java, service/DefaultPedagogiqueSessionService.java, config/CorsConfig.java, config/WebConfig.java, et application.yml (section spring.servlet.session).
Mécanisme 1 — Session HTTP utilisateur¶
C'est l'authentification classique de la boutique. L'utilisateur s'inscrit ou se connecte via le frontend React, et le backend lui ouvre une session Tomcat dont l'identifiant transite par cookie.
Diagramme de séquence¶
sequenceDiagram
autonumber
actor U as Navigateur<br/>(client final)
participant FE as perfshop-frontend
participant BE as perfshop-app
participant AS as AuthService
participant DB as MySQL (users)
participant SS as SecurityTokenService
U->>FE: Saisit email + mot de passe
FE->>BE: POST /api/auth/login<br/>{email, password}
BE->>AS: login(email, password, session)
AS->>DB: findByEmail(email)
DB-->>AS: User (ou empty)
alt User absent
AS-->>BE: Optional.empty()
BE-->>FE: 401 {error: auth.error.credentials}
else User présent
AS->>AS: BCrypt.matches(password, user.password)
alt Hash invalide
AS-->>BE: Optional.empty()
BE-->>FE: 401 {error: auth.error.credentials}
else Hash valide
AS->>DB: UPDATE last_login
AS->>AS: session.setAttribute("LOGGED_IN_USER", user)
AS-->>BE: Optional.of(user)
BE->>SS: generateToken(session, userId)
SS-->>BE: securityToken
BE-->>FE: 200 {success, securityToken,<br/>id, email, firstName, lastName}<br/>Set-Cookie: JSESSIONID=...
end
end
Détails techniques¶
| Aspect | Implémentation |
|---|---|
| Endpoint | POST /api/auth/login (AuthController.login()) |
| DTO | LoginRequest validé par @Valid (champs email, password) |
| Hashage | BCrypt strength 10 (AuthService.passwordEncoder), bon compromis sécurité/perf (~300-500 ms par hash) |
| Migration des hash | Migration V29__hash_existing_passwords.sql — les mots de passe historiques en clair ont été migrés vers BCrypt |
| Stockage de l'utilisateur en session | session.setAttribute("LOGGED_IN_USER", user) (constante AuthService.SESSION_USER_KEY) |
| Lecture du user courant | AuthService.getCurrentUser(session) retourne le User ou null |
| Test de connexion | AuthService.isLoggedIn(session) |
| Logout | POST /api/auth/logout → AuthService.logout(session) → session.invalidate() |
| Token applicatif additionnel | SecurityTokenService.generateToken(session, userId) retourne un token signé qui sert au frontend pour les requêtes ultérieures (panier, checkout) |
Configuration des sessions HTTP¶
Dans application.yml, section spring.servlet.session :
| Paramètre | Valeur | Variable d'env |
|---|---|---|
timeout |
30m |
(figé) |
cookie.http-only |
true |
(figé) |
cookie.secure |
dépend du contexte | SESSION_COOKIE_SECURE (false en local, true en HTTPS) |
cookie.same-site |
dépend du contexte | SESSION_COOKIE_SAME_SITE (lax en local, none en HTTPS cross-site) |
CORS et headers exposés¶
CorsConfig.java autorise les origines listées dans CORS_ALLOWED_ORIGINS (par défaut frontend, monitoring, chaos-admin, admin) avec allowCredentials(true) — c'est ce qui permet au cookie JSESSIONID de traverser les requêtes cross-origin entre le frontend React et le backend Spring Boot.
Les headers personnalisés utilisés par le Chaos Scripting sont explicitement listés dans exposedHeaders(...) pour être lisibles par le JavaScript du navigateur :
Anomalie A11 — grace period sur logout
Le POST /api/auth/logout consulte BusinessChaosService.getTokenGracePeriodMs(email). Si l'anomalie A11 (niveau 3 du chaos métier) est active, la session n'est pas invalidée : seul l'attribut LOGGED_IN_USER est retiré, mais le securityToken et le bundle de scripting restent en session pendant la grace period (30 secondes par défaut). Toute la chaîne checkout reste exploitable pendant ce délai. C'est intentionnel et documenté dans le code de AuthController.logout().
Mécanisme 2 — Authentification admin¶
L'authentification admin protège l'accès à chaos-admin, au portail admin, et à tous les endpoints /api/admin/** et /api/chaos/**. Elle a deux modes simultanés, gérés par la classe utilitaire AdminAuth :
Mode A — Session admin (interface web)¶
Quand le formateur se connecte via la page de login de chaos-admin ou de admin, le backend ouvre une session standard et y pose la clé admin_logged_in = true.
Mode B — Header X-Admin-Token (programmatique)¶
Quand un script externe (curl, Postman, JMeter, Robot Framework) appelle un endpoint admin, il envoie le header HTTP X-Admin-Token valorisé avec un jeton statique configuré côté serveur. Aucune session n'est ouverte.
Diagramme de séquence¶
sequenceDiagram
autonumber
participant C as Client<br/>(navigateur OU script)
participant BE as perfshop-app
participant AA as AdminAuth.isAdmin()
participant AC as AdminController
alt Mode A — Session admin
C->>BE: POST /api/admin/login<br/>{email, password}
BE->>BE: Vérification BCrypt sur AdminUser
BE->>BE: session.setAttribute("admin_logged_in", true)
BE-->>C: 200 + Cookie JSESSIONID
Note over C,BE: Requêtes suivantes
C->>BE: GET /api/chaos/backend/state
BE->>AA: isAdmin(session, headerToken)
AA->>AA: session.getAttribute("admin_logged_in") == TRUE
AA-->>BE: true
BE-->>C: 200 {state: ...}
else Mode B — Header X-Admin-Token
C->>BE: GET /api/chaos/backend/state<br/>X-Admin-Token: <jeton>
BE->>AA: isAdmin(session, headerToken)
AA->>AC: AdminController.isValidAdminToken(token)
AC-->>AA: true / false
AA-->>BE: true
BE-->>C: 200 {state: ...}
end
Code de référence — AdminAuth.isAdmin()¶
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);
}
Tous les contrôleurs sensibles (ChaosController, BusinessChaosController, FunctionalChaosController, SecurityChaosController, ChaosScriptingController, ChaosStudentController côté admin, AdminController) appellent AdminAuth.isAdmin(session, headerToken) au début de chaque méthode et retournent HTTP 403 si le test échoue.
Comptes admin et droits granulaires¶
Les comptes admin sont stockés dans la table admin_users (entité AdminUser, migration V30__create_admin_users.sql) avec cinq droits indépendants :
| Droit | Constante | Périmètre |
|---|---|---|
can_access_chaos |
canAccessChaos |
Interface chaos-admin |
can_access_monitoring |
canAccessMonitoring |
Dashboard perfshop-monitoring |
can_access_admin |
canAccessAdmin |
Backoffice admin (produits, commandes, comptes) |
can_access_jmeter |
canAccessJmeter |
Interface perfshop-jmeter-ui (ajouté en V31) |
can_access_scripts |
canAccessScripts |
Interface perfshop-scripts-ui (ajouté en V31) |
Le compte superadmin (is_superadmin = true) :
- A automatiquement tous les droits, peu importe les valeurs des colonnes
can_access_*. - N'est pas supprimable depuis l'interface de gestion des comptes.
- Est bootstrappé au premier démarrage par
AdminUserService(surApplicationReadyEvent) à partir des variables d'environnementPERFSHOP_ADMIN_EMAILetPERFSHOP_ADMIN_PASSWORD.
Pas de hash en dur dans les migrations
La migration V30 ne contient aucun INSERT : le compte superadmin est créé en Java avec un hash BCrypt fraîchement calculé à partir de la variable d'environnement. Cette séparation évite d'avoir à committer un hash dans un fichier SQL versionné.
Mécanisme 3 — Token étudiant¶
L'étudiant qui rejoint un parcours pédagogique reçoit un UUID unique qui sert de token de session pour toute la durée du parcours. Ce token transite via un header HTTP custom X-Student-Token et identifie une ligne de la table pedagogique_sessions.
Diagramme de séquence¶
sequenceDiagram
autonumber
actor E as Étudiant
participant SP as Student Page<br/>(chaos student page)
participant BE as perfshop-app
participant CSC as ChaosStudentController
participant PSS as PedagogiqueSessionService
participant DB as MySQL<br/>(pedagogique_sessions)
Note over E,DB: Étape 1 — Join du parcours
E->>SP: Saisit alias, clique « Rejoindre »
SP->>BE: POST /api/chaos/student/pedagogique/join<br/>{alias}
BE->>CSC: pedagogiqueJoin(alias)
CSC->>PSS: createSession(alias, level, timer)
PSS->>PSS: token = UUID.randomUUID()
PSS->>PSS: agentCode = compute(level, token)
PSS->>PSS: extractionAnswerHash = SHA-256(...)
PSS->>PSS: logiqueQuestionIndices (LCG seedé par token)
PSS->>DB: INSERT pedagogique_sessions
PSS-->>CSC: MutableSession
CSC-->>SP: 200 {token, alias, level, ...}
SP->>SP: localStorage.setItem("studentToken", token)
Note over E,DB: Étape 2 — Polling /status
loop Toutes les 5 secondes
SP->>BE: GET /api/chaos/student/status<br/>X-Student-Token: <UUID>
BE->>CSC: status(studentToken)
CSC->>PSS: getSession(studentToken)
PSS->>DB: SELECT (ou cache mémoire)
DB-->>PSS: PedagogiqueSessionEntity
PSS-->>CSC: MutableSession
CSC-->>SP: 200 {chaos: {...}, pedagogique: {step, attempts, ...}}
end
Note over E,DB: Étape 3 — Validation d'une étape
E->>SP: Saisit la réponse à l'énigme
SP->>BE: POST /api/chaos/student/pedagogique/validate<br/>X-Student-Token: <UUID><br/>{answer}
BE->>CSC: pedagogiqueValidate(answer, studentToken)
CSC->>PSS: getSession(studentToken)
PSS-->>CSC: session
CSC->>CSC: SHA-256(answer) == session.extractionAnswerHash ?
alt Réponse correcte
CSC->>PSS: session.step++ ; saveSession()
PSS->>DB: UPDATE current_step, attempts_json
CSC-->>SP: 200 {success: true, step: N+1}
else Réponse incorrecte
CSC->>PSS: session.attempts[step]++ ; saveSession()
CSC-->>SP: 200 {success: false}
end
Endpoints clés du token étudiant¶
| Endpoint | Méthode | Header requis | Rôle |
|---|---|---|---|
/api/chaos/student/pedagogique/join |
POST | — | Crée une nouvelle session, retourne le token UUID |
/api/chaos/student/status |
GET | X-Student-Token (optionnel) |
Polling de l'état chaos + état pédagogique du token |
/api/chaos/student/pedagogique/validate |
POST | X-Student-Token |
Valide la réponse à l'énigme courante |
/api/chaos/student/pedagogique/logique/questions |
GET | X-Student-Token |
Retourne les 5 indices de questions tirées au /join |
/api/chaos/student/pedagogique/logique/check |
POST | X-Student-Token |
Valide une réponse à une question logique |
/api/chaos/student/pedagogique/finale/validate |
POST | X-Student-Token |
Valide la réponse finale (concaténation des 5 réponses logique) |
/api/chaos/student/pedagogique/succes/{token} |
GET | (token dans l'URL) | Retourne la page de succès personnalisée |
Constante Java :
Tous les endpoints /pedagogique/** qui mutent l'état d'une session vérifient :
if (studentToken == null || studentToken.isBlank())
return ResponseEntity.status(401).body(...);
PedagogiqueSessionService.MutableSession session =
pedagogiqueSessionService.getSession(studentToken);
if (session == null) return ResponseEntity.status(404).body(...);
Persistance et limite¶
- Limite de sessions actives : 500 sessions maximum (au-delà, le
/joinretourne une erreur). Cette limite est largement dimensionnée par rapport à l'usage normal (60 étudiants par session de TP). - Stockage : table
pedagogique_sessions(migrationV36). Voirmulti-session.mdpour le détail de l'architecture write-through et du cache mémoire optionnel. - Pas de mot de passe, pas de PII obligatoire : seul l'alias est demandé (et il peut être vide — un pseudo
Agent-XXXXest généré à partir du token). - Côté navigateur : le frontend stocke le token dans
localStorage(studentToken) et l'envoie dans le headerX-Student-Tokenpour toutes les requêtes suivantes.
Synthèse — quelle authentification pour quel endpoint ?¶
flowchart LR
REQ["Requête HTTP entrante"]
REQ --> LI["LicenseInterceptor<br/>(ordre 1)"]
LI -->|Licence absente| H402["HTTP 402"]
LI -->|OK| CI["ChaosInterceptor<br/>(ordre 2)"]
CI --> CTL{Type<br/>d'endpoint ?}
CTL -->|/api/auth/**, /api/products, /api/cart, /api/orders, /api/user/**| SHU["Session HTTP utilisateur"]
CTL -->|/api/admin/**, /api/chaos/** sauf /student/**| SHA["Session admin OU<br/>X-Admin-Token"]
CTL -->|/api/chaos/student/pedagogique/**| STU["X-Student-Token"]
CTL -->|/actuator/**| BYP["Pas d'auth<br/>(exclu des intercepteurs)"]
SHU --> CTRL[Contrôleur métier]
SHA --> CTRL
STU --> CTRL
BYP --> CTRL
| Famille d'endpoint | Authentification |
|---|---|
/api/auth/login, /api/auth/logout |
Aucune (l'auth est précisément ce qui se passe ici) |
/api/products, /api/products/{id} |
Aucune (catalogue public) |
/api/cart/**, /api/orders/**, /api/user/**, /api/checkout |
Session HTTP utilisateur (cookie JSESSIONID) |
/api/admin/**, /api/chaos/** (sauf /student/**) |
Session admin OU header X-Admin-Token |
/api/chaos/student/pedagogique/** |
Header X-Student-Token |
/api/chaos/student/status, /api/chaos/student/performance/scenarios |
Aucune (lecture publique) ; le token est facultatif et enrichit la réponse s'il est fourni |
/actuator/** |
Aucune (exclus des intercepteurs Licence et Chaos) |
Pour aller plus loin¶
- Multi-session pédagogique — write-through DB et cache mémoire optionnel
- Schéma de données — entités
User,AdminUser,PedagogiqueSessionEntity - Section Référence API (LOT 4) — chaque endpoint avec son contrat exact
- Section Sécurité et licence (LOT 6) — licence et chaos sécurité (token HMAC faible, timing attack)