Article écrit par Louis Jeanne-Julien
Tout développeur ayant touché de près ou de loin aux questions d’authentification, de session, et de sécurité dans une application s’est un jour posé des questions comme : “Comment mon application sait que c’est toujours moi qui suis connecté à chaque requête?”, ou “Pourquoi je suis automatiquement déconnecté après un certain temps?”.
Ces questions peuvent avoir différentes réponses selon vos méthodes de gestion d’authentification. Je propose dans cet article de traiter cette question dans le cadre de l’utilisation de Spring Security, dont les principaux concepts sont résumés dans cet autre article.
1. Security Context : l‘élément central
Les requêtes vers les endpoints d’une API REST implémentant Spring Security peuvent nécessiter ou non d’être authentifié. Pour vérifier qu’un utilisateur est authentifié, Spring Security regarde si son Security Context contient un objet de type Authentication. Autrement dit, le Security Context est le sac à stocker l’authentification en cours de validité.
Si l’utilisateur n’est pas authentifié alors qu’il y en a besoin, dans le cas général, il sera redirigé vers la page de login pour procéder à l’authentification via un formulaire identifiant + mot de passe.
Admettons maintenant que le Security Context possède un objet Authentication. L’utilisateur peut maintenant accéder aux endpoints sécurisés, selon ses droits. Enfin, jusqu’à sa déconnexion du moins… Il est en effet normal qu’un utilisateur puisse mettre fin à sa session de connexion, ne serait-ce que pour éviter qu’une autre personne utilise son compte. Dans ce cas, le contenu du Security Context est nettoyé.
2. Les sessions dans Spring Security
Il existe d’autres situations dans lesquelles le contenu du Security Context est supprimé. Ces situations dépendent de la gestion des sessions que le développeur a configuré pour Spring Security. Un exemple de cette configuration est présenté dans l’image ci-dessous, à la ligne 64 :
Quatre configurations de session sont possibles, chacune impactant différemment la gestion du Security Context.
- SessionCreationPolicy.ALWAYS : l’application est alors dite stateful, ce qui signifie que son état est sauvegardé à chaque requête. Cela implique que le Security Context est conservé par Spring Security, et qu’il peut être réutilisé pour plusieurs requêtes sans que l’utilisateur doive s’authentifier à nouveau.
- SessionCreationPolicy.IF_REQUIRED : c’est l’option par défaut de Spring Security. La session (et donc le Security Context) n’est sauvegardée que lorsque c’est nécessaire. La définition de ‘nécessaire’ selon Spring Security reste floue pour moi, malgré une lecture attentive de la documentation.
- SessionCreationPolicy.NEVER : Spring Security ne sauvegarde aucune session, mais peut utiliser les sessions existantes qui auraient été sauvegardées autrement. Il faut en effet garder à l’esprit que la configuration des sessions présentée ici ne concerne que le comportement de Spring Security, et qu’un autre framework utilisé dans l’application est tout à fait susceptible d’enregistrer ses propres sessions.
- SessionCreationPolicy.STATELESS : Spring Security ne sauvegarde aucune session, et ne consulte aucune session existante. Cette option implique que le Security Context expire (et est donc nettoyé) à chaque requête, et donc qu’une preuve d’authentification/de droits doit être fournie à Spring Security avec chaque requête à destination d’endpoints protégés par les fonctions authenticated() ou hasAuthority().
Nous n’allons pas détailler chaque option de la liste, car la gestion des sessions est un sujet à part entière, mais nous allons considérer dans la suite de cet article que l’option retenue est la dernière : il s’agit en effet de l’option que l’on peut considérer comme étant ‘la plus sécurisée’, car les informations d’authentification sont ré-initialisées à chaque requête.
La question qui se pose maintenant est la suivante : comment éviter dans un tel scénario de demander constamment à l’utilisateur de renseigner son identifiant et son mot de passe ?
3. L’utilisation d’un JWT
Un JWT (Json Web Token) est un objet Json contenant des clefs et des valeurs, usuellement crypté sous forme d’une simple chaîne de caractères. Son utilisation typique dans un contexte d’authentification implique qu’il contient notamment un nom d’utilisateur, une liste de droits, et une date de fin de validité. Si un JWT contenant ces informations est joint à une requête, il est possible de dire à Spring Security d’utiliser ce JWT comme preuve d’authentification, tant que la date de validité n’est pas expirée.
Ainsi, la gestion de l’authentification suit alors les grandes étapes suivantes :
Mais alors, d’où vient ce JWT contenant ces informations, et comment est-il joint à une requête ?
A. Création du JWT à partir d’une Authentication
Lorsqu’un utilisateur se connecte à l’application sur une page de login, Spring Security utilise les informations renseignées pour créer un objet de type Authentication. Cet objet :
- est stocké dans le Security Context jusqu’à la fin de la requête.
- est utilisé pour générer un JWT selon les étapes suivantes :
Le JWT est alors renvoyé avec la réponse à la requête de connexion.
B. Envoi du JWT avec chaque requête
Une fois le JWT généré et récupéré, il faut le transmettre à Spring Security avec chaque nouvelle requête, pour prouver l’identité de l’utilisateur. Typiquement, le JWT est adjoint aux requêtes dans un header, qui par convention est nommé “Authorization”. Toujours par convention, sa valeur contient la chaine “Bearer ” à laquelle est concaténé le JWT crypté
L’ensemble connexion + création de JWT peut donc être résumé dans le schéma suivant :
A présent, nous connaissons les processus de connexion, de génération d’un JWT, et de vérification d’authentification par consultation du Security Context.
Il nous reste à éclaircir les processus de lecture/validation du JWT, et de l’utilisation du JWT pour peupler le Security Context.
C. Lecture/validation du JWT
La lecture du JWT et sa validation comprend 4 vérifications majeures :
- l’objet est-il bien un JWT ?
- le contenu du JWT est-il bien non vide ?
- le JWT est-il de la forme attendue ?
- le JWT est-il bien non expiré ? Il faut ici préciser que la durée de validité des JWT générés est configurée par le développeur. Il est fréquent d’utiliser des jetons valides pour 1h, afin que l’utilisateur soit automatiquement déconnecté passé ce délai.
Le processus de lecture/validation du JWT peut donc être résumé par le schéma suivant :
D. Valorisation du Security Context via lecture du JWT
Si Spring Security devait être résumé en un seul concept, ça serait “liste de filtres”. Lorsqu’une requête arrive sur l’API implémentant Spring Security, elle subit le traitement de nombreux filtres avant d’arriver enfin au endpoint ciblé. Certains de ces filtres sont mis en place automatiquement par Spring Security, et d’autres peuvent être mis en place par le développeur afin d’effectuer certains traitements.
C’est avec l’un de ces filtres customisés que l’on met en place la récupération, la lecture, et l’utilisation du JWT pour créer un objet Authentication qui sera stocké dans le Security Context. Voici donc les étapes constitutives du filtre, qui implémente l’interface OncePerRequestFilter fournie par Spring Security :
Et voilà comment Spring Security peut comprendre au cours de plusieurs requêtes consécutives que le même utilisateur est toujours connecté, sans avoir à redemander les identifiants, et sans conserver de données d’authentification entre deux requêtes.
4. Pour résumer
Si l’on ne considère que le filtre en charge de vérifier l’authentification d’un utilisateur, le parcours d’une requête arrivant sur une API implémentant Spring Security tel que dans notre exemple peut être résumé comme suit :
- une requête arrive vers l’API.
- un filtre vérifie si la requête contient un JWT d’authentification.
- le Security Context est valorisé selon l’objet Authentication généré à partir du JWT, ou est laissé vide si aucun JWT n’a pu être généré.
- si le endpoint cible est sécurisé, utilisation du Security Context pour vérifier l’authentification. Dans le cas contraire, pas de vérification d’authentification.
- en cas d’absence d’une authentification pourtant requise, redirection vers un formulaire de connexion.
- si la connexion est réussie, l’API génère un JWT valide pour une certaine durée. Ce JWT doit être renvoyé avec chaque requête comme preuve d’authentification.
Sources de l’article
https://spring.io/projects/spring-security
Contact
Besoin d’informations complémentaires? Besoin d’aide pour implémenter tout ça concrètement dans une API? Vous pouvez m’écrire à l’adresse louis.jeanne-julien@admin
Liens externes
Vous vous demandez qui est Ouidou ? N’hésitez pas à nous contacter via contact@ouidou.fr ou visiter notre site https://ouidou.fr