API — Authentication¶
This page documents the endpoints handled by AuthController and UserController, both mounted under the /api/auth prefix.
Controllers covered
AuthController→POST /login,POST /logoutUserController→GET /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¶
No body.
Response — 200 OK¶
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¶
| 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:
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¶
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 emailpassword→ rewrites the password in plaintext in thepasswordcolumn (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" }
Related links¶
- Checkout journey — usage of the
securityToken - Security chaos — details of S3, S6, S7, S9 faults
- Business chaos — details of A9, A11
- Validation — per-country validation rules