Skip to content

Authentication flow

PerfShop handles three distinct authentication mechanisms, each with its own scope, transport medium and lifetime. This page describes each one in detail with a sequence diagram and references to the source code.

Mechanism Actor Transport Server-side storage
User session End customer / student playing a buyer role JSESSIONID HTTP cookie Tomcat HttpSession (LOGGED_IN_USER attribute)
Admin token Instructor, programmatic access to admin tools X-Admin-Token header or admin_logged_in session attribute Validation via AdminController.isValidAdminToken()
Student token Student following a pedagogical journey X-Student-Token header UUID stored in the pedagogique_sessions table

Source of truth

This entire page is taken from: 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, and application.yml (spring.servlet.session section).

Mechanism 1 — HTTP user session

This is the classic shop authentication. The user signs up or signs in via the React frontend, and the backend opens a Tomcat session whose identifier travels in a cookie.

Sequence diagram

sequenceDiagram
  autonumber
  actor U as Browser<br/>(end customer)
  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: Enters email + password
  FE->>BE: POST /api/auth/login<br/>{email, password}
  BE->>AS: login(email, password, session)
  AS->>DB: findByEmail(email)
  DB-->>AS: User (or empty)

  alt User missing
    AS-->>BE: Optional.empty()
    BE-->>FE: 401 {error: auth.error.credentials}
  else User present
    AS->>AS: BCrypt.matches(password, user.password)
    alt Invalid hash
      AS-->>BE: Optional.empty()
      BE-->>FE: 401 {error: auth.error.credentials}
    else Valid hash
      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

Technical details

Aspect Implementation
Endpoint POST /api/auth/login (AuthController.login())
DTO LoginRequest validated by @Valid (email, password fields)
Hashing BCrypt strength 10 (AuthService.passwordEncoder), a good security/performance trade-off (~300-500 ms per hash)
Hash migration V29__hash_existing_passwords.sql migration — historical cleartext passwords were migrated to BCrypt
User session storage session.setAttribute("LOGGED_IN_USER", user) (constant AuthService.SESSION_USER_KEY)
Reading the current user AuthService.getCurrentUser(session) returns the User or null
Login check AuthService.isLoggedIn(session)
Logout POST /api/auth/logoutAuthService.logout(session)session.invalidate()
Additional application token SecurityTokenService.generateToken(session, userId) returns a signed token used by the frontend for subsequent requests (cart, checkout)

HTTP session configuration

In application.yml, spring.servlet.session section:

Setting Value Env variable
timeout 30m (fixed)
cookie.http-only true (fixed)
cookie.secure depends on context SESSION_COOKIE_SECURE (false locally, true over HTTPS)
cookie.same-site depends on context SESSION_COOKIE_SAME_SITE (lax locally, none for HTTPS cross-site)

CORS and exposed headers

CorsConfig.java allows the origins listed in CORS_ALLOWED_ORIGINS (by default frontend, monitoring, chaos-admin, admin) with allowCredentials(true) — that's what allows the JSESSIONID cookie to cross cross-origin requests between the React frontend and the Spring Boot backend.

The custom headers used by Scripting Chaos are explicitly listed in exposedHeaders(...) so they can be read by the browser JavaScript:

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

Anomaly A11 — logout grace period

POST /api/auth/logout consults BusinessChaosService.getTokenGracePeriodMs(email). If anomaly A11 (business chaos level 3) is active, the session is not invalidated: only the LOGGED_IN_USER attribute is removed, but the securityToken and the scripting bundle remain in the session for the duration of the grace period (30 seconds by default). The whole checkout chain stays exploitable during this delay. This is intentional and documented in the AuthController.logout() code.

Mechanism 2 — Admin authentication

Admin authentication protects access to chaos-admin, the admin portal, and all /api/admin/** and /api/chaos/** endpoints. It has two simultaneous modes, handled by the AdminAuth utility class:

Mode A — Admin session (web interface)

When the instructor logs in via the chaos-admin or admin login page, the backend opens a standard session and stores the key admin_logged_in = true in it.

Mode B — X-Admin-Token header (programmatic)

When an external script (curl, Postman, JMeter, Robot Framework) calls an admin endpoint, it sends the HTTP header X-Admin-Token with a static token configured server-side. No session is opened.

Sequence diagram

sequenceDiagram
  autonumber
  participant C as Client<br/>(browser OR script)
  participant BE as perfshop-app
  participant AA as AdminAuth.isAdmin()
  participant AC as AdminController

  alt Mode A — Admin session
    C->>BE: POST /api/admin/login<br/>{email, password}
    BE->>BE: BCrypt check on AdminUser
    BE->>BE: session.setAttribute("admin_logged_in", true)
    BE-->>C: 200 + Cookie JSESSIONID

    Note over C,BE: Subsequent requests
    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 — X-Admin-Token header
    C->>BE: GET /api/chaos/backend/state<br/>X-Admin-Token: <token>
    BE->>AA: isAdmin(session, headerToken)
    AA->>AC: AdminController.isValidAdminToken(token)
    AC-->>AA: true / false
    AA-->>BE: true
    BE-->>C: 200 {state: ...}
  end

Reference code — 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);
}

All sensitive controllers (ChaosController, BusinessChaosController, FunctionalChaosController, SecurityChaosController, ChaosScriptingController, ChaosStudentController on the admin side, AdminController) call AdminAuth.isAdmin(session, headerToken) at the beginning of each method and return HTTP 403 if the check fails.

Admin accounts and granular rights

Admin accounts are stored in the admin_users table (AdminUser entity, V30__create_admin_users.sql migration) with five independent rights:

Right Constant Scope
can_access_chaos canAccessChaos chaos-admin interface
can_access_monitoring canAccessMonitoring perfshop-monitoring dashboard
can_access_admin canAccessAdmin admin backoffice (products, orders, accounts)
can_access_jmeter canAccessJmeter perfshop-jmeter-ui interface (added in V31)
can_access_scripts canAccessScripts perfshop-scripts-ui interface (added in V31)

The superadmin account (is_superadmin = true):

  • Automatically has every right, regardless of the values of the can_access_* columns.
  • Cannot be deleted from the account management interface.
  • Is bootstrapped at first startup by AdminUserService (on ApplicationReadyEvent) from the PERFSHOP_ADMIN_EMAIL and PERFSHOP_ADMIN_PASSWORD environment variables.

No hardcoded hash in the migrations

The V30 migration contains no INSERT: the superadmin account is created in Java with a freshly computed BCrypt hash from the environment variable. This separation avoids having to commit a hash into a versioned SQL file.

Mechanism 3 — Student token

The student joining a pedagogical journey receives a unique UUID that acts as their session token for the entire duration of the journey. This token travels via a custom HTTP header X-Student-Token and identifies a row in the pedagogique_sessions table.

Sequence diagram

sequenceDiagram
  autonumber
  actor E as Student
  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: Step 1 — Joining the journey
  E->>SP: Enters alias, clicks "Join"
  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 seeded by 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: Step 2 — Polling /status
  loop Every 5 seconds
    SP->>BE: GET /api/chaos/student/status<br/>X-Student-Token: <UUID>
    BE->>CSC: status(studentToken)
    CSC->>PSS: getSession(studentToken)
    PSS->>DB: SELECT (or memory cache)
    DB-->>PSS: PedagogiqueSessionEntity
    PSS-->>CSC: MutableSession
    CSC-->>SP: 200 {chaos: {...}, pedagogique: {step, attempts, ...}}
  end

  Note over E,DB: Step 3 — Validating a step
  E->>SP: Enters the answer to the enigma
  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 Correct answer
    CSC->>PSS: session.step++ ; saveSession()
    PSS->>DB: UPDATE current_step, attempts_json
    CSC-->>SP: 200 {success: true, step: N+1}
  else Incorrect answer
    CSC->>PSS: session.attempts[step]++ ; saveSession()
    CSC-->>SP: 200 {success: false}
  end

Key student token endpoints

Endpoint Method Required header Role
/api/chaos/student/pedagogique/join POST Creates a new session, returns the UUID token
/api/chaos/student/status GET X-Student-Token (optional) Polls the chaos state + pedagogical state of the token
/api/chaos/student/pedagogique/validate POST X-Student-Token Validates the answer to the current enigma
/api/chaos/student/pedagogique/logique/questions GET X-Student-Token Returns the 5 question indices drawn at /join
/api/chaos/student/pedagogique/logique/check POST X-Student-Token Validates an answer to a logic question
/api/chaos/student/pedagogique/finale/validate POST X-Student-Token Validates the final answer (concatenation of the 5 logic answers)
/api/chaos/student/pedagogique/succes/{token} GET (token in URL) Returns the personalized success page

Java constant:

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

All /pedagogique/** endpoints that mutate a session's state check:

if (studentToken == null || studentToken.isBlank())
    return ResponseEntity.status(401).body(...);
PedagogiqueSessionService.MutableSession session =
    pedagogiqueSessionService.getSession(studentToken);
if (session == null) return ResponseEntity.status(404).body(...);

Persistence and limit

  • Active session limit: 500 sessions maximum (above that, /join returns an error). This limit is generously sized compared to normal usage (60 students per workshop session).
  • Storage: pedagogique_sessions table (V36 migration). See multi-session.md for the details of the write-through architecture and the optional memory cache.
  • No password, no mandatory PII: only the alias is requested (and it can be empty — a Agent-XXXX pseudo is generated from the token).
  • Browser side: the frontend stores the token in localStorage (studentToken) and sends it in the X-Student-Token header for all subsequent requests.

Summary — which authentication for which endpoint?

flowchart LR
  REQ["Incoming HTTP request"]
  REQ --> LI["LicenseInterceptor<br/>(order 1)"]
  LI -->|License missing| H402["HTTP 402"]
  LI -->|OK| CI["ChaosInterceptor<br/>(order 2)"]
  CI --> CTL{Endpoint<br/>type ?}

  CTL -->|/api/auth/**, /api/products, /api/cart, /api/orders, /api/user/**| SHU["HTTP user session"]
  CTL -->|/api/admin/**, /api/chaos/** except /student/**| SHA["Admin session OR<br/>X-Admin-Token"]
  CTL -->|/api/chaos/student/pedagogique/**| STU["X-Student-Token"]
  CTL -->|/actuator/**| BYP["No auth<br/>(excluded from interceptors)"]

  SHU --> CTRL[Business controller]
  SHA --> CTRL
  STU --> CTRL
  BYP --> CTRL
Endpoint family Authentication
/api/auth/login, /api/auth/logout None (auth is precisely what happens here)
/api/products, /api/products/{id} None (public catalog)
/api/cart/**, /api/orders/**, /api/user/**, /api/checkout HTTP user session (JSESSIONID cookie)
/api/admin/**, /api/chaos/** (except /student/**) Admin session OR X-Admin-Token header
/api/chaos/student/pedagogique/** X-Student-Token header
/api/chaos/student/status, /api/chaos/student/performance/scenarios None (public read); the token is optional and enriches the response if provided
/actuator/** None (excluded from the License and Chaos interceptors)

To go further

  • Pedagogical multi-session — DB write-through and optional memory cache
  • Database schemaUser, AdminUser, PedagogiqueSessionEntity entities
  • API reference section — every endpoint with its exact contract
  • Security and license section — license and security chaos (weak HMAC token, timing attack)