Skip to content

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:

private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);

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:

  1. Password change via .env — if PERFSHOP_ADMIN_PASSWORD has been modified since the last run, the stored hash no longer matches the requested password. AdminUserService detects this case via passwordEncoder.matches(...) and updates the hash. This is a recovery path if the administrator loses their password: they can simply reset it in .env and restart the container.

  2. 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 deploymentSecure cookies 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-deletabledeleteAdmin(id) throws IllegalStateException if the account is superadmin
  • Fixed permissionsupdateRights(id, ...) throws IllegalStateException — the superadmin permissions are all true permanently
  • Password changeableupdatePassword(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.