C'est du JWT et ça se prononce jot

Posted on Jan 18, 2023

On va parler sécurité, donc je mets un mur

S’il y a un élément qui me plait dans le monde informatique, c’est bien les débats : framework X vs Y, langage A vs B, Mac vs Linux vs Win$, Arch Linux vs the world, Sys admin vs le reste de l’équipe.

Le problème des débats, c’est qu’ils peuvent générer du bruit et masquer l’information utile. C’est justement ce que j’ai rencontré lors de l’implémentation de JWT sur un projet interne à Coddity, et sur un cas spécifique : la gestion de sessions utilisateur.

Ce qui me plait sur ce sujet, c’est que c’est une vraie question d’ingénierie qui a des répercussions sur la sécurité, la complexité d’un système et l’intégrité des données.

Entre les tutoriels todo-app, et les articles plus sérieux qui virent au combat d’égos dans les commentaires, il devient ardu d’avoir une vision claire de l’utilisation de JWT et de ce qu’il faut implémenter sur ce cas d’utilisation. Et spoiler alert : à minima ce n’est pas simple, et ce n’est pas la meilleure idée du monde.

L’objet de l’article est donc de synthétiser, en Français, ce qu’est JWT, ce qu’il permet, ce qu’il ne permet pas, les risques qu’il introduit, dans le cas de la gestion d’une session utilisateur.

Coté hashtags, nous allons parler : #norme, #chiffrement, #stateless, #révocation, #none, #XSS, #CSRF, #cookie, #BDD

DISCLAIMER1 : Article écrit par un humain sans l’intervention de ChatGPT. DISCLAIMER2 : Illustrations de l’auteur

Encore ce meme ???

JWT et ça se prononce [jot]

Le JWT est défini dans les RFC suivantes:

  • RFC 7519, datant de 2015 et spécifiant le format
  • RFC 7797, spécifiant les options sur la signature d’un JWT : le JWS
  • RFC 8725, plus récente (2020), portant sur les bonnes pratiques d’implémentation.

Déjà, le fait que le JWT se voit dédier une RFC complémentaire après 5 ans et portant sur les bonnes pratiques doit logiquement faire lever un sourcil.

Objet du JWT

Que dit la RFC ?

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

JWT est un mécanisme d’échanges de données, les “claims”, entre 2 systèmes. Celles-ci sont encodées dans un JSON et signées. La génération du JWT est réalisée par le serveur applicatif qui va le signer avec une clef, lui permettant de vérifier l’authenticité des données par la suite lors des échanges avec le client. Les claims contenues dans le JWT étant signées et non chiffrées, les données échangées dans un JWT sont en clair, encodées simplement en base64.

Structure

Un JWT est représenté par une séquence constituée de plusieurs parties séparées par un ‘.’ Chaque partie étant une composante du JWT, encodée en base64:

  • header : contenant le type et l’algorithme utilisé pour signer
  • payload : contenant les claims, c’est à dire les informations que nous transportons
  • signature : la signature du header et du payload selon algorithme de chiffrement

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.YEAWENac8QpCBNlp1XVecfh8CY1DGHfO_vnrsiA7B20

va représenter la structure suivante :

  • Header :
{
  "alg": "HS256",
  "typ": "JWT"
}
  • payload :
{
  "sub": "1234567890",
  "name": "Debbie Harry",
  "iat": 1516239022
}
  • signature : HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload)) avec une clef secrète “un super secret”

Le header va contenir les informations nécessaires à la vérification de la signature du JWT en particulier l’algorithme utilisé. J’attire votre attention que c’est le JWT qui va donner au serveur l’instruction pour vérifier l’intégrité de celui-ci. On en reparle plus bas.

Claims

Les claims sont les données échangées dans le JWT, certaines étant attendues dans le JWT pour que celui-ci ne soit pas rejeté par le serveur. Et, rien ne vous empêche de mettre les paroles de “blitzkrieg bop” des Ramones dans le JWT, vous pouvez créer vos propres claims (en fait il y a une limitation, et de taille, qu’on verra plus bas).

Les principaux claims normés sont :

  • “iss” pour issuer : identifie le système qui émet le JWT (optionnel)
  • “sub” pour subject : identifie l’objet du JWT (optionnel)
  • “aud” pour audience : identifie le système auquel est dédié le JWT (optionnel)
  • “exp” pour expiration time : obvious (optionnel)
  • “nbf pour not before : identifie le temps avant lequel le JWT ne doit pas etre accepté (optionnel)
  • “iat” pour issued at : obvious (optionnel)
  • “jti” pour JWT ID : identifiant unique du JWT (optionnel)

Vous remarquerez que tous ces claims sont optionnels selon la RFC. WAIT FOR IT : nous verrons plus loin que ces éléments sont importants et sont de potentielles failles de sécurités si ces claims sont mal utilisés.

Et comme dit précédemment, rien de ne vous empêche d’avoir un custom claims qui contient toute votre représention d’un user.

  • payload :
{
  "sub": "1234567890",
  "iat": 1516239022,
  "user":{
    "userID":"id",
    "firstName":"Debbie",
    "lastName":"Harry",
    "address":"1st street, NYC, USA",
    "employer": "Blondie",
    "bankAccount":"US3723000144 0049 123123"
  }
}

Signature

La signature sera…(roulement de tambours)…la signature du header et du payload selon l’algorithme de chiffrement spécifié dans le header.

Debugger

Vous pouvez utiliser ce debbuger pour tester vos JWT

Avantages

Une fois ces éléments admis, les avantages du JWT sont apparaissent :

  • Le JWT contient des données qui sont accessibles par le serveur sans passer par une base de données : vous pouvez passer en claims les informations sur l’utilisateur que vous souhaitez, pas la peine de les récupérer avec une requête en base (wait for it)
  • La signature du JWT garantit l’intégrité des données transmises
  • Pas la peine de stocker en DB un token de session, JWT est stocké, une fois qu’il est généré par le serveur, côté client. Le JWT permet donc une autorisation des requêtes stateless
  • Il peut être partagé facilement entre différents services
  • Un JWT peut être utilisé facilement dans un environnement qui scale

Session utilisateur

Stateful vs Stateless

Une session utilisateur est un moyen de créer un état (stateful) pour un client qui utilise un service dans un système d’échange de données qui est majoritairement sans état (stateless).

Le cas d’usage le plus simple est de considérer un utilisateur qui se connecte à une application en fournissant ses identifiants de connexion et qui, pour chaque requête, entre le client et le serveur, fournit un moyen à ce dernier d’identifier et d’autoriser l’utilisateur.

cookie de session

A la création de la session, lors du login, le serveur peut :

  • la lier à un utilisateur unique
  • lui associer des permissions
  • lui associer une durée de vie
  • lui associer des données

Le serveur crée un ID de session et n’a pas à envoyer de données construite au client.

Cycle de vie d’une session

La session peut être amenée à se terminer dans différents cas :

  • l’utilisateur se déconnecte
  • la session expire naturellement
  • le serveur invalide la session pour des raisons de sécurité par exemple
  • le serveur bannit l’utilisateur

Dans tous les cas, il suffit au serveur de supprimer la session en DB pour prendre en compte cette terminaison.

Persistance

A chaque requête de l’utilisateur une connexion à la base de données qui stocke la session est nécessaire pour l’autoriser. Et ce dernier point apporte une problématique : se connecter à une DB à chaque requête est coûteux en termes de performance. On pourrait stocker les sessions dans la mémoire du serveur mais cela empêche de facto la scalabilité du système. Cependant, l’utilisation d’une base de données in-memory pour stocker les sessions, peut permettre de limiter la baisse de performance pour chaque requête.

Côté client, la persistance de la session utilisateur se fait traditionnellement par cookies de session qui, en fonction de l’implémentation, peuvent ouvrir des brêches de sécurité, on en parlera un peu plus bas.

Créer une session avec un JWT

génération du JWT par le serveur

“Mais attends, si j’ai bien suivi, on peut utiliser le JWT puisqu’il est stateless et il suffit de vérifier sa signature pour le valider! Comme ça on élimine la DB, c’est plus rapide et sécurisé ! YOLO” Boris, jeune développeur

validation de la requête

Pow Pow Pow, on se calme

Le JWT pour gérer les sessions utilisateurs est-il vraiment Stateless ?

Oui le JWT est stateless. Un utilisateur se loggue, le serveur génère un JWT avec les informations de l’utilisateur et il est renvoyé au client qui pourra le renvoyer dans ses échanges avec le serveur.

Sauf que… Vous voyez venir le problème.

Comme il n’y a pas de persistance du JWT côté serveur, celui-ci ne peut que vérifier sa signature, vérifier sa “date de validité” et, si les claims ont été correctement créés, si l’utilisateur peut accéder à la ressource demandée.

En revanche : comment déconnecter un utilisateur ? comment bannir un utilisateur ? Impossible d’invalider un JWT et jusqu’à sa date d’expiration, l’utilisateur pourra accéder à la ressource demandée.

“Ok dans ce cas on rajoute une DB avec les JWT bannis ou déconnectés et c’est reglé non ? Ou mieux changer de clef de signature ? " Toujours Boris

Effectivement, on peut ajouter une DB, rapide, in-memory, pour stocker les JWT que le serveur doit considérer comme invalides. Et… On se retrouve avec la même architecture que les cookies de session. Et changer la clef ? Tous les JWT de tous les utilisateurs seront invalidés. Solution à écarter dès qu’on dépasse les 2 utilisateurs. Une autre idée serait de donner des durées de vie très courte, mais celà pose un problème d’utilisation. Un utilisateur qui se retrouve toutes les 2 minutes devant un prompt de connexion ne va pas apprécier. On pourrait utiliser un refresh token, mais on retourne à la case départ, puisque ce refresh token doit pouvoir être invalidé, donc utilisation d’une DB etc.

Question de poids

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySUQiOiJzM2c0NTYzMXFzZzEzcXNkZmciLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTIwMn0.lMX8ytjSqvingScbR3IQrbZ3aESaSeQuhfg2nsvUXFQ qui correspond aux données de claims suivantes :

{
  "userID": "s3g45631qsg13qsdfg",
  "iat": 1516239022,
  "exp": 1516239202
}

Ce claims est le strict minimum que nous pouvons implémenter : un userID permettant d’identifier l’utilisateur, une date d’expiration, une date de délivrance, le tout faisant 168 octets. Quelle serait la taille d’un cookie de session signé équivalent ? Quelques dizaine d’octets soit à minima un facteur 5. D’autant qu’avec ces claims basiques, nous le verrons plus bas, nous n’avons pas une solution sécurisée, nous devrons ajouter des clefs. Imaginons que vous souhaitiez rajouter des claims liés à l’utilisateur (rien ne vous empeche, ça évite les appels en DB), la taille du JWT va inexorablement augmenter.

Mais en quoi est-ce un problème ?

Outre le fait d’augmentation de la bande passante nécessaire, comment transmettre le JWT ? Nous le verrons plus bas, la solution la plus sécurisée est de le transmettre dans un cookie HTTPOnly. Mais attention, la taille maximum des cookies autorisée pour un domaine est de 4kb par les navigateurs, taille pouvant être vite atteinte si vous déposez d’autres cookies côté client. Le poids du JWT peut donc vous pousser à le transmettre dans le header, et de le stocker dans le local storage du navigateur : alerte sécurité.

Persistence coté client

La règle d’or :

“never trust information from the client” random senior dev

Imaginons que pour X raisons, le JWT soit transmis dans un header et stocké dans le local storage du navigateur. Votre JWT est vulnérable aux attaques XSS, c’est à dire une injection de javascript malicieux dans votre code client. La solution est de le passer dans un cookie, et pour que le cookie ne soit pas accessible par le JS, et contrer ainsi une attaque XSS, vous devez le passer en cookie HTTPOnly. Mais souvenez vous, le JWT est signé et non chiffré, vous pouvez accéder aux données du JWT et ses claims dans les outils de développement des navigateurs par exemple. Il est donc préférable de signer et chiffrer le cookie. (Vous pouvez aussi ajouter le chiffrement du contenu du payload du JWT, pour la déconne).

Ce qui nous fait tout de même plusieurs opérations de chiffrement, qui sont gourmandes en CPU et en temps de traitement, pour chaque requête :

  • signature du JWT lors de sa création
  • signature du cookie HTTPOnly
  • chiffrement du cookie HTTPOnly
  • déchiffrement du cookie
  • vérification de la signature du cookie
  • vérification de la signature du JWT

Vous auriez utilisé dès le départ une solution de cookie de session, vous aurez économisé de facto toutes les opérations de chiffrement liées au JWT qui sont ici non nécessaires.

Le problème avec l’utilisation des cookies HTTPOnly, c’est que vous êtes ouvert aux attaques de type CSRF ! Vous vous retrouvez à devoir intégrer une protection supplémentaire dans vos mécanismes d’échanges entre le client et le serveur (mais je tire le trait, cette protection est indispensable dès lors que vous gérez de l’authentification utilisateur avec des cookies).

Synthèse de l’architecture

=> Pour gérer le cycle de vie d’une session utilisateur, vous devez utiliser une base de données qui sera consultée à chaque requête.

=> Pour pouvoir utiliser le JWT de façon totalement sécurisée, vous devez les transmettre via des cookies HTTPOnly qui nécessitent eux aussi une opération de chiffrement supplémentaire.

=> Pour pouvoir utiliser un JWT dans un cookie, vous pouvez être amené à limiter les informations transmises dans les claims.

Utiliser le JWT pour gérer une session utilisateur nous fait perdre tous ses avantages : stateless, transport d’information utile, rapidité. Nous verrons un peu plus bas dans quel cas celui-ci est au contraire très intéressant.

Vulnérabilités d’implémentation

En introduction, je parlais d’une RFC établie 4 ans après la RFC initiale du JWT, portant sur les best practice de mise en place du JWT qui s’adresse à la fois aux créateurs de librairies JWT et aux utilisateurs de ces librairies. Si cette nouvelle RFC existe, c’est qu’il y a de bonnes raisons.

Je laisse de côté volontairement les risques portant sur la génération du JWT ou sa vérification par les librairies, charge à vous et à l’industrie d’identifier celles implémentant correctement la RFC.

Le chiffrement

JWT intègre dans sa définition la possibilité d’utiliser un algorithme un peu particulier, l’algorithme none, c’est à dire aucun algorithme de chiffrement, qui va permettre de ne pas signer le JWT. Etrange non, pour un token d’authentification ? Celui-ci a été implémenté pour les cas où l’intégrité du token est déjà protégée par un chiffrement d’une couche de transport, par exemple TLS.

Le problème étant que certaines librairies implémentant le JWT considèrent / considéraient un JWT signé avec l’algorithme none comme parfaitement valide et ne vont pas rejeter la requête ! Comment est ce possible ?

Souvenez-vous que le header du JWT contient un champs “alg”, celui-ci donne au serveur le type d’algorithme à utiliser pour vérifier la signature.

Attendez une minute, pour valider le payload, il faut utiliser un information founie par le client, une source non sûre ?

Un attaquant peut donc intercepter le JWT et le renvoyer en modifiant ce champs, l’utilisant comme vecteur d’attaque comme le cas de l’algorithme none.

Pour contrer le problème de l’algorithme none, la solution simple est de refuser toute utilisation de celui-ci si une clef a été fournie dans l’implémentation.

D’autres failles existent quant au choix des algorithmes de chiffrement, je vous renvoie à cet article et celui-ci pour plus de précisions sur les vulnérabilités en fonction des algorithmes utilisés, aini que la RFC 8725 mentionnée en début d’article.

Les claims

Outre le fait de pouvoir envoyer des données customs, les claims fournissent des moyens de contrôle intéressants et pertinents :

  • qui a émis le JWT
  • quel est l’identifiant du JWT
  • quel est l’objet du JWT
  • quel système doit utiliser le JWT
  • Combien de temps le JWT est valide

Le problème c’est que ces claims sont optionnels, et rien n’empêche leur non utilisation dans le design d’implémentation, ce qui constitue un risque.

Mais dans quel cas les JWT sont adaptés ?

Je pense avoir recensé une grande partie des raisons qui font que utiliser JWT pour gérer les sessions utilisateurs, cependant, ils ne sont pas dénués d’interêts, en particulier pour les cas d’usages qui nécessitent un token d’authentification à usage unique.

Par exemple, un client doit accéder à une ressource présente dans un object storage (B), votre serveur applicatif (A) peut générer un JWT ayant à fortiori une durée de vie courte, avec un scope limité pour permettre au client de récupérer cette ressource. Le scope limité sera ici d’accéder à une ressource précise sur un domaine précis.

De par leur définition stateless, et la facilité à y intégrer des périmètres limités, les JWT sont aussi intéressants sur les échanges serveur-serveur, ou dans les échanges entre microservices.

Conclusion

En écrivant cet article, j’ai voulu vous faire gagner du temps, en vous permettant d’aller au-delà des fameux articles “todo-app with JWT” que l’on trouve par dizaine sur dev.to.

Non, JWT n’est pas adapté nativement à gérer des sessions utilisateurs, il demande une ingénierie supplémentaire qui le rapproche des systèmes de gestion de session utilisateur existants. D’ailleurs, si on regarde le systèmes d’authentification implémentés dans les frameworks les plus courants :

  • symfony: cookie de session
  • laravel: cookie de session
  • django: cookie de session

Je voulais introduire du code en Golang pour appuyer l’article, mais je pense que ça aurait pu perdre les personnes qui ne connaissent pas le langage, donc à venir, l’écriture d’un article sur une implémentation d’un système d’authentification en Go !

Si vous avez des remarques sur le contenu écrit plus haut, n’hésitez pas à m’en faire part via mail à matthieu [arobase] coddity.com

POST SCRIPTUM : Pour la déconne, j’ai demandé à ChatGPT de me donner les avantages de JWT. Maintenant, vous êtes assez solides sur vos appuis pour constater que les articles “todo-app with JWT” font partie des données d’entrainement du modèle.

Dis ChatGPT, pourquoi c'est bien JWT ?