Aller au contenu

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/logoutAuthService.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 :

X-Session-Token, X-Action-Token, X-CSRF-Token,
X-Step-Token, X-Signature, X-Request-ID, X-Key-Hint

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 (sur ApplicationReadyEvent) à partir des variables d'environnement PERFSHOP_ADMIN_EMAIL et PERFSHOP_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 :

static final String STUDENT_TOKEN_HEADER = "X-Student-Token";

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 /join retourne une erreur). Cette limite est largement dimensionnée par rapport à l'usage normal (60 étudiants par session de TP).
  • Stockage : table pedagogique_sessions (migration V36). Voir multi-session.md pour 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-XXXX est généré à partir du token).
  • Côté navigateur : le frontend stocke le token dans localStorage (studentToken) et l'envoie dans le header X-Student-Token pour 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)