Aller au contenu

API — Authentification

Cette page documente les endpoints gérés par AuthController et UserController, tous deux montés sous le préfixe /api/auth.

Contrôleurs couverts

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

Les endpoints /register et /me DELETE n'existent pas dans le code : la création de compte utilisateur final passe par POST /api/admin/users (côté formateur) ou par le parcours frontend qui appelle la couche de service. Voir admin.md.


Vue d'ensemble

Méthode Endpoint Auth Description
POST /api/auth/login Aucune Connexion utilisateur, génère un cookie de session et un securityToken
POST /api/auth/logout Session Déconnexion (comportement altéré en Chaos Métier niveau 3+)
GET /api/auth/status Aucune État de la session courante (authentifié, token, id)
GET /api/auth/me Session Profil complet de l'utilisateur connecté
PUT /api/auth/me Session Mise à jour du profil avec validation stricte

POST /api/auth/login

Authentifie un utilisateur final via email + mot de passe. Crée une session HTTP (cookie JSESSIONID) et génère un securityToken UUID utilisé ensuite par le parcours checkout.

Auth : aucune Contrôleur : AuthController.login() Service appelé : AuthService.login() (validation BCrypt contre la table users)

Requête

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

{
  "email": "alice@example.com",
  "password": "alice123"
}
Champ Type Requis Contrainte
email string oui Format email valide (@Email)
password string oui Non vide (@NotBlank)

Le DTO de liaison est com.perfshop.dto.LoginRequest.

Réponse — 200 OK

{
  "success": true,
  "securityToken": "a7f1c2e8-4b9d-4f11-9c8e-1a2b3c4d5e6f",
  "id": 42,
  "email": "alice@example.com",
  "firstName": "Alice",
  "lastName": "Durand"
}
Champ Description
securityToken UUID requis pour POST /api/orders (validation finale de commande)
id Identifiant interne de l'utilisateur
email, firstName, lastName Infos de base pour pré-remplir le profil côté frontend

Headers de réponse additionnels

En fonction des chaos actifs, le login peut enrichir les headers HTTP de la réponse :

Header Activation Chaos
X-Session-Token, X-Request-ID, etc. Scripting ≥ 1 Chaos Scripting
X-Debug-Token Sécurité ≥ 3 S7 — Token HMAC faible

S7 — Token HMAC faible

En Chaos Sécurité niveau 3 (Expert) et au-delà, le header X-Debug-Token est ajouté à la réponse. Il contient un token HMAC-SHA256 signé avec la clé secret123 — connue de l'étudiant. L'étudiant peut décoder le token (format base64(userId:timestamp).hmac-sha256), modifier le userId, re-signer avec la clé et usurper une identité dans les endpoints qui lisent ce header.

Timing attack pédagogique (S6)

En Chaos Sécurité niveau 2 (Confirmé) et au-delà, le temps de réponse du login trahit l'existence du compte :

  • Email inconnu → réponse immédiate (< 5 ms) — pas de BCrypt invoqué
  • Email connu → réponse après ~300 ms — BCrypt vérifie le mot de passe

Cette différence mesurable est loggée dans activityLog de SecurityChaosService pour que le monitoring affiche l'attaque. Voir S6.

Codes d'erreur

Code Corps Cause
401 {"error": "Identifiants incorrects"} Email inconnu ou mot de passe incorrect (clé auth.error.credentials)
500 {"error": "Erreur serveur"} Exception non gérée (clé auth.error.server)

POST /api/auth/logout

Détruit la session utilisateur et invalide le securityToken associé.

Auth : session HTTP (pas d'erreur si déjà déconnecté) Contrôleur : AuthController.logout()

Requête

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

Pas de corps.

Réponse — 200 OK

{
  "message": "Deconnexion reussie",
  "gracePeriodMs": 0
}

Le champ gracePeriodMs vaut 0 en comportement nominal. En Chaos Métier actif, il peut valoir 30000.

A11 — Token session non invalidé

En Chaos Métier niveau 3 et au-delà, le logout ne détruit pas la session : il se contente de retirer l'utilisateur de la clé LOGGED_IN_USER, mais le securityToken et le bundle de Chaos Scripting restent exploitables pendant 30 secondes. Un attaquant qui a intercepté le cookie peut rejouer une commande après le logout. Voir A11.

La valeur retournée dans gracePeriodMs indique la durée restante pendant laquelle l'ancienne session est encore acceptable. Cette valeur est intentionnellement exposée — elle permet aux scripts de test de vérifier que la faille est bien active.


GET /api/auth/status

Retourne l'état d'authentification de la session courante. Endpoint sans erreur — retourne authenticated: false si aucune session.

Auth : aucune (lecture seule) Contrôleur : UserController.getStatus()

Réponse — 200 OK

{
  "authenticated": true,
  "hasToken": true,
  "userId": 42,
  "graceActive": false
}
Champ Description
authenticated true si un utilisateur est connecté en session
hasToken true si le securityToken checkout est encore valide
userId ID de l'utilisateur courant ou null
graceActive true si le Chaos Métier niveau ≥ 3 active la grace period A11

Cet endpoint est utilisé par le frontend pour décider s'il faut afficher la barre de navigation connectée, et par les outils de test pour vérifier l'état après logout.


GET /api/auth/me

Retourne le profil complet de l'utilisateur connecté.

Auth : session HTTP (obligatoire) Contrôleur : UserController.getProfile() Repository : UserRepository.findById() — relecture fraîche depuis la DB pour éviter les désynchros cache session / base.

Réponse — 200 OK (comportement nominal)

{
  "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"
}

Le champ address est un alias legacy de street — conservé pour la compatibilité avec les anciens clients frontend.

S3 — Hash BCrypt exposé

En Chaos Sécurité niveau 1 (Junior) et au-delà, la réponse inclut un champ password contenant le hash BCrypt complet de l'utilisateur :

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

Un attaquant peut alors tenter une attaque offline par dictionnaire. Voir S3.

Codes d'erreur

Code Corps Cause
401 {"error": "Non authentifié"} Pas de session (clé auth.error.not_authenticated)

PUT /api/auth/me

Met à jour le profil de l'utilisateur connecté. Validation stricte via ValidationService (formats nationaux de code postal, téléphone, etc.).

Auth : session HTTP (obligatoire) Contrôleur : UserController.updateProfile()

Requête

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"
}
Champ Type Contraintes
civility string M, Mme ou Mx
firstName string 2–100 caractères, sans caractères de contrôle
lastName string 2–100 caractères
birthDate string (ISO) YYYY-MM-DD, âge ≥ 16 et ≤ 120
phone string Format national selon country (FR : 10 chiffres commençant par 0 non mobile, BE : 9 chiffres, etc.)
street string 5–200 caractères
postalCode string Format national (FR : 5 chiffres, BE : 4 chiffres, UK : SW1A 2AA, etc.)
city string 2–100 caractères, sans accents refusés
region string 2–100 caractères (optionnel)
country string Code ISO 3166-1 alpha-2 (FR, BE, DE…)

Tous les champs sont optionnels : seuls ceux transmis sont mis à jour. Les champs absents du body ne sont pas touchés.

Réponse — 200 OK

{
  "success": true,
  "message": "Profil mis à jour",
  "profile": { /* même structure que GET /me */ }
}

Codes d'erreur

Code Corps Cause
401 {"error": "Non authentifié"} Pas de session
422 {"error": "Données invalides", "fields": {...}} Validation métier échouée. fields contient un mapping champ → message d'erreur localisé
500 {"error": "Erreur base de données...", "sql": "...", "chaos": true, "level": 3} DataIntegrityViolationException — souvent déclenchée par les injections SQL du Chaos Métier (champs postalCode ou country avec payload)

Exemple de réponse 422

{
  "error": "Données invalides",
  "fields": {
    "phone": "Format téléphone invalide pour FR — attendu : 10 chiffres commençant par 0 (ex: 0612345678)",
    "postalCode": "Code postal FR invalide '7500' — attendu : 5 chiffres (ex: 75001)",
    "birthDate": "Âge minimum requis : 16 ans (âge calculé : 14 ans)"
  }
}

Chaque message provient d'une clé i18n dédiée dans messages_fr.properties (préfixe validation.*).

S9 — Mass Assignment

En Chaos Sécurité niveau 3 (Expert) et au-delà, les champs sensibles du body sont appliqués sans whitelist :

  • email → réécrit l'email de l'utilisateur
  • password → réécrit le mot de passe en clair dans la colonne password (écrasant le hash BCrypt)

Ce side-effect est intentionnel : l'attaquant peut non seulement corrompre le compte, mais aussi verrouiller la victime (qui ne peut plus se connecter car BCrypt.matches() ne matche jamais un clair). La restauration nécessite une intervention admin via PUT /api/admin/accounts/{id}/password. Voir S9.

A9 — Log poisoning

En Chaos Métier niveau 3 et au-delà, les champs texte libres (firstName, lastName, street, city, phone, promoCode) sont insérés sans échappement dans les logs serveur. Un payload contenant \n[ERROR] Fake log peut créer des fausses entrées dans Loki. Voir A9.

Injection SQL via postalCode / country

En Chaos Métier niveau 2 et au-delà, les champs postalCode et country sont traités avec une concaténation SQL qui peut déclencher une DataIntegrityViolationException — visible dans la réponse 500 avec le détail sql exposé et le flag chaos: true.


Flux complet — Authentification et checkout

sequenceDiagram
    autonumber
    participant F as Frontend React
    participant B as Backend Spring
    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: Crée HttpSession + SET LOGGED_IN_USER
    B->>B: Génère securityToken (UUID)
    B-->>F: 200 { securityToken, id, email, firstName, lastName }<br/>Set-Cookie: JSESSIONID=...

    Note over F,B: L'utilisateur remplit son panier

    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: "Deconnexion reussie" }

Liens associés