Skip to content

Étape 2 — Correction de l'API d'authentification des utilisateurs

Objectif de l'étape : rendre POST /api/login fonctionnel et lui faire retourner un token JWT valide lorsque l'authentification réussit (cf. Étape 1 — analyse du code existant pour le diagnostic initial des trois bugs).

Corrections apportées

1. UserController.java — liaison du corps de requête manquante

- public ResponseEntity<?> login(LoginRequestDTO loginRequestDTO) {
+ public ResponseEntity<?> login(@Valid @RequestBody LoginRequestDTO loginRequestDTO) {

Sans @RequestBody, Spring tentait de lier LoginRequestDTO comme un model attribute (paramètres de requête) au lieu de désérialiser le JSON du corps de la requête : login/password restaient toujours null. @Valid active en plus la validation Bean Validation du DTO (voir point 2).

2. LoginRequestDTO.java — validation des champs

  public class LoginRequestDTO {
+     @NotBlank
      private String login;
+     @NotBlank
      private String password;

Aligné sur RegisterDTO, qui appliquait déjà cette validation. Une requête sans login/password renvoie désormais 400 Bad Request avant même d'atteindre la couche service.

3. UserService.login — comparaison du mot de passe

- if (user.isPresent() && passwordEncoder.matches(password, password)) {
-     UserDetails userDetails = org.springframework.security.core.userdetails.User.builder()
-             .username(login).build();
-     return jwtService.generateToken(userDetails);
+ if (user.isPresent() && passwordEncoder.matches(password, user.get().getPassword())) {
+     return jwtService.generateToken(user.get());

Deux bugs corrigés d'un coup :

  • Le mot de passe fourni était comparé à lui-même (matches(password, password) est toujours vrai) au lieu d'être comparé au hash stocké en base (user.get().getPassword()).
  • La reconstruction manuelle d'un UserDetails via org.springframework.security.core.userdetails.User.builder() omettait .password(...), ce qui provoquait une IllegalArgumentException (« Cannot pass null or empty values to constructor ») au moment du build() — donc un login réussi aurait tout de même échoué. L'entité User implémentant déjà UserDetails (comme le fait CustomUserDetailService pour Spring Security), on réutilise directement user.get() : plus simple et plus cohérent avec le reste du code.

4. JwtService.generateToken — génération du token

Implémentation avec la bibliothèque JJWT (io.jsonwebtoken:jjwt-api/impl/jackson, ajoutée au pom.xml) :

public String generateToken(UserDetails userDetails) {
    Date issuedAt = new Date();
    Date expiration = new Date(issuedAt.getTime() + expirationMs);

    return Jwts.builder()
            .subject(userDetails.getUsername())
            .issuedAt(issuedAt)
            .expiration(expiration)
            .signWith(getSigningKey())
            .compact();
}
  • Clé de signature HMAC lue depuis la configuration (jwt.secret), elle même injectée depuis .env (JWT_SECRET, secret aléatoire 256 bits généré avec openssl rand -hex 32) — même mécanisme que les credentials DB, via AppConfig.propertySourcesPlaceholderConfigurer().
  • Durée de validité configurable (jwt.expiration-ms / JWT_EXPIRATION_MS, 1 h par défaut).
  • signWith(key) sans algorithme explicite : JJWT choisit automatiquement l'algorithme HMAC le plus fort compatible avec la taille de la clé (ici HS512, la clé de 256 bits fournie le permet).

Fichiers modifiés

Fichier Changement
pom.xml Ajout des dépendances jjwt-api, jjwt-impl, jjwt-jackson (0.12.6)
.env Ajout de JWT_SECRET et JWT_EXPIRATION_MS
application.yml Ajout du bloc jwt.secret / jwt.expiration-ms
JwtService.java Implémentation de generateToken
UserService.java Correction du bug de comparaison de mot de passe et simplification
UserController.java Ajout de @Valid @RequestBody sur login
LoginRequestDTO.java Ajout de @NotBlank sur login/password

Vérifications effectuées

Back-end redémarré (mvn spring-boot:run) et testé (équivalent Postman, via curl, requêtes JSON identiques à ce qu'enverrait Postman) :

Scénario Requête Résultat
Identifiants valides POST /api/login avec un login/mot de passe existants 200 OK + JWT (header.payload.signature)
Mot de passe invalide idem avec un mauvais mot de passe 400 Bad Request"Invalid credentials"
Utilisateur inconnu login inexistant 400 Bad Request"Invalid credentials"
Corps de requête vide {} 400 Bad Request — erreur de validation Bean Validation
Via le proxy Angular même requête sur localhost:4200/api/login 200 OK + JWT (confirme le câblage front → back)

Le JWT retourné a été décodé pour contrôle :

// header
{"alg":"HS512"}
// payload
{"sub":"test.etape1","iat":1787189495,"exp":1787193095}

exp - iat = 3600 s, conforme à JWT_EXPIRATION_MS=3600000. Le sub correspond bien au login de l'utilisateur authentifié.

Suite de tests existante

UserServiceTest (tests unitaires Mockito, sans dépendance Docker) : 3/3 passent, sans régression sur register.

UserControllerTest (test d'intégration Testcontainers) échoue avec IllegalStateException: Could not find a valid Docker environmentce n'est pas une régression liée à cette étape (aucun test de login n'existe encore dans cette classe, seul register y est testé). C'est un problème d'environnement Docker Desktop détaillé ci-dessous, à corriger avant de commencer l'Exercice 2 (tests), qui reposera fortement sur Testcontainers.

✅ Testcontainers / Docker Desktop (WSL2) — résolu

Ce point bloquait le démarrage de l'Exercice 2 (tests) ; il est maintenant résolu. Deux problèmes distincts et indépendants étaient empilés :

1. Socket Docker Desktop en mode proxy (résolu côté Windows)

Le socket /var/run/docker.sock exposé par Docker Desktop dans WSL ne parlait, par défaut, qu'à un socket-proxy limité (utilisable par le CLI docker, mais pas par des bibliothèques qui parlent l'API HTTP brute du démon comme docker-java/Testcontainers). Corrigé en activant Docker Desktop → Settings → Advanced → « Allow the default Docker socket to be used », puis en redémarrant Docker Desktop (wsl --shutdown côté Windows + relance de la distribution WSL).

2. Négociation de version d'API Docker trop ancienne (résolu côté projet)

Une fois le socket réellement branché sur le moteur Docker, un second problème est apparu : Docker Desktop continue de faire transiter les anciennes versions d'API (/v1.24/..., /v1.32/...) par le socket-proxy (réponse HTTP 400 minimaliste), alors que les versions récentes (/v1.40/... et au-delà) atteignent le vrai moteur. Or Testcontainers 1.20.0 (via son docker-java embarqué) négocie par défaut la version 1.32 dès que rien ne la force explicitement — d'où l'échec persistant "Could not find a valid Docker environment" même socket corrigé.

Correctif : forcer une version d'API récente via la propriété système api.version, configurée une fois pour toutes dans pom.xml (maven-surefire-pluginsystemPropertyVariables), pour que mvn clean test fonctionne directement sans option supplémentaire à retenir par chaque développeur :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <api.version>1.41</api.version>
        </systemPropertyVariables>
    </configuration>
</plugin>

3. Bug latent dans UserControllerTest : mysql:latest

Une fois Testcontainers capable de parler au moteur Docker, le conteneur MySQL de test démarrait puis crashait immédiatement :

[ERROR] [MY-000067] [Server] unknown variable 'innodb_log_file_size=5M'.
[ERROR] [MY-013236] [Server] The designated data directory /var/lib/mysql/ is unusable.

MySQLContainer (Testcontainers 1.20.0) ajoute par défaut l'option de démarrage --innodb_log_file_size=5M (optimisation historique). Cette variable a été renommée dans les versions récentes de MySQL — or le tag mysql:latest utilisé dans UserControllerTest.java pointait justement vers une version aussi récente. Corrigé en épinglant une version stable et connue-compatible :

- static MySQLContainer mySQLContainer = new MySQLContainer("mysql:latest");
+ static MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0");

(compose.yaml, utilisé pour la base de dev via spring-boot-docker-compose, n'est pas concerné : il ne passe pas ce flag et n'a jamais été affecté.)

Vérification

mvn clean test
...
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

UserServiceTest (3 tests, Mockito) et UserControllerTest (3 tests, Testcontainers + conteneur MySQL réel) passent tous les deux, sans option Maven supplémentaire à fournir — la voie est libre pour l'Exercice 2.

Points de vigilance restants (rappel de l'étape 1)

  • RegisterComponent (front-end) ne gère toujours pas les erreurs HTTP — prévu pour l'écran de login à l'étape 3.
  • Aucune route /login ni service login() côté Angular pour l'instant : c'est l'objet de l'étape 3.