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/logout → AuthService.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:
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(onApplicationReadyEvent) from thePERFSHOP_ADMIN_EMAILandPERFSHOP_ADMIN_PASSWORDenvironment 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:
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,
/joinreturns an error). This limit is generously sized compared to normal usage (60 students per workshop session). - Storage:
pedagogique_sessionstable (V36migration). Seemulti-session.mdfor 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-XXXXpseudo is generated from the token). - Browser side: the frontend stores the token in
localStorage(studentToken) and sends it in theX-Student-Tokenheader 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 schema —
User,AdminUser,PedagogiqueSessionEntityentities - API reference section — every endpoint with its exact contract
- Security and license section — license and security chaos (weak HMAC token, timing attack)