Sécuriser une API REST avec JWT et Spring Boot
1. Introduction
JWT (JSON Web Token) est devenu un standard pour la sécurisation des applications Web. Son utilisation s’est beaucoup répandue ces dernières années du fait de l’augmentation d’applications utilisant le protocole HTTP(S) et encore plus avec le « passage au numérique » d’un grand nombre d’organisations. Le Web est partout et la sécurité des applications est un enjeux crucial. JWT permet d’identifier le consommateur d’une API et de s’assurer qu’il possède les permissions nécessaires.
Un token JWT est transmis à travers les headers d’une requête HTTP. Il est composé de 3 parties: le header, le payload, la signature. Les trois parties sont encodées en base 64 et concaténées avec un « . » afin de former un seul élément. C’est donc une manière compacte d’échanger des informations. La signature permet de s’assurer que le token n’a pas été altéré depuis sa création. Pour avoir plus de détails sur le fonctionnement de JWT et la structure d’un token, tu peux consulter le standard RFC 759.
Dans cet article, nous allons nous focaliser sur la mise en œuvre de JWT pour sécuriser une application REST avec Spring Boot et Java 17. Le module de Spring qui permet de gérer la sécurité est Spring Security.
Voici le scénario qui se déroule lorsqu’un utilisateur souhaite accéder à une ressource sécurisée :
- Un utilisateur s’authentifie avec ses credentials et reçois en retour un Token JWT.
- L’utilisateur tente d’accéder à une ressource sécurisée en passant le token dans les headers de la requête.
- L’application extrait le subject du token. Dans notre cas, le subject correspond au nom de l’utilisateur.
- L’application va ensuite vérifier que l’utilisateur existe et mettre à jour le contexte Spring Security avec l’utilisateur authentifié.
- L’utilisateur est donc autorisé à consommer la ressource qu’il demande.
Il s’agit ici d’un scénario simple où le token ne dispose pas de claims spécifiques. On utilise uniquement les champ sub, exp et iat mais selon le besoin on pourrait enrichir le token en y ajoutant des claims qui permettraient d’avoir une gestion plus fine des permissions par exemple. Voici à quoi ressemble le token (décodé) utilisé dans notre application grâce à https://jwt.io/:
{
"sub": "r.palvair@gmail.com",
"iat": 1649324897,
"exp": 1649325197
}
On peut également voir que le header détient l’information sur l’algo utilisé pour la signature :
{
"alg": "HS512"
}
2. La configuration
La configuration de la sécurité de notre application se trouve dans la classe WebSecurityConfiguration
. Cette classe définit un ensemble de composants qui surchargent par exemple des implémentations par défaut fournit par Spring. Ainsi, on peut voir que l’on a développé des implémentations pour les interfaces UserDetailsService
(jwtUserDetailsService), OncePerRequestFilter
(jwtRequestFilter). On a également la possibilité de configurer une instance de la classe DaoAuthenticationProvider
en spécifiant le PasswordEncoder
que l’on souhaite utiliser (BCryptPasswordEncoder) ainsi que l’instance UserDetailsService
. On utilise la fonction de hachage BCrypt de sorte que le mot de passe ne soit pas stocké « en clair ». Le PasswordEncoder
va donc encrypter le mot de passe envoyé par l’utilisateur lors de la tentative d’authentification.
Ensuite, on configure les règles de sécurité pour les requêtes entrantes. On dispose de plusieurs options afin d’activer par exemple la protection contre les attaques CSRF et d’activer CORS. On peut également brancher des « filters » qui seront exécutés à chaque requête. On utilise ici notre filter custom JWtRequestFilter
.
Pour finir, on rend accessible sans permissions le endpoint qui permet aux utilisateurs de s’authentifier le /authentifier
. Les autres endpoints sont quant à eux uniquement accessibles aux utilisateurs authentifiés.

Le point d’entrée : JwtRequestFilter
Chaque requête exceptée celles à destination du endpoint /authentifier sera interceptée par un filter JwtRequestFilter
qui est en charge de récupérer le token, de le valider grâce à un composant tiers et mettre à jour le contexte Spring Security. Ce filter étend la classe OncePerRequestFilter
qui comme son nom l’indique sera exécutée une seule fois pour chaque requête.

On remarque ici l’utilisation de composants comme le HeaderExtractor
et TokenValidator
. Le HeaderExtractor
permet de manipuler les headers de la requête et en particulier de récupérer le header Authorization. Le TokenValidator
permet de s’assurer que le token passé dans la requête est encore valide (pas expiré) et que l’utilisateur présent dans le champ sub existe bien dans le référentiel. Une fois le token validé, le context Spring security est mis à jour avec l’utilisateur identifié.
Implémentation de UserDetailsService
UserDetailsService
est une interface contenant une seule méthode dont l’objectif est de récupérer un utilisateur par son username. L’implémentation est propre à chaque application bien que la plupart du temps on peut relever des similitudes car bien souvent on utilise une base de données pour stocker les informations sur les utilisateurs. Dans notre cas, on utilise une base de données embarquée de type H2. Il s’agit d’une base données en mémoire qui est initialisée au chargement de l’application.
Etant donné que l’on utilise une base de données, on va donc se servir d’un Repository pour interroger notre base. J’ai donc mis en place une interface UserRepository
qui dispose de méthodes dont on a besoin à l’instant t et qui pourrait évoluer si nécessaire. La méthode qui nous intéresse particulièrement est getByUserName
qui comme son nom l’indique permet de récupérer un utilisateur à partir du son username.

A noter, que l’appel de la méthode loadUserByUsername
de l’interface UserDetailsService
est faite automatiquement par Spring Security lors de la phase d’authentification tout comme le cryptage du mot de passe en sollicitant le PasswordEncoder
.

Une fois l’authentification réussie, on retourne un objet implémentant l’interface UserDetails
. Cette interface contient un certains nombre de méthodes à implémenter. Selon les besoins de l’application, l’implémentation peut être plus ou moins complexe, ce n’est pas le cas ici, j’ai volontairement gardé quelque chose de simple. On retrouve donc 2 propriétés « custom » : nom, prenom. Les propriétés ou du moins les getters getPassword()
et getUsername()
sont prévues dans la déclaration de l’interface.
Le endpoint AuthenticationResource
Ce endpoint permet aux utilisateurs de s’authentifier en envoyant une commande contenant leur credentials.

On vérifie que la commande est correcte, que tous les champs sont renseignés et ensuite on appelle le service « métier » JwtAuthenticationService
qui est en charge d’authentifier l’utilisateur grâce aux composants Spring security et de générer le token JWT à transférer lors des prochaines requêtes. La logique métier est implémentée par la méthode authenticate(final AuthenticationCommand command)
.

En retour de l’appel, si l’utilisateur est authentifié, on reçoit une réponse en JSON dont le contenu comprend une propriété jwtToken
qui correspond au token JWT généré sous sa forme compacte.

Le endPoint UserResource
Comme vu dans le début de cet article, tous les endpoints de l’application excepté celui pour que les utilisateurs s’authentifient soit AuthenticationResource
sont sécurisés. Autrement dit, il est nécessaire de fournir un token JWT valide afin de pouvoir y accéder. Dans le cas contraire, une exception sera levée et l’appelant recevra une réponse avec un code HTTP correspondant au cas de figure. Le endpoint UserResource une fois accessible par un utilisateur authentifié se charge de retourner les informations d’un utilisateur par son email.
Démo
Avec l’aide du logiciel Postman, je vais effectuer des requêtes sur les 2 endpoints développés pour démontrer le fonctionnement de l’application. Les 2 requêtes sont les suivantes :
- Exécuter une requête d’authentification sur le endpoint /authentifier avec les credentials appropriés dans le but d’obtenir un token JWT.
- Exécuter une requête de consultation sur un endpoint sécurisé en l’occurence /users/{email} pour récupérer les infos d’un utilisateur.
Requête d’authentification
L’application une fois lancée est disponible via l’url http://localhost:8080/jwt-authentication
. On a donc besoin d’appeler le endpoint /authentifier
pour comme son nom l’indique authentifier l’utilisateur en utilisant l’url http://localhost:8080/jwt-authentication/authentifier
avec le verbe POST. Dans le corps de la requête, on passe un payload en json avec les champs email
et password
qui correspondent à la commande attendue par la ressource REST. En retour, étant donné qu’on utilise les bons credentials, on a un code HTTP 200 et une réponse en JSON qui contient le token JWT dans un champ token
(pas de confusion possible pour le coup).

Requête de consultation
Pour récupérer un utilisateur par son email, on utilise le endpoint /users/{email}
ou {email} est remplacé par l’email de l’utilisateur que l’on recherche. On prendra soin de passer le token récupéré précédemment dans le header Authorization. En sortie, on a un code HTTP 200 et une réponse en JSON qui contient le détail de l’utilisateur.

J’ai démontré ici les cas passants mais il existe d’autres scénarios qui peuvent être explorés comme par exemple:
- Récupérer des infos d’un utilisateur sans token afin de vérifier que l’on reçoit bien un code HTTP 403 Forbidden.
- Dans la même idée, récupérer les infos d’un utilisateur avec un token expiré produira un code HTTP 403 Forbidden.
Conclusion
Dans cet article, on a vu comment sécuriser une application avec JWT à l’aide du Framework Spring Boot et plus particulièrement Spring Security pour la couche sécurité. Bien que Spring Security soit un framework plutôt complet, il nous faut développer nous-même la gestion de l’authentification avec JWT. Dans un prochain article, on verra comment sécuriser une application mais cette fois-ci en déportant la gestion du token JWT a un serveur d’authorization comme KeyCloack.
Le code source de cet article est disponible ici sur mon github.