Sécuriser une API REST (3/3) : gestion du JWT coté client

Sécuriser une API REST (3/3) : gestion du JWT coté client

Après avoir vu la théorie sur la sécurisation d’une API REST et l’implémentation en Node.js, nous allons clore cette série d’articles avec la gestion du JSON Web Token (JWT) coté client et voir les erreurs bien trop souvent commises.

Note : Avant de poursuivre, je tiens à préciser que les exemples de cet article sont fonctionnels, mais sont simplifiés au maximum pour que vous compreniez les concepts clés. Comme pour les autres articles certains raccourcis sont pris (middleware simplifié, gestion des erreurs, accès à la base de données, etc.) pour améliorer la compréhension, je ne me concentre pas sur l’architecture de l’application ce n’est pas le but de l’article. Comme toujours, libre à vous d’adapter en fonction de vos projets.

Connexion à l’API

Lors de la connexion à une API REST, on récupère souvent le JWT et le “refresh token” dans la réponse HTTP, c’est d’ailleurs ce que nous avons fait dans le précédent article :

Vient alors la question du stockage de ceux-ci côté client.

Erreur n°1 : Stockage des tokens dans le “localStorage”

Lorsque l’on souhaite stocker les tokens coté client, on pense naturellement à l’utilisation du localStorage. Le code pour se connecter à notre API et récupérer les tokens pourrait ressembler à ceci :

Il suffit ensuite de stocker les tokens dans le localStorage :

Il est possible de voir le contenu du localStorage dans la console de votre navigateur. Pour Chrome, il suffit de se rendre dans l’onglet “Application” puis “Local Storage”.

Affichage du "localStorage" dans la console du navigateur
Affichage du “localStorage” dans la console du navigateur

Lorsque le client souhaite effectuer une requête sur une route sécurisée de notre API, celui-ci devra transmettre le JWT via l’en-tête HTTP Authorization:

Malheureusement, cette façon de stocker les tokens ouvre la porte à une faille bien connue : la faille XSS.

La faille XSS

XSS ou cross-site scripting est un type de faille permettant d’injecter du contenu dans une page web par exemple du HTML ou du JavaScript. 

Il existe plusieurs types d’attaques XSS, d’ailleurs un article dédié à la sécurité web devrait sortir prochainement dans lequel nous verrons en détail les différents types d’attaques XSS et comment s’en protéger.

Pour notre exemple, nous allons simplement voir l’attaque la plus répandue qui est l’attaque XSS stockée. Ce type d’attaque consiste simplement à un pirate d’envoyer du contenu malicieux à notre API (code JavaScript ou HTML par exemple) qui va le stocker en base de données et le restituer tel quel à notre application web qui elle va l’exécuter.

Le code malicieux peut être simplement :

Affichage un dialogue d'alerte via une faille XSS
Affichage un dialogue d’alerte via une faille XSS

Bon c’est pas jolie, jolie, mais rien de bien méchant, mais cela peut être ce type de code :

Le navigateur exécute ce code et effectue une requête GET sur le serveur du pirate avec les tokens en paramètre de l’URL sans que l’utilisateur s’en aperçoive. Il suffit ensuite au pirate de logger de son côté la requête pour récupérer les tokens :

Bon cette fois-ci c’est beaucoup plus grave ! Ceci n’est qu’un exemple parmi tant qu’autres permettant de voler les tokens depuis le localStorage. Il existe bien entendu des solutions pour prévenir ce genre d’attaques comme nettoyer les données reçues pour les attaques XSS stockées par exemple, mais on en parlera dans un prochain article. Malgré tout stocker les tokens dans le localStorage est une très mauvaise pratique.

Erreur n°2 : Stockage des tokens dans les cookies

Comme nous l’avons vu stocker les tokens dans le localStorage ouvre la porte aux attaques XSS. Une autre solution consiste à stocker ceux-ci dans des cookies. 

Dans le précédent article, notre API envoyait directement au client les tokens dans la réponse HTTP comme nous l’avons vu en introduction. Notre code ressemblait à ceci :

Modifions celui-ci pour envoyer cette fois-ci les tokens dans des cookies :

Dans le cas d’une requête qui concerne le même domaine, le navigateur enregistre automatiquement les cookies. Par contre lorsque l’on effectue des requêtes entre domaines différents, ce qui est fréquent dans le cas d’une API REST, il devient obligatoire d’ajouter la propriété credentials aux options de la méthode fetch et de lui assigner la valeur include:

Si vous utilisez la classe XMLHttpRequest ou la librairie axios à la place de fetch, il suffit de définir l’option withCredentials à true.

Comme pour le localStorage, vous pouvez directement voir les cookies depuis la console de votre navigateur. Pour Chrome, il suffit de se rendre dans l’onglet “Application” puis “Cookies”.

Affichage des cookies dans la console du navigateur
Affichage des cookies dans la console du navigateur

Il faut maintenant, dès lors que l’on effectue une requête sur une route sécurisée, envoyer les cookies à notre API. Comme pour l’enregistrement des cookies, si la requête concerne le même domaine, ceux-ci sont envoyés automatiquement. Dans le cas contraire, il faut ajouter la propriété credentials aux options de la méthode fetch et de lui assigner la valeur include (ou withCredentials à true pour la classe XMLHttpRequest ou la librairie axios) :

Côté serveur, il faut par contre modifier le middleware d’authentification puisque celui-ci récupérait le JWT depuis l’en-tête HTTP Authorization, il faut maintenant le récupérer depuis les cookies:

Bon normalement là tout est bon, on doit maintenant être protégé contre les attaques XSS !

Eh bien ! pas du tout. En fait il est tout à fait possible de récupérer les cookies en JavaScript :

On peut donc utiliser la même faille XSS que pour localStorage :

Bon vous vous en doutez il existe une solution pour protéger nos cookies d’une attaque XSS.

Il existe deux options permettant de sécuriser nos cookies :

  • HttpOnly : Cette option permet d’interdire l’utilisation du cookie côté client, il est donc impossible de récupérer celui-ci via l’instruction vue précédemment, on est donc protégé des failles XSS;
  • secure : Cette option permet d’envoyer le cookie uniquement dans via le protocole HTTPS.

Modifions donc notre code coté serveur:

Cette fois-ci c’est tout bon… enfin pas vraiment. On est bien protégé des attaques XSS mais on est vulnérable à un autre type d’attaque : les attaques CSRF.

La faille CSRF

Cross Site Request Forgery ou CSRF est un type de faille qui consiste simplement à faire exécuter à une victime une requête HTTP à son insu.

On va prendre un exemple très simple pour mieux comprendre  :

  • Un utilisateur est authentifié sur notre API. Le cookie contenant le JWT est alors stocké dans son navigateur;
  • Une personne mal intentionnée incite l’utilisateur à visiter la page d’un site contenant du code malveillant exécutant une requête vers notre API;
  • La requête est envoyée sur notre API avec le cookie contenant le JWT de l’utilisateur qui est ajouté automatiquement par le navigateur;
  • Notre API effectue l’opération correspondant à la requête à l’insu de l’utilisateur.
Exemple d'attaque CSRF
Exemple d’attaque CSRF

Il y a plusieurs manières de procéder à une attaque CSRF,  par exemple cela peut être un formulaire qui est envoyé une fois la page chargée :

Ce code effectue une requête POST sur notre API et ajoute un article dont le contenu est “You have been hacked !”. Imaginez donc ce qu’il est possible de faire avec ce type d’attaque.

Concernant le refresh token, le stockage dans un cookie ne pose pas de problème. Celui-ci est uniquement utilisé pour générer un nouveau JWT. Donc même dans le cas d’une attaque CSRF, l’attaquant n’a pas moyen de récupérer le nouveau JWT ou d’effectuer d’autres actions.

Ce qu’il nous faut c’est donc un moyen de protéger le JWT contre les attaques XSS et CSRF.

FUUUUSION… HA !

Nous avons vu que le stockage des tokens dans le localStorage nous exposé aux attaques XSS et que le stockage de ceux-ci dans des cookies nous exposé cette fois-ci aux attaques CSRF.

Du coup, quelle solution pour nous prémunir de ces attaques ? Et bien nous allons fusionner les deux solutions. En effet, le localStorage n’est pas sensible aux attaques CSRF et inversement les cookies ne sont pas sensibles aux attaques XSS (via la propriété HttpOnly).

L’idée est de diviser nos informations d’authentification en deux parties et de stocker celles-ci à la fois dans le localStorage et dans un cookie. On envoie ensuite ses deux informations au serveur, l’une via le cookie et l’autre via un en-tête HTTP.

Concrètement, voici comment cela se déroule :

  • Lors de l’authentification, notre API va générer un token unique que l’on appelle token CSRF et qui sera stocké dans le payload du JWT;
  • Notre API envoie le JWT dans un cookie et le token CSRF dans le corps de la réponse HTTP;
  • Le navigateur va s’occuper de stocker le cookie et nous allons stocker le token CSRF dans le localStorage;
  • Lors d’une requête sur notre API, celle-ci doit comporter à la fois le cookie contenant le JWT et un en-tête HTTP x-xsrf-token contenant le token CSRF;
  • Notre API décode le JWT et vérifie sa validité;
  • Notre API vérifie que le token CSRF contenu dans l’en-tête HTTP correspond à celui du payload du JWT, si c’est le cas l’utilisateur est authentifié.
Authentification à l'aide du JWT et du token CSRF
Authentification à l’aide du JWT et du token CSRF

Voyons voir maintenant ce que cela donne au niveau du code.

Coté serveur, commençons par modifier notre route de connexion  :

Coté client, la fonction de connexion reste la même, il nous faut juste stocker le token CSRF dans le localStorage

Il nous faut maintenant gérer l’authentification via le middleware coté serveur :

Pour terminer, modifions notre code coté client pour effectuer une requête sur notre API :

Pour finir…

Pour clôturer cette série d’articles, nous avons vu quelques bonnes pratiques concernant la sécurisation d’une API REST, leurs implémentations en Node.js et que le simple stockage des tokens coté client pouvait exposer nos applications à plusieurs failles de sécurité.

Nous n’avons pas encore terminé côté sécurité, puisque nous verrons dans un prochain article comment nous prémunir entre autres des failles XSS et CSRF de manière plus générale et quelles sont les autres bonnes pratiques en termes de sécurité lorsque nous développons une application web.

Développeur back (nodejs & php), je fais aussi du front (react). Je partage mes connaissances et ma passion au travers de mes articles. N'hésitez pas à me suivre sur Twitter.

2 commentaires

  1. Merci pour cette série d’articles très instructifs.

    J’ai cependant un doute sur la robustesse de la solution utilisant “CSRF token + Cookie HttpOnly” face aux attaques XSS. Si un attaquant parvient à injecter du JS, il n’a certes pas directement accès au JWT mais a accès au token CSRF. Comme les cookies sont automatiquement passés, il lui suffit alors de récupérer le csrf-token et de forger une requête HTTP avec fetch(). Ainsi, le token CSRF **et** le JWT sont passés et valides.

    Qu’en dites-vous ?

    Cordialement,

    1. Je m’attendais à cette question bien vue 😉 ! Il est effectivement possible de procéder à une attaque XSS et d’injecter du code effectuant une requête HTTP via fetch. C’est pourquoi il faut à tout prix se prémunir des failles XSS, comme je l’ai dit je compte faire un article qui s’intéresse plus en détail aux différents soucis de sécurité dont notamment les failles XSS (OWASP Top Ten). Mais même en étant protégé contre les failles XSS, on n’est pas à l’abri d’utiliser une librairie vérolée. Bon après vu que la méthode présentée dans l’article empêche de voler le JWT, la librairie vérolée ne pourra effectuer que des requêtes sur notre API en utilisant le token CSRF + le JWT, autrement dit elle doit être conçue uniquement pour effectuer une attaque sur notre API, donc peu de chance que ça arrive.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.