Skip to content

API — Authentication

This page documents the endpoints handled by AuthController and UserController, both mounted under the /api/auth prefix.

Controllers covered

  • AuthControllerPOST /login, POST /logout
  • UserControllerGET /status, GET /me, PUT /me

The /register and /me DELETE endpoints do not exist in the code: end-user account creation goes through POST /api/admin/users (instructor side) or through the frontend journey that calls the service layer. See admin.md.


Overview

Method Endpoint Auth Description
POST /api/auth/login None User login, generates a session cookie and a securityToken
POST /api/auth/logout Session Logout (behavior altered in Business chaos level 3+)
GET /api/auth/status None Current session state (authenticated, token, id)
GET /api/auth/me Session Full profile of the logged-in user
PUT /api/auth/me Session Profile update with strict validation

POST /api/auth/login

Authenticates an end user via email + password. Creates an HTTP session (JSESSIONID cookie) and generates a UUID securityToken used later by the checkout journey.

Auth: none Controller: AuthController.login() Service called: AuthService.login() (BCrypt validation against the users table)

Request

POST /api/auth/login HTTP/1.1
Content-Type: application/json

{
  "email": "alice@example.com",
  "password": "alice123"
}
Field Type Required Constraint
email string yes Valid email format (@Email)
password string yes Not blank (@NotBlank)

The binding DTO is com.perfshop.dto.LoginRequest.

Response — 200 OK

{
  "success": true,
  "securityToken": "a7f1c2e8-4b9d-4f11-9c8e-1a2b3c4d5e6f",
  "id": 42,
  "email": "alice@example.com",
  "firstName": "Alice",
  "lastName": "Durand"
}
Field Description
securityToken UUID required for POST /api/orders (final order validation)
id Internal user identifier
email, firstName, lastName Basic info to pre-fill the profile on the frontend

Additional response headers

Depending on the active chaos, login may enrich the HTTP headers of the response:

Header Activation Chaos
X-Session-Token, X-Request-ID, etc. Scripting ≥ 1 Scripting chaos
X-Debug-Token Security ≥ 3 S7 — Weak HMAC token

S7 — Weak HMAC token

In Security chaos level 3 (Expert) and above, the X-Debug-Token header is added to the response. It contains an HMAC-SHA256 token signed with the key secret123 — known to the student. The student can decode the token (format base64(userId:timestamp).hmac-sha256), modify the userId, re-sign with the key and impersonate an identity on endpoints that read this header.

Pedagogical timing attack (S6)

In Security chaos level 2 (Intermediate) and above, the login response time betrays the account's existence:

  • Unknown email → immediate response (< 5 ms) — no BCrypt invoked
  • Known email → response after ~300 ms — BCrypt verifies the password

This measurable difference is logged in SecurityChaosService's activityLog so that monitoring displays the attack. See S6.

Error codes

Code Body Cause
401 {"error": "Invalid credentials"} Unknown email or incorrect password (key auth.error.credentials)
500 {"error": "Server error"} Unhandled exception (key auth.error.server)

POST /api/auth/logout

Destroys the user session and invalidates the associated securityToken.

Auth: HTTP session (no error if already logged out) Controller: AuthController.logout()

Request

POST /api/auth/logout HTTP/1.1
Cookie: JSESSIONID=...

No body.

Response — 200 OK

{
  "message": "Logged out successfully",
  "gracePeriodMs": 0
}

The gracePeriodMs field is 0 under nominal behavior. With Business chaos active, it may be 30000.

A11 — Session token not invalidated

In Business chaos level 3 and above, logout does not destroy the session: it simply removes the user from the LOGGED_IN_USER key, but the securityToken and the Scripting chaos bundle remain usable for 30 seconds. An attacker who has intercepted the cookie can replay an order after logout. See A11.

The value returned in gracePeriodMs indicates the remaining duration during which the old session is still acceptable. This value is intentionally exposed — it allows test scripts to verify that the fault is indeed active.


GET /api/auth/status

Returns the authentication state of the current session. Error-free endpoint — returns authenticated: false if no session.

Auth: none (read-only) Controller: UserController.getStatus()

Response — 200 OK

{
  "authenticated": true,
  "hasToken": true,
  "userId": 42,
  "graceActive": false
}
Field Description
authenticated true if a user is logged in the session
hasToken true if the checkout securityToken is still valid
userId ID of the current user or null
graceActive true if Business chaos level ≥ 3 activates the A11 grace period

This endpoint is used by the frontend to decide whether to display the logged-in navigation bar, and by test tools to check the state after logout.


GET /api/auth/me

Returns the full profile of the logged-in user.

Auth: HTTP session (mandatory) Controller: UserController.getProfile() Repository: UserRepository.findById() — fresh re-read from the DB to avoid session cache / database desync.

Response — 200 OK (nominal behavior)

{
  "id": 42,
  "email": "alice@example.com",
  "civility": "Mme",
  "firstName": "Alice",
  "lastName": "Durand",
  "birthDate": "1990-05-15",
  "phone": "0612345678",
  "street": "12 rue de la Paix",
  "postalCode": "75001",
  "city": "Paris",
  "region": "Île-de-France",
  "country": "FR",
  "address": "12 rue de la Paix"
}

The address field is a legacy alias of street — kept for compatibility with older frontend clients.

S3 — Exposed BCrypt hash

In Security chaos level 1 (Junior) and above, the response includes a password field containing the full BCrypt hash of the user:

{
  "...": "...",
  "password": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
}

An attacker can then attempt an offline dictionary attack. See S3.

Error codes

Code Body Cause
401 {"error": "Not authenticated"} No session (key auth.error.not_authenticated)

PUT /api/auth/me

Updates the logged-in user's profile. Strict validation via ValidationService (national formats for postal code, phone, etc.).

Auth: HTTP session (mandatory) Controller: UserController.updateProfile()

Request

PUT /api/auth/me HTTP/1.1
Content-Type: application/json
Cookie: JSESSIONID=...

{
  "civility": "Mme",
  "firstName": "Alice",
  "lastName": "Durand",
  "birthDate": "1990-05-15",
  "phone": "0612345678",
  "street": "12 rue de la Paix",
  "postalCode": "75001",
  "city": "Paris",
  "region": "Île-de-France",
  "country": "FR"
}
Field Type Constraints
civility string M, Mme or Mx
firstName string 2–100 characters, no control characters
lastName string 2–100 characters
birthDate string (ISO) YYYY-MM-DD, age ≥ 16 and ≤ 120
phone string National format per country (FR: 10 digits starting with 0 non-mobile, BE: 9 digits, etc.)
street string 5–200 characters
postalCode string National format (FR: 5 digits, BE: 4 digits, UK: SW1A 2AA, etc.)
city string 2–100 characters, no rejected accented characters
region string 2–100 characters (optional)
country string ISO 3166-1 alpha-2 code (FR, BE, DE…)

All fields are optional: only those transmitted are updated. Fields absent from the body are not touched.

Response — 200 OK

{
  "success": true,
  "message": "Profile updated",
  "profile": { /* same structure as GET /me */ }
}

Error codes

Code Body Cause
401 {"error": "Not authenticated"} No session
422 {"error": "Invalid data", "fields": {...}} Business validation failed. fields contains a mapping field → localized error message
500 {"error": "Database error...", "sql": "...", "chaos": true, "level": 3} DataIntegrityViolationException — often triggered by the SQL injections of Business chaos (postalCode or country fields with payload)

Example 422 response

{
  "error": "Invalid data",
  "fields": {
    "phone": "Invalid phone format for FR — expected: 10 digits starting with 0 (e.g. 0612345678)",
    "postalCode": "Invalid FR postal code '7500' — expected: 5 digits (e.g. 75001)",
    "birthDate": "Minimum age required: 16 years (calculated age: 14 years)"
  }
}

Each message comes from a dedicated i18n key in messages_en.properties (validation.* prefix).

S9 — Mass Assignment

In Security chaos level 3 (Expert) and above, sensitive body fields are applied without whitelist:

  • email → rewrites the user's email
  • password → rewrites the password in plaintext in the password column (overwriting the BCrypt hash)

This side-effect is intentional: the attacker can not only corrupt the account, but also lock out the victim (who can no longer log in because BCrypt.matches() never matches plaintext). Restoration requires an admin intervention via PUT /api/admin/accounts/{id}/password. See S9.

A9 — Log poisoning

In Business chaos level 3 and above, free-text fields (firstName, lastName, street, city, phone, promoCode) are inserted without escaping into the server logs. A payload containing \n[ERROR] Fake log can create fake entries in Loki. See A9.

SQL injection via postalCode / country

In Business chaos level 2 and above, the postalCode and country fields are handled with SQL concatenation that can trigger a DataIntegrityViolationException — visible in the 500 response with the exposed sql detail and the chaos: true flag.


Full flow — Authentication and checkout

sequenceDiagram
    autonumber
    participant F as React Frontend
    participant B as Spring Backend
    participant DB as MySQL

    F->>B: POST /api/auth/login { email, password }
    B->>DB: SELECT * FROM users WHERE email=?
    DB-->>B: User row
    B->>B: BCrypt.matches()
    B->>B: Creates HttpSession + SET LOGGED_IN_USER
    B->>B: Generates securityToken (UUID)
    B-->>F: 200 { securityToken, id, email, firstName, lastName }<br/>Set-Cookie: JSESSIONID=...

    Note over F,B: The user fills their cart

    F->>B: POST /api/checkout/address { ... }
    B->>B: isLoggedIn(session)? + validateProfile()
    B-->>F: 200 { step: "address", nextStep: "shipping" }

    F->>B: POST /api/orders { securityToken, items }
    B->>B: validateToken(session, providedToken)
    B->>DB: INSERT INTO orders ...
    B-->>F: 201 { success, orderNumber, orderId }

    F->>B: POST /api/auth/logout
    B->>B: session.invalidate()
    B-->>F: 200 { message: "Logged out successfully" }