Authentification d’API via JWT et les Cookies
Bienvenue en 2016, nous évoluons aujourd’hui dans un monde où tout n’est qu’API, le besoin d’interopérabilité entre des applications clientes toujours plus nombreuses et vos services a vu apparaître le développement des « Web Services », protocole XML-RPC puis SOAP et nous voilà dans une époque où l’architecture REST (acronyme de Representational State Transfer) est reine. Vous entendez surement de plus en plus parler d’architecture micro-services, le buzz word du moment, jusqu’à la prochaine tendance.
Pour rappel pour ceux qui n’ont pas effectué leur homework ou qui ne savent pas taper REST dans leur moteur de recherche préféré QWANT(google is not your friend :). Rest est une architecture stateless (le serveur ne conserve pas d’état d’objets ou de sessions) qui repose sur l’utilisation du protocole HTTP :
- Les URI permettent d’identifier les ressources.
- Ex : http://monapi.org/users
- Les verbes HTTP permettent d’identifier les opérations à effectuer sur les ressources
- GET sur le endpoint ci-dessus retourne l’ensemble des USERS
- POST sur le endpoint ci-dessus créé un USER
- Les réponses HTTP pour identifier la réponse
- On utilise les codes d’erreur classiques HTTP (200: OK, 401: NOT FOUND, etc)
- On envoie la représentation de la ressource dans le corps de la réponse
- Header HTTP pour l’authentification
Ce qui nous amène sans transition au sujet de ce billet: authentifier son API !
Nous avons choisi de parler de l’authentification de votre API car :
- La sécurité sur le web c’est extrêmement important
- On trouve peu d’information dans la langue de Molière sur la toile à ce sujet
- L’information partagée est principalement orientée authentification via JWT, mais l’utilisation de JWT seule n’offre pas un niveau de sécurité suffisant dans la plupart des cas.
Nous vous proposons donc de découvrir avec nous, et dans votre langue préférée, l’implémentation pour votre API d’une « double » sécurité pour l’authentification! Les exemples que nous fournissons ici sont basées sur un développement full stack js (Angular en front, node.js en back).
Démarrons de ce pas avec un introduction rapide à JWT.
JSON Web Tokens (JWT)
JWT pour Json Web Token donc, est aujourd’hui la solution la plus utilisée pour une authentification d’API. Pour faire simple, un JWT est un objet Json qui est encodé par un serveur à l’aide d’une clé privée. Dans notre cas, nous utilisons la librairie node.js jsonwebtoken.
var jwt = require('jsonwebtoken');
Ce qui nous permet de créer un token avec la méthode sign() et en passant en paramètre l’objet que nous souhaitons tokeniser, ici l’objet « user » qui est l’utilisateur correspondant aux identifiants renseignés lors de l’appel au endpoint que vous utilisez pour la connexion (ex: /login ou /connexion).
var token = jwt.sign(user, config.secret, { expiresIn: 604800 //=1week //6000000 //'2 days' // expires in 2 days });
Le token ainsi créé est envoyé au client lors d’un login avec succès et se présente sous cette forme.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJmb3htYWNiZWFsQGhvdG1haWwuY29tIiwiZmlyc3RuYW1lIjoiTmljb2xhcyIsImxhc3RuYW1lIjoiRHV2YWwiLCJhY3RpdmUiOnRydWUsInByb2ZpbGVzIjpudWxsLCJidWlsZGluZ3MiOm51bGwsInhzcmZUb2tlbiI6IlhnQnR2ZFpGX1BXdEViaC1DZW53cVIyTiIsImlhdCI6MTQ3NDg4NTI0NiwiZXhwIjoxNDc1NDkwMDQ2fQ.pPTmz3ho5olARHYGn17xpbfBVrQW6_ezxNzOpVhjWLU
- Le header, encodé en base 64, contenant le type d’algorithme utilisé pour hasher le contenu
- La Payload, encodée en base 64, le contenu des informations à échanger entre le client et le serveur, en général les informations liées au profil utilisateur mais aussi un ensemble de « claims » jwt : Qui est la personne (sub, pour subject en anglais), ses accès (scope), l’expiration du token (exp) et qui est à l’origine de ce token (iss, pour issuer en anglais).
{ "iss": "http://www.simplx.fr", "exp": 1400217551, "scopes": ["customer", "admin"], "sub": "mycustomer@simplx.fr" }
- La signature, qui correspond à la concaténation des deux parties ci dessus, encodé par l’algorithme défini dans le header et une clé secrète.
La personne qui dispose de la clé secrète peut vérifier la validité du token, et décoder la deuxième partie, payload, qui contient les informations utiles.
Où stocker son token JWT ?
Vous construisez une application Web, vous avez développé le formulaire de login qui transmet les identifiants au back-end via une requête post :
POST /api/login HTTP/1.1 Host: simplx.fr Content-Type: application/json Cache-Control: no-cache { "email" : "mycustomer@simplx.fr", "password" : "test" }
et celui-ci vous retourne le token d’authentification qu’il faudra fournir à chaque requête suivante (Je rappelle que notre API Rest est stateless, le serveur ne garde pas de sessions utilisateurs et n’a pour seule connaissance que ce que lui transmet le client via la requête http).
Il faut donc stocker ce token. Deux choix s’offrent à vous :
- stockage dans un cookie
- stockage via web storage d’HTML 5 (local storage ou session storage).
Cookie
Nous avons donc généré côté serveur un token, nous le renvoyons sous forme de Cookie avec la fonction set de la librairie Cookies. Sous node.js :var Cookies = require( "cookies" );
new Cookies(req,res).set('access_token',token, { httpOnly: true, //cookie not available through client js code secure: true // true to force https });
Le paramètre HttpOnly nous permet de définir que le Cookie ne sera pas accessible par le javascript du client. Nous définissons aussi le paramètre secure, pour obliger le client à transmettre le Cookie via HTTPS (Lecteur, s’il te plait, utilise HTTPS !!).
Le Cookie est par la suite automatiquement transmis au serveur par le navigateur, lorsque celui-ci détecte une requête vers le serveur d’origine du Cookie. Charge au serveur d’autoriser ensuite ou non la requête suivant la validité du token reçu.
Web Storage
Le token est renvoyé dans la response HTTP comme suit :
HTTP/1.1 200 OK { "access_token": "eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB", "expires_in":3600 }
Pour le stocker en session storage, il suffit d’affecter la valeur du token à une propriété du storage, cela se fait très simplement avec AngularJs .
$window.sessionStorage.accessToken = response.body.access_token;
Pour le renvoyer par la suite avec chaque requête, on utilise le paramètre « Authorization » du header HTTP. La valeur de cette clé, selon la best practice, doit être défini à : Bearer + la valeur du token stocké en sessionStorage. Exemple de requête HTTP ainsi formé avec cette requête GET :
GET /api/client HTTP/1.1 Host: www.simplx.fr Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Njc3MzE3ODIsInVpZCI6Im5pY29sYXMuZHV2YWxAc2ltcGx4LmZyIiwiZXhwIjoxNDY4MDkxNzgyfQ.0zXGcewCMcX857_uUwKkuk5FPVnOk1HSIfs4mZNsnhI
Web storage vs Cookie
Ces deux modes offrent une solution de stockage pour des informations de sessions côté client, mais quelle est leur différence? Avantages/Inconvénients?
Comme indiqué précédemment, l’utilisation d’un Cookie entraîne le renvoi automatique par le navigateur à chaque requête, suivant la taille du Cookie cela peut être consommateur de bande passante (rien de grave à notre époque mais il faut le savoir). Néanmoins le Cookie est limité en taille (4ko), donc suivant la quantité d’information que vous souhaitez stocker, il faudra peut être vous tourner vers le web storage du navigateur, dont le contenu n’est jamais partagé avec le serveur, et qui a une limite de taille supérieure à 5 Mo.
Problème de sécurité avec le web storage, il est accessible par tout code javascript du même domaine et est donc sensible aux attaques par cross scripting (XSS attacks, une injection de javascript dans votre page web pour exécuter du code malveillant). Bien sûr, si la page web a été bien développée, elle est censée être protégée de ce type d’attaque… Cependant, il est extrêmement fréquent aujourd’hui même pour les développeurs les plus aguerris, d’utiliser des librairies javascript trouvées ça et là sur Internet, et, soyons honnête, vous ne regardez jamais vraiment le code importé. Sachez que l’on trouve sur Github des librairies téléchargées des millions de fois, alors qu’elles ne contiennent que des fonctions de manipulation de string! Et si ce code javascript importé présente une faille, alors votre site devient vulnérable et le token stocké en web storage accessible et réutilisable par un tiers.
Utilisons donc les Cookies me direz vous ? Et bien oui et non…
Les Cookies sont en effet protégés des attaques XSS, nous l’avons vu avec le flag HttpOnly qui empêche l’accès via le javascript du domaine, mais ils sont sensibles à un autre type d’attaque : Cross-site request forgery (CSRF).
CSRF est une attaque au cours de laquelle un site internet malicieux cherche à se faire passer pour le domaine qui est à l’origine du Cookie, et ainsi forcer le navigateur à lui envoyer ce Cookie de manière totalement transparente, à votre insu. Car comme vous le savez désormais (on ne le répétera jamais assez), le navigateur envoie automatiquement le Cookie au serveur, s’il détecte que le Cookie est lié au domaine de ce site.
Démonstration : Vous avez obtenu votre Cookie en vous loguant sur www.simplx.fr, bravo ! Vous continuez à surfer tranquillement sur le site, tout va bien, les requêtes sont authentifiées par le Cookie transmis automatiquement par le navigateur. Un jour vous recevez un mail d’un site se faisant passer pour simplx.fr. Vous cliquez sur un des liens du mail et vous arrivez sur un certain toto.fr, ce site, pâle copie de SimplX, disposera d’un formulaire qui émule un POST vers simplx.fr effectuant une action dont vous n’avez pas conscience. Le navigateur croit avoir à faire à simplx.fr, il transmet alors le Cookie, et la tierce personne peut ainsi faire exécuter tout sorte de requête (suppression par exemple), et ce en votre nom, sans même que vous vous en aperceviez.
Mais alors, quelle solution pour une authentification sécurisée ?
Nous vous proposons ici notre solution, éprouvée avec des projets actuellement en production. Cette solution repose sur l’utilisation combinée de JWT et des Cookies HttpOnly.
En effet, nous allons modifier la PayLoad de notre JWT, en y ajoutant une « claim » (i.e propriété) custom : xsrfToken, un id aléatoirement généré.
Si on considère la Payload vu précédemment, elle devient :
{ "iss": "http://www.simplx.fr", "exp": 1400217551, "scopes": ["customer", "admin"], "sub": "mycustomer@simplx.fr", "xsrfToken": "XgBtvdZF_PWtEbh-CenwqR2N" }
La solution est donc de renvoyer le token encodé via un Cookie HttpOnly au client. Mais on renvoie aussi lors de la même requête de login, la propriété « xsrfToken » via le contenu de la réponse Http. Charge à l’application cliente de stocker cette propriété, comme présenté précédemment lors du stockage du JWT dans le webstorage.
Récapitulatif, côté serveur :
token.xsrfToken = uid.sync(18); //generate random token var jwt_token = jwt.sign(token, config.secret, { expiresIn: 604800 //=1week //6000000 //'2 days' // expires in 2 days }); new Cookies(req,res).set('access_token',jwt_token , { httpOnly: true, secure: true }); // return the information including token as JSON res.status(200).send({ message: 'Enjoy your token!', xsrfToken : token.xsrfToken });
Côté client :
Rien à faire pour envoyer le cookie, SAUF si vous passez par des requêtes XHR, auquel cas, avec AngularJs il vous faut définir la propriété de config du $httpProvider comme suit :
config.withCredentials = true
Par contre, pour la nouvelle propriété xsrfToken stockée en web storage, il vous faut la lire et la renvoyer comme propriété du header http de chacune de vos requêtes authentifiées.
Cela se fait via le même objet config du $httpProvider.
config.headers['x-xsrf-token'] = xsrfToken;
Maintenant charge au serveur de faire le double contrôle, car il reçoit le JWT via le Cookie, il vérifie donc sa validité et le décode. Or, nous avons vu que le Cookie pouvait avoir été transmis suite à une attaque CSRF, c’est donc ici qu’intervient le xsrfToken. Nous récupérons la valeur de la propriété xsrfToken stocké dans le JWT décodé par la serveur grâce à la clef secrète, nous récupérons par ailleurs la valeur de la propriété x-xsrf-token du header de la requête et nous vérifions que ces deux valeurs sont les mêmes. Seule la personne qui dispose à la fois du Cookie, et de la valeur décodée de xsrfToken en webstorage peut être à l’origine de la requête, garantissant donc l’authenticité du message et de son auteur.
Exemple du code côté serveur :
//récupération du token xsrf du header de la requête var xsrfToken = req.headers['x-xsrf-token']; //récupération du JWT du cookie var token = new Cookies(req,res).get('access_token'); //vérification du jwt jwt.verify(token, config.secret, function(err, decoded) { if (decoded.xsrfToken != xsrfToken) { //erreur attaque csrf } else { //pas d'erreur on continue next(); } }
Conclusion
Les tokens JWT sont un excellent moyen de communication, ils permettent d’échanger entre un serveur et différentes applications clientes des informations d’utilisateur et de rôles de manière stateless. Ils sont signés et cryptés pour éviter d’être modifiés côté client, mais attention à l’endroit où vous décidez de les stocker! Nous vous recommandons vraiment d’utiliser les Cookies HttpOnly pour le transmettre, couplé au mécanisme de contrôle de token xsrf, vous serez protégés de manière efficace contre les attaques XSS (car HTML 5 Web Storage est vulnérable et le XSS bien plus fréquent que les attaques CSRF).
Si vous voulez en savoir encore plus, ou que vous avez une solution encore plus sécurisée, n’hésitez pas à nous contacter pour en discuter.
Longue vie aux Applications Web.
Laisser un commentaire