En toute sécurité avec JWT (1re partie)

Actualité ekino -

Premier volet d'une série d'articles sur les aspects sécurité des Json Web Token (JWT) : les mécanismes de sécurité prévus par les spécifications JWT

Article paru le :
En toute sécurité avec JWT : première partie
Article paru le :

Json Web Token (JWT) est un standard qui connaît aujourd’hui une grande popularité dans la communauté des développeurs informatiques. Il décrit une façon d’échanger des informations entre plusieurs tiers, avec la souplesse et la légèreté du format JSON et tout cela de façon sécurisée.

Dans un précédent article (https://www.ekino.com/introduction-aux-json-web-tokens/), nous avions introduit le concept porté par les Json Web Token et détaillé la façon dont ils sont structurés pour répondre à la problématique des sessions côté client.

Dans cet article, nous allons davantage nous plonger dans le monde de la sécurité informatique. En première partie, nous approfondirons les principes de sécurité sur lesquels sont bâtis les Json Web Token. Dans la seconde, nous effectuerons un état des lieux des failles connues, avec pour chacune quelques bonnes pratiques pour s’en protéger.

JWT en bref

JWT est un concept technologique qui a été standardisé, implémenté dans de très nombreux langages de programmation (des librairies sont disponibles pour ASP, PHP, Java, NodeJS, Ruby, Python, …). C’est d’ailleurs l’un de ses avantages : faire communiquer entre eux des applicatifs écrits dans des langages différents.

Historiquement, il a été élaboré par une communauté constituée en 2011, baptisée “JOSE” (pour « JSON Object Signing and Encryption Group »). Les travaux de ce groupe ont abouti à la production de plusieurs RFCs complémentaires, que l’on peut trouver sur le site de l’IETF (Internet Engineering Task Force) :

  • JWT : Json Web Token (RFC 7519)
  • JWS : Json Web Signature (RFC 7515) qui s’intéresse particulièrement aux jetons signés
  • JWE : Json Web Encryption (RFC 7516) qui porte sur les jetons chiffrés
  • JWK : Json Web Key (RFC 7517) qui décrit un standard au format JSON pour échanger les clés
  • JWA : Json Web Algorithms (RFC 7518) qui décrit les algorithmes recommandés pour l’utilisation des Json Web Tokens

 

JWT dans la pratique

Sessions côté client

Une application très répandue de ce concept est la mise en place de sessions côté client (aussi appelées « Stateless Sessions »).

D’un point de vue technique, une session utilisateur contient un ensemble d’informations sur un utilisateur authentifié que l’on conserve temporairement en mémoire. Dans des infrastructures complexes, impliquant de la répartition de charges notamment, il serait plus simple de déporter le stockage de la session côté client, de façon à soulager le serveur de cette gestion (cache partagé entre les instances, affinité de sessions, …) et avoir des applicatifs « Stateless » donc scalables. Le format JSON d’ailleurs se prête bien à ce type de stockage.

Pour que le serveur puisse faire confiance à ces informations, comme si elles provenaient de lui-même, il lui suffit de les signer et d’envoyer cette signature avec les informations. Quand elles lui seront retournées, il sera alors en mesure de vérifier l’authenticité de ces informations en comparant la signature reçue à la signature calculée.

Fédération d’identités

Une seconde application majeure de ce concept est la mise en œuvre de systèmes de fédération d’identités. Cela concerne les infrastructures où plusieurs tiers, parfois sans relation entre eux, partagent des services d’authentification et d’autorisation. On pense à des systèmes implémentant le principe OAuth2 ou à une architecture construite en micro-services.

Le jeton JWT est un candidat idéal pour répondre à cette problématique puisque :

  • il encapsule dans son contenu toutes les informations nécessaires
  • il dispose d’une date de péremption et d’un identifiant unique (possibilité de le révoquer par liste noire)
  • on ne peut altérer son contenu sans le rendre invalide (sauf failles que l’on verra dans la seconde partie de cet article)
  • n’importe quelle application du SI peut en vérifier l’authenticité (à condition qu’elle possède la clé)

 

Quelles protections embarque nativement le jeton JWT ?

Voyons maintenant par quels procédés les Json Web Token prévoient de sécuriser le contenu véhiculé.

Il faut distinguer 2 cas de figure, décrits par 2 RFCs différentes :

  • La signature de jeton : Json Web Signature (JWS)
  • Le chiffrement de jeton : Json Web Encryption (JWE)

 

JWS : signer les informations échangées

C’est le scénario le plus courant : les informations du jeton circulent en clair. Par conséquent, on ne mettra pas des données confidentielles dans le contenu du jeton JWT.

L’essentiel est de pouvoir s’assurer que ce contenu n’a pas changé depuis qu’il a été signé par le serveur. Ce qui veut dire que toutes les applications clientes qui vont exploiter les données du jeton devront s’assurer de son authenticité, en vérifiant sa signature (3ème partie du jeton JWT).

Rappel sur la structure d’un jeton JWS

Pour rappel, un jeton JWT signé est simplement constitué de trois parties, séparées par un point et apparaissant dans cet ordre :

JWS structure en 3 parties

Partie 1 – HEADER : l’entête

Au format JSON, l’entête contient des méta-données nécessaires pour manipuler le jeton JWT, en particulier l’algorithme utilisé pour générer la partie signature (le nom de l’attribut est “alg”).

Partie 2 – PAYLOAD : le contenu

Le contenu est un objet JSON qui contient les informations à échanger entre les applications.

Ces informations sont stockées dans des attributs ou “claims”. La spécification a défini un certain nombre de “claims” réservés (“jti” pour “JWT ID”, “sub” pour “subject”, “exp” pour “expiration”, …) mais permet d’en déclarer de nouveaux pour les besoins de l’application.

Partie 3 – SIGNATURE : la signature

La signature est calculée en envoyant les 2 premières parties au format JSON à un algorithme de “hashage” (celui qui est précisé dans la partie entête). Cet algorithme utilise une clé secrète et produit un résultat au format binaire.

Pour pouvoir être véhiculé simplement, chacune des parties du jeton JWT est encodée en “Base64Url” (encodage en Base64 “URL safe”, I.E qui peut transiter dans des URL). C’est ce que l’on désigne par format compact.

JWS encodage Base64 et hashage HMAC et SHA-256

Un exemple pour illustrer

Prenons le cas d’un jeton signé avec l’algorithme HMAC + SHA-256, contenant 2 “claims” métiers :

  • customUserName : le nom de l’utilisateur connecté
  • customIsAdmin : vaut « true » si l’utilisateur a le profil « administrateur »

 
Les “claims” réservées étant :

  • iss : pour « issuer ». Le jeton est délivrée par l’application « myApplication ».
  • sub : pour « subject ». Le jeton contient des informations sur l’utilisateur « user123 ».
  • exp : pour « expiration ». Le timestamp à partir duquel le jeton expire.
  • iat : pour « issued at ». Le timestamp à partir duquel le jeton a été émis.
  • jti : pour « JWT ID ». Un identifiant unique pour qualifier ce jeton (pour pouvoir le blacklister éventuellement).

 
Et une clé secrète pour signer le jeton : “s3cr3t”.

HEADER : algorithme & type de jeton
{
  « alg »: « HS256 »
}
PAYLOAD : données
{
  « iss »: « myApplication »,
  « sub »: « user123 »,
  « exp »: « 1489419760 »,
  « iat »: « 1489398160 »,
  « jti »: « 7caed540-f2c6-4921-9fcd-e405f5460d81 »,
  « customUserName »: « John Doe »,
  « customIsAdmin »: false
}
JWT TOKEN : version compact du jeton (sauts de ligne ajoutés pour la lisibilité)
eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJteUFwcGxpY2F0aW9uIiwic3ViIjoidXNlcjE
yMyIsImV4cCI6IjE0ODk0MTk3NjAiLCJpYXQiOiIxND
g5Mzk4MTYwIiwianRpIjoiN2NhZWQ1NDAtZjJjNi00O
TIxLTlmY2QtZTQwNWY1NDYwZDgxIiwiY3VzdG9tVX
Nlck5hbWUiOiJKb2huIERvZSIsImN1c3RvbUlzQWRtaW
4iOmZhbHNlfQ
.
rHeSujlwY4k7-E2zgSUj6505qkNbLRBOWgHpDctpGcI

 

Si une personne modifie le contenu de ce jeton, i.e la partie « header » ou la partie « payload », la signature change radicalement. Et sans la connaissance du secret, cette personne est incapable de régénérer une signature valide.

Par exemple, si une personne modifiait la claim « customIsAdmin » et la valorisait à « true », la signature actuelle ne serait plus correcte. Car en re-calculant côté serveur cette signature à partir de la clé secrète « s3cr3t » et l’algorithme HMAC + SHA-256, on devrait avoir pour ces données la valeur : 6bQ5NgF_h1uD-KNpipyKmKYo0j2oeSyqU3FrWwNXbjA (après encodage en Base64).

C’est de cette manière que le système peut faire confiance au jeton JWT et s’assurer qu’il n’a pas été altéré par une personne extérieure au SI.

 

Deux modes d’utilisation de JWS

L’exemple ci-dessus nous a présenté le premier mode d’utilisation de JWS : le « secret partagé ». Selon ce mode, les applicatifs qui possèdent le secret peuvent à la fois fabriquer, signer des jetons et en vérifier l’authenticité.

Pour ce mode, la spécification JWS fait exclusivement mention d’algorithmes HMAC (« Hash-based Message Authentication Codes ») dits symétriques :

  • HMAC + SHA-256, qui doit être nécessairement supporté par les implémentations
  • HMAC + SHA-384 : variation de l’algorithme précédent avec la fonction de hashage SHA-384
  • HMAC + SHA-512 : variation de l’algorithme précédent avec la fonction de hashage SHA-512

 
Le choix de l’algorithme dépend du niveau de sécurité désiré.

Quelques exemples d’architectures logicielles avec utilisation d’un secret partagé :

  • Une application Web unique, dupliquée en plusieurs instances sur lesquelles la charge est répartie via un « load balancer ». Après une authentification réussie de l’utilisateur, l’application lui renvoie un jeton JWT signé. A chaque requête HTTP du navigateur, le jeton JWT est renvoyé au serveur : n’importe quelle instance peut le vérifier car elle partage le même secret.

 
JWT architecture logicielle multi-instances

  • Une architecture à base de micro-services : un service se charge de l’authentification et produit le jeton. Par la suite, pour chaque requête HTTP, plusieurs services sont impliqués dans la réponse et ont besoin du jeton pour connaître les droits de l’utilisateur par exemple. Pour disposer du même secret, ils déclarent comme dépendance commune une librairie spécifique chargée de la manipulation des jetons JWT. Celle-ci porte dans son code le secret partagé.

 
JWT architecture logicielle micro-services avec secret partagé

Il existe un autre mode d’utilisation utile dans d’autres cas : « clé publique/clé privée » . Dans ce mode, l’application détentrice de la clé privée est la seule qui peut signer les messages et donc les produire. Grâce à la clé publique, les applications clientes peuvent en vérifier la validité.

C’est un mode plus sécurisé que le précédent pour 2 raisons :

  • le secret n’est conservé qu’à un seul endroit, que l’on protégera davantage
  • une seule application seulement peut produire les jetons, ce qui distribue mieux les responsabilités

 

Voici ce que devient mon jeton JWT pour un algorithme de la famille RSA (la partie « payload » reste inchangée) :

HEADER : algorithme & type de jeton
{
  « alg »: « RS256 »,
  « jwk »: {
    « kty »: « RSA »,
    « e »: « AQAB »,
    « n »: « pbhS_PcYXO3vbNGKXxWzeXP2TJKO5RtrzPr6Zol
O36KlOcxoejr8Imp-i2QXEjnzMovYfX3jPihb2_Fa7TDFRzV
woidqWVexXs0UWxXt2NFZ9sOdUWaqAk9wJh3cHDQZJFN
5H2-eQuj5FszOOjXO5VQqxkZgjcQRlEIylUmZzqM »

  }
}
PAYLOAD : données
{
  « iss »: « myApplication »,
  « sub »: « user123 »,
  « exp »: « 1489419760 »,
  « iat »: « 1489398160 »,
  « jti »: « 7caed540-f2c6-4921-9fcd-e405f5460d81 »,
  « customUserName »: « John Doe »,
  « customIsAdmin »: false
}
JWT TOKEN : version compact du jeton (sauts de ligne ajoutés pour la lisibilité)
eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFB
QiIsIm4iOiJwYmhTX1BjWVhPM3ZiTkdLWHhXemVYUDJUSktP
NVJ0cnpQcjZab2xPMzZLbE9jeG9lanI4SW1wLWkyUVhFam56T
W92WWZYM2pQaWhiMl9GYTdUREZSelZ3b2lkcVdWZXhYcz
BVV3hYdDJORlo5c09kVVdhcUFrOXdKaDNjSERRWkpGTjVIM
i1lUXVqNUZzek9PalhPNVZRcXhrWmdqY1FSbEVJeWxVbVp6c
U0ifX0
.
eyJzdWIiOiJ1c2VyMTIzIiwiY3VzdG9tVXNlck5hbWUiOiJKb2hu
IERvZSIsImN1c3RvbUlzQWRtaW4iOmZhbHNlLCJpc3MiOiJteU
FwcGxpY2F0aW9uIiwiZXhwIjoxNDg5NTA1Njk3LCJpYXQiOjE
0ODk1MDU2NzYsImp0aSI6IjdjYWVkNTQwLWYyYzYtNDkyMS
05ZmNkLWU0MDVmNTQ2MGQ4MSJ9
.
He3NOOEV-a7O78dB9bZJOrwabLK9SpLuA9iLU69V__vuMSunK
HZdiAUAbseiNn8kSiuS7_GaOrHSFWd0Jz83_x3JC7LYWk8sfLKG
-JhGW952twa5bA0Kzg2OWBDEiPVuFGpC8m4xUtVzkRGksC-_c
N8abNUhDNPqEomLREWQugI

 

On remarquera que la partie « header » fait maintenant mention de l’algorithme « RSASSA + SHA-256 », complété par un nouveau champ « jwk ». Ce champ est un objet JSON qui décrit la clé en accord avec la spécification JWK (Json Web Key). Plus particulièrement, le champ « kty » décrit le type de clé (« RSA » dans notre exemple) et les champs « e » et « n » sont des composants de la clé publique, encodés en « Base64Url ».

Pratiquement, ce mode d’utilisation de JWS peut convenir à un architecture à base de micro-services, comprenant un service chargé de l’authentification de l’utilisateur et signant le jeton à l’aide de la clé privée, et d’autres services chargés de consommer le jeton et d’en vérifier l’authenticité à partir de la clé publique.

JWT architecture logicielle micro-services avec clés publique et privée

Pour ce mode, la spécification JWS fait mention des algorithmes asymétriques suivants :

  • RSASSA + SHA-256
  • RSASSA + SHA-384 : variation de l’algorithme précédent avec la fonction de hashage SHA-384
  • RSASSA + SHA-512 : variation de l’algorithme précédent avec la fonction de hashage SHA-512
  • ECDSA + P-256 et SHA-256 : une alternative à RSASSA, appelée « Elliptic Curve », générant des clés plus courtes pour le même niveau de sécurité.
  • ECDSA + P-384 et SHA-384 : variation de l’algorithme précédent avec la fonction de hashage SHA-384
  • ECDSA + P-512 et SHA-512 : variation de l’algorithme précédent avec la fonction de hashage SHA-512

 

JWE : chiffrer les informations échangées

Dans certains cas plus rares, il est parfois nécessaire de chiffrer les informations du jeton pour les rendre illisibles à toute personne extérieure à l’échange. En particulier s’il contient des informations sensibles qu’il faut absolument protéger et qui transitent en paramètres d’URL ou dans les entêtes HTTP.

La spécification JWE s’est intéressée particulièrement à ce type de jeton.

Structure d’un jeton JWE

D’un point de vue structurel, le jeton se décompose en 5 parties, séparées par des points et apparaissant dans cet ordre :

JWE structure en 5 parties

Comme pour le jeton JWS, chacune de ces parties est encodée en “Base64Url”.

Partie 1 – HEADER : l’entête

C’est l’équivalent de l’entête JWS : son objectif est de donner les informations nécessaires pour manipuler le jeton JWT.

De nouveaux champs ont cependant été introduits :

  • “enc” : l’algorithme utilisé pour chiffrer le contenu à partir de la clé de chiffrement du contenu (appelée “CEK” pour “Content Encryption Key”). A distinguer du champ “alg” qui pour JWE est l’algorithme qui a servi à chiffrer la clé CEK. Nous reviendrons sur ce point par la suite.
  • “zip” : l’algorithme de compression qui est appliqué au contenu avant chiffrement. Cet entête est optionnel. Une valeur courante est “DEF” pour désigner l’algorithme “Deflate”.

 
Certains champs décrits dans JWS changent de signification, comme “jwk” utilisé dans le mode “clé publique / clé privée”. Dans le cadre de JWE, il se rapporte à la clé publique ayant chiffré la clé “CEK”.

Partie 2 – KEY : la clé de chiffrement du contenu, elle-même chiffrée (optionnelle)

Dans le cas le plus simple, la clé ayant servi à chiffrer le contenu est un secret partagé. Aussi, il n’est pas nécessaire de la faire transiter entre les différentes applications. Cette deuxième partie est alors laissée à vide.

La spécification a néanmoins prévu d’autres possibilités, qui nécessitent un chiffrement en 2 étapes :

  • Chiffrement du contenu à partir d’une clé symétrique générée aléatoirement
  • Chiffrement de cette clé symétrique à partir d’une autre clé (symétrique OU asymétrique selon le mode) et transmission du résultat dans la deuxième partie du jeton JWT

 
Concrètement, dans le mode “clé publique / clé privée”, la clé de chiffrement du contenu (CEK) est une clé symétrique générée aléatoirement. Cette clé est ensuite elle-même chiffrée au moyen de la clé publique. C’est une optimisation bien connue, que l’on retrouve par exemple dans le protocole SSL/TLS.

Dans tous les cas, le contenu est toujours chiffré avec un algorithme symétrique.

Partie 3 – IV : le vecteur d’initialisation (optionnel)

Il s’agit d’une chaîne de caractère générée aléatoirement et de taille fixe, que certains algorithmes de chiffrement utilisent pour chiffrer le contenu bloc par bloc (“AES CBC” par exemple). Cette partie peut être laissée à vide si le vecteur d’initialisation n’est pas requis.

Partie 4 – PAYLOAD : le contenu chiffré

Cette partie contient le contenu que l’on a chiffré à partir de la clé CEK décrite ci-dessus. On peut très bien considérer comme contenu un objet JSON que l’on aurait converti en chaîne de caractère. Voir même chiffrer un jeton JWT signé (on parle alors de “nested token”).

Partie 5 – TAG : l’empreinte de chiffrement (optionnelle)

Nécessaire pour certains algorithmes de chiffrement, cette empreinte permet de s’assurer de l’intégrité du texte chiffré (en prévention des attaques « padding Oracle » par exemple). D’autres algorithmes intègrent déjà ce contrôle dans leur mécanisme et pour eux cette partie peut être laissée à vide.

2 modes d’utilisation de JWE

Prenons un exemple de jeton JWE contenant comme donnée sensible un numéro de carte bancaire. Le “payload” de notre jeton est au format JSON, ce qui n’est pas obligatoire dans la spécification.

HEADER : algorithmes
{
  « enc »: « A128CBC-HS256 »,
  « alg »: « dir »
}
KEY : clé CEK chiffrée
(vide)
IV : vecteur d’initialisation (128 bits)
FYGxWdb2GHaBGcTvJSOEvA
PAYLOAD : données chiffrées (ci-dessous avant chiffrement)
{
  « customCreditCard »: « 4321 1234 5678 9123 »
}
TAG : empreinte du contenu chiffré (pour contrôle)
HySG2ci9KQRx63JunNZXcQ
JWT TOKEN : version compact du jeton (sauts de ligne ajoutés pour la lisibilité)
eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0.
.
FYGxWdb2GHaBGcTvJSOEvA.
WIwZ7JgpJ8qPSRe3Yhe7GPxeZxKcg1diLI8OofFNGcfkWrAKVFC6jrG3TPW-4diy.
HySG2ci9KQRx63JunNZXcQ

 

L’entête “alg” a la valeur “dir”, ce qui signifie que la clé partagée a servi directement à chiffrer le contenu. Par conséquent, la deuxième partie est laissée à vide.

L’algorithme de chiffrement est le couple “AES-128 CBC + HMAC SHA 256”. Le premier algorithme (“AES-128 CBC”) se charge de rendre illisible le contenu. Le second (“HMAC SHA 256”) est utilisé pour fabriquer la 5ème partie, c’est à dire celle qui permet le contrôle d’intégrité du contenu après déchiffrement.

Cela nous a fait découvrir le premier mode d’utilisation du JWE : “secret partagé”. Toutes les applications qui possèdent le secret peuvent chiffrer / déchiffrer le jeton et vérifier son intégrité. On se retrouve dans un cas d’utilisation analogue au secret partagé de JWS, le chiffrement en plus.

Par la même occasion, l’exemple ci-dessus a illustré l’un des 5 modes de gestion de la clé qui chiffre le contenu : le chiffrement direct. Le secret partagé a chiffré directement le contenu. Il sert aussi à le déchiffrer.

D’autres modes de gestion de la clé sont disponibles pour le fonctionnement en secret partagé. En particulier, le mode “key wrapping” procède en 2 étapes : on chiffre le contenu avec une clé aléatoirement générée puis cette clé est elle-même chiffrée avec le secret partagé et transmise à l’intérieur du jeton.

L’intérêt de ce procédé se comprend dans le cas d’un stockage volumineux de données chiffrées. S’il faut régulièrement changer le secret partagé, il est plus simple de ne re-chiffrer que la clé CEK plutôt que de déchiffrer le contenu avec l’ancienne clé et le chiffrer à nouveau avec la nouvelle clé.

Il existe évidemment un autre mode d’utilisation de JWE, basé sur le paradigme “clé publique/clé privée”. Cependant, ce mode est très différent de celui décrit dans la spécification JWS. Seule l’application détentrice de la clé privée peut déchiffrer le jeton. Les autres applications se servent de la clé publique pour chiffrer le jeton et par ce moyen introduire de nouvelles données dans l’échange. Cela ouvre la porte à de nouveaux cas d’usage que nous n’aborderons pas ici.

Conclusion

Dans ce premier volet, nous avons commencé notre étude des Json Web Token à travers le prisme de la sécurité informatique. Nous avons considéré les éléments de sécurité que le standard prévoit dans ses spécifications. JWS décrit comment signer un jeton. Cette signature est la condition sinequanone pour pouvoir faire confiance aux informations qu’il véhicule, à condition de posséder la clé (symétrique ou asymétrique selon le mode). Plus rarement mis en oeuvre, JWE décrit comment chiffrer un jeton pour assurer la confidentialité du contenu, lui même pouvant être un jeton signé.

Dans le prochain volet de cet article, nous nous intéresserons tout particulièrement aux vulnérabilités connues des jetons JWT. Et aux moyens de nous protéger des failles de sécurité :

En toute sécurité avec JWT (2de partie)

 

Références