É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
UserDetailsviaorg.springframework.security.core.userdetails.User.builder()omettait.password(...), ce qui provoquait uneIllegalArgumentException(« Cannot pass null or empty values to constructor ») au moment dubuild()— donc un login réussi aurait tout de même échoué. L'entitéUserimplémentant déjàUserDetails(comme le faitCustomUserDetailServicepour Spring Security), on réutilise directementuser.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é avecopenssl rand -hex 32) — même mécanisme que les credentials DB, viaAppConfig.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 :
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 environment — ce
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-plugin → systemPropertyVariables), 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¶
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
/loginni servicelogin()côté Angular pour l'instant : c'est l'objet de l'étape 3.