Bienvenue dans ce dernier chapitre du cours sur Spring Security ! Nous allons voir comment sécuriser une API grâce à Spring Security et JWT (JSON Web Tokens). Ce n’est pas une mince affaire et autant vous prévenir de suite, le niveau est assez élevé. D’ailleurs, ce chapitre est avant tout une entrée en matière, vous aurez encore de nombreuses choses à découvrir par la suite ! Prêt ? C’est parti !
C’est quoi la différence ou lien entre Spring security et JWT ?
Commençons par définir le contexte. Nous développons une API REST qui sera consommée par un ou plusieurs clients, telle qu’une application web. Notre API a ainsi le rôle de serveur de ressources (en anglais, resource server). Et nous ne voulons pas que n’importe qui accède aux ressources mises à disposition par l’API. Il nous faut donc sécuriser notre API.
Mais comment ?
Réfléchissons au caractère sans état (en anglais, stateless) d’une API REST. Cela signifie que d’une requête à l’autre on ne conserve pas d’état, autrement dit d’information sur le serveur. On exclut donc une authentification par session et on va avoir besoin d’une authentification par jeton (en anglais, token authentication).
Avec cela à l’esprit, vous êtes à même de faire le lien avec les JSON Web Tokens dont nous avions parlé dans le chapitre Sécurisez l’accès à une application en utilisant l’authentification et l'autorisation.
Autre aspect, nous voulons nous passer d’un serveur d’autorisation (en anglais, authorization server) externe à notre API. L’objectif est qu’elle soit autonome pour l’authentification des requêtes.
Est-ce vraiment une bonne idée ?
Oui ! Et non ! Enfin, ça dépend. Volontairement, je ne vais pas développer mais retenez que cela dépend de vos contraintes. Par exemple, si vous avez besoin de jetons d’actualisation (en anglais, refresh tokens), ce que nous allons voir ne conviendra plus. Mais si vous avez un unique backend sous forme d’API au sein de votre architecture c’est une très bonne solution.
Voyons donc comment utiliser Spring Security et JWT pour sécuriser une API !
Mettez en place votre projet
Vous êtes désormais rodé à la création de projet avec Spring Boot, je vais donc simplement vous demander de créer une nouveau projet avec les critères suivants :
Spring Boot, version 3.X
Java, version 17
Package : com.openclassrooms
Dépendances :
Spring Web
OAuth2 Resource Server (qui inclut Spring Security)
La dépendance OAuth2 Resource Server est la nouveauté ! Elle nous permet de mettre en place une authentification par jeton (précisément, bearer token authentication).
Bien évidemment, il nous faut ensuite un point de terminaison (en anglais, endpoint) permettant à des clients d’interagir avec l’API. Créons donc un contrôleur REST nommé ResourceController dans un nouveau package com.openclassrooms.controllers :
package com.openclassrooms.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ResourceController {
@GetMapping("/")
public String getResource() {
return "a value...";
}
}
Maintenant, attaquons-nous à la sécurité !
Configurez Spring Security
Laissez-moi vous montrer comment configurer Spring Security avec la démonstration suivante :
Reprenons le code, étape par étape :
Pour commencer créons une nouvelle classe nommée SpringSecurityConfig dans un nouveau package com.openclassrooms.configuration.
Voici la configuration que nous mettons en place :
CSRF est désactivé car non nécessaire dans ce scénario.
La sessionPolicy est STATELESS.
Toutes les requêtes doivent être authentifiées.
Le mode d’authentification est une HTTP Basic Auth (envoi d’un username et d’un password).
Nous créons également un utilisateur en mémoire et fournissons un encodeur BCrypt.
Et voilà le résultat :
package com.openclassrooms.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
public UserDetailsService users() {
UserDetails user = User.builder().username("user").password(passwordEncoder().encode("password")).roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Testez votre travail avec Postman :
C’est déjà fini ? Je croyais que ce serait compliqué !
Pas si vite ! Cela fonctionne mais cela ne vous a pas échappé nous n’utilisons pas encore une authentification par jeton mais le mode HTTP Basic Auth. Je souhaite simplement y aller petit à petit. Voyons la suite.
Complétez votre chaîne de filtres de sécurité
L'API REST recevra un JWT avec la requête entrante. Il faut donc que l’API soit capable de traiter ce token. Nous allons nous servir d’un filtre de sécurité disponible nommé BearerTokenAuthenticationFilter.
Pour l’ajouter à notre chaîne de filtre, tirons profit de la dépendance OAuth2 Resource Server et ajoutons l’instruction suivante dans la méthode filterChain :
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))
Effet boule de neige, nous devons fournir un JwtDecoder !
Générez une clé 256 (via un générateur en ligne par exemple) puis ajouter le code suivant dans la classe SpringSecurityConfig :
private String jwtKey = "laclegeneree256….";
@Bean
public JwtDecoder jwtDecoder() {
SecretKeySpec secretKey = new SecretKeySpec(this.jwtKey.getBytes(), 0, this.jwtKey.getBytes().length,"RSA");
return NimbusJwtDecoder.withSecretKey(secretKey).macAlgorithm(MacAlgorithm.HS256).build();
}
Codez la génération du JWT
Notre code est désormais en capacité de décoder un JWT et donc de traiter une requête entrante qui possède un Bearer Token. Si le token est valide, alors l'émetteur de la requête accède à la ressource demandée.
Mais encore faut-il permettre aux clients de l’API d’obtenir un token. Nous allons donc ajouter un nouveau point de terminaison /login dans une classe LoginController. Pour générer le token, créons une classe JWTService avec une méthode generateToken().
Retrouvez-moi dans la démonstration ci-dessous :
Reprenons le code. Tout d’abord la classe JWTService :
package com.openclassrooms.services;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
@Service
public class JWTService {
private JwtEncoder jwtEncoder;
public JWTService(JwtEncoder jwtEncoder) {
this.jwtEncoder = jwtEncoder;
}
public String generateToken(Authentication authentication) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(1, ChronoUnit.DAYS))
.subject(authentication.getName())
.build();
JwtEncoderParameters jwtEncoderParameters = JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims);
return this.jwtEncoder.encode(jwtEncoderParameters).getTokenValue();
}
}
J’attire votre attention sur le fait que l’objet JwtEncoderParamaters est généré à partir de l’objet JwtClaimsSet, lui même construit avec plusieurs informations :
issuer : un nom arbitraire pour l'émetteur du token ;
issuedAt : moment d’émission ;
expiresAt : moment d’expiration (ici 1 jour) ;
subject : le nom de l’utilisateur associé.
La classe JWTService a donc besoin d’une instance de JwtEncoder valide, fournissons la via notre configuration de sécurité. La méthode suivante a été ajoutée dans la classe SpringSecurityConfig :
@Bean
public JwtEncoder jwtEncoder() {
return new NimbusJwtEncoder(new ImmutableSecret<>(this.jwtKey.getBytes()));
}
Maintenant, passons au LoginController :
package com.openclassrooms.controllers;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.openclassrooms.services.JWTService;
@RestController
public class LoginController {
private JWTService jwtService;
public LoginController(JWTService jwtService) {
this.jwtService = jwtService;
}
@PostMapping("/login")
public String getToken(Authentication authentication) {
String token = jwtService.generateToken(authentication);
return token;
}
}
Le code est très explicite : l'endpoint /token va appeler le service pour générer un token sur la base des informations de l’utilisateur connecté.
Décrivons le cheminement :
Une requête de type POST sur l’URL /token avec un header “Basic Auth” (c’est-à-dire le nom d’utilisateur et le mot de passe encodé en base64) est envoyée.
Spring Security commence par authentifier cet utilisateur puis met à disposition une implémentation de l’interface Authentication dans le contexte Spring de la sécurité (en anglais, SecurityContext).
Lors de l’exécution de la méthode getToken du contrôleur, l’objet de type Authentication est transmis à JWTService pour en extraire une information et ainsi générer le token.
Le token est retourné via la réponse à la requête.
Nous n’avons plus qu’à tester avec Postman. Tout d’abord, la génération du token :
Puis maintenant, on utilise notre token pour accéder à la ressource, c’est à dire à notre point de terminaison / qui renvoie une donnée :
Félicitations ! Vous avez une API REST sécurisée avec JWT qui est en mesure de générer et valider un token.
Je ne vous cache pas que ce chapitre est avant tout une entrée en matière. Je vous encourage à aller plus loin :
Utilisez une clé asymétrique et en n’écrivant rien en dur (le mécanisme de Spring Boot pour gérer une configuration externalisée fera votre bonheur)
Ajoutez un scope dans le token généré par la classe JWTService et s’en servir comme critère d’autorisation (ainsi en Basic Auth on accède uniquement à /login).
Et pour aller encore plus loin, vous pouvez consulter la documentation officielle de Spring Security.
En résumé
Une API peut être sécurisée grâce à un mécanisme de validation de JWT implémenté au sein de l’API grâce à Spring Security.
Le module OAuth2 Resource Server de Spring nous offre un filtre de servlet prêt à l'emploi pour valider un JWT.
La génération d’un token est rendue possible grâce à différentes classes/interfaces de Spring Security comme JwtClaimsSet, JwtEncoderParameters ou encore JwtEncoder.
Félicitations, vous avez terminé ce cours ! Je vous invite à réaliser le dernier quiz pour valider votre formation. Bonne continuation !