Authentifier les utilisateurs avec des jetons JWT
Avoir une application qui est basée sur une API ne veut pas dire que tout le monde peut accéder à toutes les données de l'API. Il sera important d'implémenter un mécanisme d'authentification pour l'application afin de permettre l'accès ou non aux données selon le statut de connexion et les permissions données à l'utilisateur connecté.
Imaginons qu'on veuille éventuellement consommer l'API que l'on a créé via une application mobile native. Dans un tel cas, il ne serait pas possible d'utiliser la session java dans le navigateur pour gérer l'authentification.
Pour cette raison, on implémentera une authentification avec des jetons JWT. Ceux-ci permettront de stocker des informations sur l'utilisateur, incluant ses différents rôles pour la gestion des accès. On peut également gérer la durée de vie du jeton et grâce à la signature avec une clé de chiffrement, s'assurer que le jeton ait bien été émis par notre serveur.
Dépendance requise
Pour pouvoir gérer l'authentification avec les JWT, nous ajouterons la dépendence de départ OAuth2ResourceServer qui se trouve dans la catégorie Security.

Alternativement, on peut ajouter la dépendance suivante au fichier pom.xml
Configuration
Plusieurs parties de configuration pourront être récupérées de la configuration faite pour les pages JSP.
Les éléments suivants peuvent être récupérés sans changements:
CustomUserDetailsServicePasswordEncoderAuthenticationManager- La configuration faite pour les clés RSA lors de la vérification de courriel
Créer un utilisateur
La création des utilisateurs se fera également d'une façon similaire à ce qu'on connait déjà.
- L'application Java ne fournira pas de formulaire, ce sera l'application client qui le fera.
- L'API exposera un point de terminaison
POSTpermettant la création d'utilisateur, - On envoie tous les détails de l'utilisateur dans la requête.
- Le contrôleur reçoit la requête et l'envoie au service pour créer l'utilisateur dans la base de données.
Créer les utilitaires pour la gestion des JWT
Dans le fichier SecurityConfig.java, on ajoute un utilitaire pour encoder et décoder un JWT.
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(rsaKeyProperties.publicKey()).privateKey(rsaKeyProperties.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaKeyProperties.publicKey()).build();
}
On ajoute également un utilitaire permettant de convertir un JWT en un objet que Java peut utiliser pour l'authentification.
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("roles");
converter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
Créer un service dédié aux opérations sur les jetons (TokenService). On y ajoute une méthode pour générer le jeton de l'utilisateur connecté.
@Service
public class TokenService {
// Utiliser l'encodeur créé dans SecurityConfig.java
private final JwtEncoder jwtEncoder;
public TokenService(JwtEncoder jwtEncoder) {
this.jwtEncoder = jwtEncoder;
}
// Donner en paramètre l'objet authentication pour avoir accès aux détails de l'utilisateur
public String generateToken(Authentication authentication) {
Instant now = Instant.now();
// Récupérer les rôles
String roles = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
// Construire le jeton
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(5, ChronoUnit.MINUTES))
.subject(authentication.getName())
// On ajoute un "claim" personnalisé contenant les rôles. On pourra s'en servir pour les autorisations.
// La clé doit corresponde à la clé du convertisseur d'authentification.
.claim("roles", roles)
.build();
// Retourner le jeton créé sous forme de String
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
Modifier le SecurityFilterChain
- Retirer les configuration
formLoginetlogout. - Désactiver la protection CSRF avec
.csrf(csrf -> csrf.disable()) - Ajouter le serveur de ressources OAuth2 en lui donnant je convertisseur d'authentification
- Spéfier qu'on utilise une authentification sans état (Stateless)
Authentifier un utilisateur
Puisque nous n'utilisons pas .formLogin(), il faudra gérer la connexion nous même.
- L'API doit exposer un point de terminaison
POSTqui recevra la requête de connexion. - La requête de connexion doit recevoir le nom d'utilisateur (tel que défini dans le
CustomUserDetailsService) et le mot de passe. - On tente ensuite d'authentifier l'utilisateur grâce au
AuthenticationManager. - Si l'authentification fonctionne, on génère un JWT à partir de celle-ci et on l'envoie dans la réponse.
- Sinon on envoie une réponse avec le statut 401
Unauthorized.
@PostMapping("/auth/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request) {
try{
// Si l'authentification échoue, on ira directement au bloc catch
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
String token = tokenService.generateToken(authentication);
return ResponseEntity.ok().body(token);
}
catch(Exception e){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
Notez que ceci est une version simple de l'authentification. Il est possible d'utiliser des exceptions plus précises et faire plus de manipulation de l'utilisateur. Par exemple, si l'application client doit bloquer des ressources selon le rôle de l'utilisateur, il faudra les envoyer dans la réponse d'une façon ou d'une autre.
Rappel
Le jeton envoyé au client contient plus d'informations sur l'utilisateur, mais ce jeton est encodé de façon à ce qu'il ne puisse pas être lu ou modifié du côté client. On ne peut donc pas se fier sur celui-ci pour récupérer l'information de l'utilisateur connecté. On pourra créer une réponse plus complexe et la retourner au client lorsque la connexion est établie.
Retourner plus d'informations au client
Pour pouvoir retourner plus d'informations au client (afin de faire la gestion des autorisations par exemple), on pourra créer une nouvelle classe contenant ce que l'on veut retourner.
@Data
@AllArgsConstructor
public class AuthResponse {
private String token;
private String username;
private Set<String> roles;
}
- Mettre à jour la méthode d'authentification pour retourner ce type d'objet
- Construire l'objet dans la méthode
- Retourner
ResponseEntity.ok(authResponse)
@PostMapping("/auth/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
try{
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
// Récupérer le courriel
String email = authentication.getName();
// Récupérer l'utilisateur pour avoir ses rôles
Utilisateur utilisateur = utilisateurService.getByEmail(email);
if(utilisateur != null) {
Set<String> roles = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
String token = tokenService.generateToken(authentication);
// Retourner les informations au client
return ResponseEntity.ok(new AuthResponse(token, email, roles));
}
else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
catch(Exception e){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
Ainsi le client recevra un JSON qu'il pourra manipuler.
Tester avec Postman ou cURL
Pour créer ou connecter un utilisateur, il suffit d'envoyer les requêtes appropriées aux points de terminaison de l'API.
Pour tester un point de terminaison qui est protégé par l'authentification, il faudra fournir le JWT retourné lors de la connexion à l'application.
Dans Postman, on retrouve une section Authorization dans la création de requête. On doit choisir Bearer Token comme type d'authentification et fournir le JWT dans la boîte de texte associée.

Avec cURL, on devra spécifier l'entête à ajouter et ajouter le JWT.