Admin authentication¶
Admin authentication controls access to the instructor interfaces of PerfShop: chaos-admin panel, monitoring (admin login), scripts-ui, jmeter-ui, and account management. It is distinct from e-commerce shop user authentication (handled by AuthController) and from the vulnerable security portal (AdminPortalController, reserved for Security Chaos 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)
Account model¶
An admin account is represented by the AdminUser entity persisted in the admin_users table created by V1__schema.sql. Its main fields:
| Column | Type | Role |
|---|---|---|
id |
BIGINT AUTO_INCREMENT | Unique identifier |
email |
VARCHAR(100) UNIQUE | Login identifier |
password_hash |
VARCHAR(255) | BCrypt hash strength 10 ($2b$10$...) |
is_superadmin |
BOOLEAN | true for the single superadmin account |
can_access_chaos |
BOOLEAN | Access to chaos-admin and /api/chaos/* endpoints |
can_access_monitoring |
BOOLEAN | Access to the admin monitoring dashboard |
can_access_admin |
BOOLEAN | Access to the /api/admin/* backoffice |
can_access_jmeter |
BOOLEAN | Access to jmeter-ui |
can_access_scripts |
BOOLEAN | Access to scripts-ui |
created_at |
DATETIME | Creation date |
Password hashing¶
Passwords are stored hashed with BCrypt strength 10. AdminUserService exposes a static BCryptPasswordEncoder:
Strength 10 means 2¹⁰ = 1024 internal iterations — a reasonable compromise between security and login latency. The resulting hash starts with $2b$10$ and is 60 characters long. The utility method hashPassword(plain) is exposed as static to be called from other controllers.
Verification at login goes through passwordEncoder.matches(password, admin.getPasswordHash()) — BCrypt integrates its own salt into the hash, no external salt is necessary.
Superadmin bootstrap¶
At Spring Boot startup, AdminUserService listens for the ApplicationReadyEvent event and runs bootstrapSuperAdmin():
flowchart TD
Start([ApplicationReadyEvent]) --> Check{Superadmin<br/>account<br/>exists?}
Check -- no --> Create[INSERT with BCrypt of<br/>PERFSHOP_ADMIN_PASSWORD]
Check -- yes --> Verify{Hash matches<br/>.env?}
Verify -- yes --> Done([✅ Superadmin ready])
Verify -- no --> UpdatePwd[UPDATE password_hash]
UpdatePwd --> Done
Create --> Done
Two special cases are handled:
-
Password change via
.env— ifPERFSHOP_ADMIN_PASSWORDhas been modified since the last run, the stored hash no longer matches the requested password.AdminUserServicedetects this case viapasswordEncoder.matches(...)and updates the hash. This is a recovery path if the administrator loses their password: they can simply reset it in.envand restart the container. -
Catch-up of JMeter/Scripts permissions — for instances migrated from a historical schema that did not yet contain these two flags, the bootstrap adds them retroactively on the superadmin account.
The default values used at creation are:
| Variable | Default |
|---|---|
perfshop.admin.email |
admin@perfshop.fr |
perfshop.admin.password |
perfshop |
These values are suitable for an isolated pedagogical environment. They must be changed for any public exposure.
Login flow¶
The instructor login flow follows this sequence:
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
Important point: the backend refuses login with HTTP 402 if no license is active, thanks to LicenseInterceptor which blocks /api/admin/*. And admin login itself is at /api/admin/login, so the interceptor does block it. Consequence: an instance without a license cannot be administered via the instructor panel. See License system.
The two authentication modes¶
The utility AdminAuth.isAdmin(session, adminToken) accepts two ways to recognize an 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 — HTTP session¶
After a successful login, AdminController sets the admin_logged_in = true attribute on the HTTP session. As long as the session persists (cookie JSESSIONID + SameSite=lax), subsequent calls are automatically authenticated. This is the standard mode for HTML pages served from the same domain (Docker Desktop, simple local network).
Mode 2 — X-Admin-Token header¶
In addition, AdminController generates an opaque token on each login and returns it in the response. Clients can then send this token in the X-Admin-Token header on each request:
PUT /api/admin/accounts/42/rights HTTP/1.1
X-Admin-Token: abc123def456...
Content-Type: application/json
{ "canAccessChaos": true, ... }
This path is necessary in several cases:
- Cross-origin deployment — the chaos-admin panel and the backend are on different domains, so the session cookie does not follow CORS requests
- Mixed HTTP/HTTPS deployment —
Securecookies on HTTPS, but the HTTP client does not have access to them - Programmatic clients — curl, Postman, JMeter scripts that do not have a persistent cookie jar
The chaos-admin panel uses an adminFetch() wrapper that automatically injects X-Admin-Token from sessionStorage.chaos_token into all requests:
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 });
}
The two paths coexist without conflict: if either works, access is granted.
Superadmin protection¶
The superadmin (is_superadmin = true) benefits from three protections hard-coded in AdminUserService:
- Non-deletable —
deleteAdmin(id)throwsIllegalStateExceptionif the account is superadmin - Fixed permissions —
updateRights(id, ...)throwsIllegalStateException— the superadmin permissions are alltruepermanently - Password changeable —
updatePassword(id, newPassword)works for all accounts, including the superadmin
This asymmetry guarantees that there is always an account capable of administering the platform. If a bug or a mishandling breaks all other accounts, the superadmin remains functional.
Account CRUD — AdminUserService¶
| Method | Role | Constraint |
|---|---|---|
findAll() |
List all accounts | — |
findById(id) |
Search by ID | — |
findByEmail(email) |
Search by email | — |
authenticate(email, password) |
Verify credentials, returns Optional<AdminUser> |
— |
createAdmin(...) |
Create an account | Email must be unique |
deleteAdmin(id) |
Delete | The superadmin is non-deletable |
updatePassword(id, newPassword) |
Change the password | Minimum 6 characters |
updateRights(id, ...) |
Modify access permissions | The superadmin has fixed permissions |
The corresponding HTTP endpoints are exposed by AdminController (see Administration API).
My account page¶
Every logged-in admin can change their own password from chaos-admin/public/admin/mon-compte.html. The mon-compte.js script calls PUT /api/admin/accounts/me with the new password after client-side confirmation verification. The backend verifies the minimum length (6 characters) and updates the BCrypt hash via AdminUserService.updatePassword(). See Chaos admin (instructor).
Environment variables¶
| Variable | Usage | Default |
|---|---|---|
PERFSHOP_ADMIN_EMAIL |
Superadmin email (bootstrap) | admin@perfshop.fr |
PERFSHOP_ADMIN_PASSWORD |
Superadmin password (bootstrap) | perfshop |
SESSION_COOKIE_SECURE |
Cookie Secure flag |
false |
SESSION_COOKIE_SAME_SITE |
Cookie SameSite |
lax |
The two default values must be changed in production via .env or via the My account page after the first login.