JWT (JSON Web Token)
Principe
JWT est un jeton permettant d’échanger des informations de manière sécurisée (RFC 7519).
Un Json Web Token est un standard définissant un moyen sûr et compact de transmettre des informations entre plusieurs tiers.
Il est sûr pour les raisons suivantes :
- Les tokens sont signés pour assurer l’intégrité des données
- Les signatures emploient des mécanismes de chiffrement (HMAC, RSA, …) permettant de s’assurer de l’origine des tokens.
Composition d’un JWT
Le JWT est composé de 3 parties, chacune contenant des informations différentes :
- header : contient le type de token et le chiffrement utilisé.
- payload : contient des
claims
(les informations à partager entre tiers). Ces informations peuvent être des informations standards (comme la date de création, la date de fin de validité, …) - signature : permet d’attester de la validité du token
Le détail du standard se trouve ici : RFC 7519
La signature permet d’en vérifier la légitimité. JWT est souvent utilisé pour offrir une authentification stateless (sans état). De nombreuses librairies permettent de manipuler ces jetons (tokens) évitant ainsi l’écriture d’un code personnel pouvant donner lieu à des vulnérabilités.
Nous allons voir plus loin qu’avec l’aide du framework Spring Boot et de JWT, nous implémenterons simplement un mécanisme d’authentification stateless. Il faudra aussi aborder la révocation des tokens qui peut engendrer des problèmes de sécurité.
Détail du fonctionnement
L’information est échangée sous la forme d’un jeton signé afin de pouvoir en vérifier la légitimité. JWT est couramment utilisé pour implémenter des mécanismes d’authentification stateless pour des SPA (Single Page Application) ou pour des application mobiles.
Le jeton est compact et peut être inclus dans une URL sans poser de problème.
En pratique
- Le header et le payload sont structurés en JSON
- Les 3 parties sont chacunes encodées en base64url
- Elles sont ensuite concaténées en utilisant des points (”.”).
Utilité du header
Il sert à identifier l’algorithme utilisé pour générer la signature ainsi que le type de token (JWT ou un autre type d’objet).
Exemple de header :
{
"alg": "HS256",
"typ": "JWT"
}
Ci-dessus, le header indique que la signature a été générée en utilisant HMAC-SHA256.
Utilité du Payload
Le payload est la partie du token qui contient les informations que l’on souhaite transmettre. Ces informations sont appelées “claims”. Il est possible d’ajouter au token les claims que l’on souhaite, mais un certain nombre de claims sont déjà prévus dans les spécifications de JWT comme vous avez pu le constater grâce à la section 4.1 du RFC-7519.
Par exemple, sub (pour subject) identifie le sujet du token, iss (pour issuer) va permettre d’identifier l’émetteur du token ou encore exp (pour expiration date) qui indique la date d’expiration du token. Il est fortement conseillé d’assigner une valeur à ce dernier champ afin de limiter la durée de vie du token. Si la date d’expiration est dépassée, le token sera rejeté.
Exemple de payload
{
"sub": "Robin des Bois",
"exp": "1385768104",
"admin": "true"
}
Ci-dessous, vous constatez que l’on a ajouté admin avec la valeur true.
Utilité de la signature
Elle est créée à partir du header, du payload générés et d’un HMAC secret.
La signature est la dernière partie du token. Lorsque celle-ci est invalide, elle engendre un rejet du token !
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Contruction du Token
Une fois ces 3 éléments générés, on peut assembler notre token JWT :
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
En reprenant les exemples précédents, on arrive au résultat suivant :
Header: {
"alg": "HS256",
"typ": "JWT"
}
Payload: {
"sub": "Robin des Bois",
"exp": 1385768104,
"admin": true
}
Voici le token généré avec le secret key laposte-promo5 (pour la signature HMAC) :
Le tout est encodé en base64-URL pour éviter les problèmes dûs aux différentes utilisations d’encodage suivant les systèmes d’exploitation et les pays.
Testez vous-même sur le site de JWT ou celui-ci
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSb2JpbiBkZXMgQm9pcyIsImV4cCI6MTM4NTc2ODEwNCwiYWRtaW4iOnRydWV9.Hixa98JmdhlV2WNGnYW3BiMLHe_GPWfX0njQ8G7yB4Y
soit : Header . Payload . Signature
version décomposée :
- header : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- payload : eyJzdWIiOiJSb2JpbiBkZXMgQm9pcyIsImV4cCI6MTM4NTc2ODEwNCwiYWRtaW4iOnRydWV9
- signature : Hixa98JmdhlV2WNGnYW3BiMLHe_GPWfX0njQ8G7yB4Y
Maintenant que l’on a notre token, utilisons-le pour nous authentifier !
Pourquoi ça fonctionne pour l’authentification
L’authentification à base de JWT fonctionne avec les acteurs suivants :
- Un serveur d’authentification : c’est lui qui crée les JWT (et donc qui les signe).
- Un serveur d’application : c’est lui qui utilise les JWT pour vérifier l’identité des requêteurs. Il faut noter que les serveurs d’application peuvent être plusieurs à utiliser les mêmes JWT pour authentifier des utilisateurs (ex : dans une entreprise, on crée un serveur d’authentification et toutes les applications métiers se servent des JWT qui ont été générés par le serveur d’authentification pour vérifier l’identité des utilisateurs). Il faut aussi noter que lorsqu’on développe une petite application, on peut grouper serveur d’authentification et d’application dans un seul et même serveur.
- Un requêteur : c’est lui qui fait appel au serveur d’application (depuis un navigateur, ou depuis une autre API, …). C’est lui qui fait des appels HTTP à notre serveur en joignant à ses requêtes le JWT qui lui aura précédemment été fourni pour s’authentifier.
Si on regarde la chronologie des actions, on a :
- Le requêteur demande à s’authentifier en envoyant ses identifiants au serveur d’authentification (login + mdp).
- Le serveur d’authentification vérifie que les identifiants de connexion sont valides (le mot de passe doit correspondre au login).
- Le serveur d’authentification génère un JWT avec un header et un payload qu’il signe avec un secret (que lui seul et le(s) serveur(s) d’application connaissent).
- Le requêteur reçoit le JWT qu’il devra renvoyer à chaque requête au serveur d’application.
Cela fonctionne parce que le serveur d’authentification et le serveur d’application sont les seuls capables de vérifier la validité du JWT !
En effet, le JWT est lisible par le requêteur car son contenu est en base64-URL. Cependant, il n’est pas capable de générer la signature du token car il ne connaît pas la clé permettant de la générer. Il doit donc forcément être reconnu par le serveur d’authentification (avec login + mdp) pour pouvoir obtenir un JWT valide.
Plusieurs types de signatures sont possibles pour créer un JWT. Ils ont chacun leurs avantages et leurs inconvénients.
Il existe de nombreuses librairies permettant de créer et manipuler les JWT. Il est déconseillé de manipuler un JWT avec son propre code ! Une liste non exhaustive de librairies est disponible à cette adresse https://jwt.io/libraries?language=Java
Pour utiliser notre token, il faut tout d’abord le créer. Pour cela, il est nécessaire de s’authentifier avec son login et son mot de passe auprès de l’application afin que celle-ci nous renvoie le token. Une fois le token obtenu, on peut faire appel à nos URL sécurisées en envoyant le token avec notre requête.
La méthode la plus courante pour envoyer le token est de l’envoyer à travers l’en-tête HTTP Authorization en tant que **Bearer
voir l’introduction sur le site https://jwt.io/introduction
Authorization: Bearer ‘token’
La mise en pratique
Quelques mots sur la technique de hash
Les fonctions de hachage sont des fonctions mathématiques qui permettent de calculer une empreinte d’une donnée. Ces fonctions génèrent une chaîne de caractère à partir d’une donnée fournie en entrée.
Les caractéristiques des fonctions de hachage sont les suivantes :
- Quelque soit la longueur de la donnée fournie en entrée, l’empreinte en sortie a une taille fixe.
- La fonction doit être à sens unique. Autrement dit, il doit être impossible (ou extrêmement difficile) de retrouver un message à partir de son hash.
- Une petite variation de la donnée d’entrée doit donner une grande variation du hash.
Grâce à ses caractéristiques, le hash peut être utilisé comme signature d’une donnée.
Illustration
adresse du site pour les tests : https://bcrypt-generator.com/
- Là, ça match !
- Ici, ça match pas !
Pour plus de détails, consultez la définition de Wikipedia des fonctions de hachage cryptographique
A propos de la signature HMAC
Avantages
La signature HMAC est très simple à mettre en place. Il suffit au serveur d’authentification de hasher le header et le payload en y ajoutant un secret (une chaîne de caractères).
Inconvénients
Si plusieurs serveurs d’application utilisent le même JWT, alors il faut qu’ils partagent tous le même secret pour être en mesure de vérifier la validité du JWT. Cela augmente les chances de vol de secret et complexifie le changement du secret (à chaque changement, il faut informer tous les serveurs d’application par voie sécurisée).
De même, si le secret est trop simple alors les attaques par force brute pour le deviner peuvent être facile.
A propos de la signature RSA
Avantages
La signature par RSA est plus sûre que la signature HMAC. En effet il est quasiment impossible de tenter des attaques par force brute (pour le moment).
Elle est aussi plus sûre car la mise en oeuvre du RSA permet de distinguer la génération des JWT et leur vérification (grâce à la combinaison clé publique / clé privée). En effet, ce sera le serveur d’authentification qui va générer la paire de clé permettant de chiffrer et déchiffrer la signature.
Pour le comprendre, il faut savoir que les méthodes de chiffrement asymétriques (comme RSA) permettent deux choses : de chiffrer des données ou de signer des données. Ceci dépend du choix de clé qui est fait lors du chiffrement d’un message. Les raisons du fonctionnement de ces méthodes de chiffrement asymétriques sont mathématiques. Il convient juste de retenir ici qui ces méhodes fonctionnent avec deux clés qui sont liées par des propriétés mathématiques : une clé publique qui est disponible pour tout le monde et une clé privée que seul le créateur de la paire de clé doit connaître.
Pour illustrer ces différents cas d’utilisation on parlera toujours d’Alice et de Bob qui souhaitent communiquer ensemble. Dans les deux cas, Alice va générer une paire de clés et va transmettre sa clé publique à Bob.
Cas #1 : Chiffrement - Alice souhaite recevoir un message chiffré de la part de Bob.
Bob va écrire son message, puis le chiffrer avec la clé publique d’Alice. Bob peut envoyer son message sereinement car il est sûr que seule Alice peut le déchiffrer. En effet, seule la clé privée connue d’Alice pourra transformer le message chiffré de Bob en un message lisible pour un humain.
Cas #2 : Signature - Bob souhaite vérifier qu’un message envoyé par Alice vient vraiment d’Alice.
Alice va écrire son message, le hasher puis chiffrer le résultat du hash du message avec sa clé privée. Le résultat du hash chiffré sera la signature qu’Alice ajoutera à la fin de son message. Lorsque Bob va recevoir le message d’Alice, il va lui aussi hasher le message reçu et va déchiffrer la signature d’Alice grâce à la clé publique. Si le résultat du hash correspond au déchiffrement de la signature alors Bob peut être sûr que le message a bien été envoyé par Alice.
Dans le cas de l’utilisation de JWT, nous serons dans le cas #2.
Le serveur d’authentification peut être comparé à Alice :
- il génère la paire de clé
- il publie la clé publique (accessible par tous les serveurs d’application)
- il génère un hash du header + payload
- il chiffre le résultat du hash avec sa clé privée (ce qui donne la signature)
- il ajoute la signature à la fin du token
Le(s) serveur(s) d’application peut être comparé à Bob :
- il récupère la clé publique
- il génère un hash du header + payload
- il déchiffre la signature du token
- il compare le résultat du hash (hash + payload) à la signature déchiffrée
Si le résultat du hash correspond à la signature déchiffrée alors le serveur d’application peut être sûr que l’utilisateur a bien été authentifié par le serveur d’authentification.
Comme la clé publique est distribuée à tous les serveurs d’application et qu’elle n’est pas confidentielle, il n’y a pas de risque de vol. Il est aussi plus facile de changer la paire de clé. Cela permet en plus de séparer les rôles. Ici, seul le serveur d’authentification peut générer les JWT.
Inconvénients
La signature RSA est un peu plus complexe à mettre en place que la signature HMAC.
Il nous reste à voir son application avec Spring Boot…