• 8 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 16/11/2023

Sécurisez votre API avec JWT

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 :

  1. CSRF est désactivé car non nécessaire dans ce scénario.

  2. La sessionPolicy est STATELESS.

  3. Toutes les requêtes doivent être authentifiées.

  4. 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 :

La requête est de type GET avec l’url localhost:8080. L’onglet “Authorization” spécifie un type Basic Auth avec le username “user” et le password “password”.
Requête exécutée avec Postman sur l’API REST.

 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 :

  1. 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.

  2. 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).

  3. 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.

  4. 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 :

La requête est de type POST avec l’url localhost:8080/login. L’onglet “Authorization” spécifie un type Basic Auth avec le username “user” et le password “password”.
Requête exécutée avec Postman sur l’API REST pour générer le token.

Puis maintenant, on utilise notre token pour accéder à la ressource, c’est à dire à notre point de terminaison / qui renvoie une donnée :

La requête est de type GET avec l’url localhost:8080. L’onglet “Authorization” spécifie un type Bearer Token avec le token précédemment obtenu renseignée.
Requête exécutée avec Postman sur l’API REST pour récupérer la ressource.

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 !

Exemple de certificat de réussite
Exemple de certificat de réussite