Les API REST sont aujourd’hui omniprésentes, elles permettent la communication et l’échange de données entre applications et systèmes hétérogènes. Vient alors la question de la sécurisation de l’échange de ces données. Nous allons donc voir dans cet article comment sécuriser une API REST.
Qu’est-ce qu’une API REST
Avant de commencer, faisons un bref rappel sur ce qu’est une API REST. REST (pour REpresentational State Transfer) est un style d’architecture basé sur le protocole HTTP et qui permet de manipuler des ressources via un URI. Pour manipuler ces ressources, une API REST utilise les méthodes HTTP suivantes :
- GET : Récupération d’une ressource;
- POST : Ajout d’une ressource;
- PUT : Mise à jour complète d’une ressource;
- PUT : Mise à jour partielle d’une ressource;
- DELETE : Suppression d’une ressource;
- HEAD : Similaire à GET, mais permet uniquement de récupérer les en-têtes HTTP.
Par exemple une API REST qui gère des articles d’un blog pourrait mettre à disposition les actions suivantes :
| Action | M\u00e9thode | URI |
|---|---|---|
| R\u00e9cup\u00e9rer la liste des articles | GET | /api/posts |
| R\u00e9cup\u00e9rer un article en particulier | GET | /api/posts/8 |
| R\u00e9cup\u00e9rer le d\u00e9tail d’un article | GET | /api/posts/8/details |
| Ajouter un article | POST | /api/posts |
| Modifier un article en particulier | PUT | /api/posts/8 |
| Supprimer un article en particulier | DELETE | /api/posts/8 |
Une API REST doit également respecter un certain nombre de contraintes :
- Client-Serveur : Il y a séparation des rôles entre le client et le serveur permettant à chacun d’évoluer séparément;
- Sans état (stateless) : Chaque requête du client contient toutes les informations nécessaires au traitement. Il n’y a donc pas de session côté serveur;
- Cache : La réponse du serveur peut-être mise en cache côté client ou côté serveur;
- Interface uniforme (Uniform Interface) : Chaque ressource doit être identifiable de manière unique via son URI, représentable (via le format XML ou JSON par exemple), manipulable via sa représentation (en utilisant les méthodes HTTP), autodescriptive (doit contenir toutes les informations pour son traitement, par exemple on peut connaitre son format via l’utilisation des types MIME);
- Organisation en couches (Layered System) : Le système peut être séparé en plusieurs couches (serveurs proxy, load balancers, firewalls, etc.);
- Code à la demande (Code-On-Demand) : Cette contrainte optionnelle permet au client d’exécuter du code à la demande, c’est-à-dire d’étendre une partie de la logique du serveur au client, via l’envoi de code JavaScript par exemple.
Sécurité
Maintenant que l’on sait ce qu’est une API REST, voyons comment sécuriser les échanges entre le client et celle-ci.
Utilisation de HTTPS
Bon, j’espère ne rien vous apprendre, mais la première étape de la sécurisation d’une API REST est l’utilisation du protocole HTTPS. Cela permettra de chiffrer les données transmises et reçues empêchant ainsi leur lecture.
Authentification
Il faut ensuite un mécanisme authentification des échanges entre le client et l’API REST. Comme je l’ai rappelé précédemment, une API REST est sans état (stateless) c’est-à-dire qu’il n’y a pas de session côté serveur pour l’authentification de l’utilisateur. De ce fait, chaque requête doit contenir les informations nécessaires à l’authentification.
On pourrait très bien utiliser la méthode “Basic” de l’authentification HTTP (“basic auth”), mais cela implique de transmettre pour chaque requête, le couple login/mot de passe ce qui n’est franchement pas top. Nous allons plutôt utiliser un JSON Web Token (JWT).
Présentation de JWT
JWT ou JSON Web Token est un standard ouvert décrit dans la RFC 7519 qui permet l’authentification d’un utilisateur à l’aide d’un jeton (token) signé. Le principe est le suivant :
- Lors du premier échange, le client envoie son couple login/mot de passe au serveur;
- Si le couple est valide, le serveur génère un token et l’envoie au client. Ce token permettra d’authentifier l’utilisateur lors des prochains échanges;
- Le client stocke ensuite le token en local;
- Le token est renvoyé, par le client, pour chaque appel à l’API (via l’en-tête HTTP « Authorization ») permettant ainsi d’authentifier l’utilisateur.
Le token généré est composé de trois parties séparées par un point :
- Header;
- Payload;
- Signature.
Pour la suite, prenons le JWT suivant :
“eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2NvZGVoZXJvZXMuZnIiLCJpYXQiOjE1MjEzMDk2MDAsImV4cCI6MTUyMTMxMzIwMCwiYXVkIjoiaHR0cHM6Ly9zaXRlY2xpZW50LmZyIiwic3ViIjoiMTI0Iiwicm9sZSI6InVzZXIifQ.Lml5MSnQKGhTxTtkM92sAEXxQEDvOYPtVZWphciwOiM”
Header
La première partie du JWT est le header. Il s’agit d’un objet JSON encodé en base64 qui représente l’en-tête du token. Il est composé de deux parties :
- Le type du token;
- L’algorithme utilisé pour la signature.
Dans notre exemple de JWT, le header est “eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9”. Si nous le décodons, nous obtenons :
{ "typ":"JWT", "alg":"HS256"}Payload
La seconde partie du JWT est le payload. Il s’agit tout comme le header, d’un objet JSON encodé en base64 qui représente cette fois-ci le corps du token. C’est dans cette partie que l’on mettra les informations de l’utilisateur (identifiant, rôle, etc.) ou toute autre information utile au serveur. Le standard définit trois types de propriétés (appelées claims) :
- Propriétés réservées : Il s’agit de noms réservés définis par la spécification. On y retrouve notamment :
- “iss” (Issuer) : Permet d’identifier le serveur ou le système qui a émis le token;
- “sub” (Subject) : Il s’agit généralement de l’identifiant de l’utilisateur que le token représente;
- “aud” (Audience) : Il s’agit généralement de l’application ou du site qui reçoit le token;
- “iat” (Issued At) : Il s’agit de la date de génération du token;
- “exp” (Expiration Time) : Il s’agit de la date d’expiration du token.
- Propriétés publiques : Il s’agit de noms normalisés tels que “email”, “name”, “locale”, etc. La liste complète est disponible à cette adresse;
- Propriétés privées : Il s’agit de propriétés que vous définissez vous-même pour répondre aux besoins de votre application.
Dans notre exemple de JWT, le payload est “eyJpc3MiOiJodHRwczovL2NvZGVoZXJvZXMuZnIiLCJpYXQiOjE1MjEzMDk2MDAsImV4cCI6MTUyMTMxMzIwMCwiYXVkIjoiaHR0cHM6Ly9zaXRlY2xpZW50LmZyIiwic3ViIjoiMTI0Iiwicm9sZSI6InVzZXIifQ”. Si nous le décodons nous obtenons :
{ "iss":"https://codeheroes.fr", "iat":1521309600, "exp":1521313200, "aud":"https://siteclient.fr", "sub":"124", "role":"user"}⚠️ Attention le payload ne doit pas contenir de données sensibles.
Signature
La dernière partie est la signature du token. Il s’agit d’un hash des deux premières parties du token réalisé en utilisant l’algorithme qui est précisé dans le header. Dans notre exemple de token ci-dessus, l’algorithme utilisé est HS256 (HMAC-SHA-256), la signature est donc créée de cette manière :
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), 'secret')L’algorithme utilise une clé secrète (détenue par le serveur), utilisée pour signer les tokens mais également s’assurer de la validité de ceux-ci en vérifiant leur signature. De ce fait, si un utilisateur malveillant modifie le contenu du token, la signature ne sera plus correcte et le jeton sera ainsi rejeté. Dans notre exemple ci-dessus, si l’utilisateur change son rôle en “admin” la signature est bien modifiée :
- Signature du token avec le rôle “user” : Lml5MSnQKGhTxTtkM92sAEXxQEDvOYPtVZWphciwOiM
- Signature du token avec le rôle “admin” : bOSIz8-3jCRQJI4MrSh86T0EVNS_ZeY6cphSaGybZ1Y
Il est également possible d’utiliser d’autres types d’algorithmes pour la création de la signature (comme RSA-SHA256 par exemple).
Je vous invite à aller sur le site https://jwt.io/ pour tester vos JWT.
Expiration d’un token
Comme nous l’avons vu plus haut, un JWT possède une date d’expiration (propriété “exp” du payload), ainsi lorsqu’un JWT expiré est envoyé lors d’une requête à l’API, celle-ci renverra une erreur 401 indiquant que le JWT n’est plus valide. La durée de validité d’un JWT va dépendre du type de données échangées. Elle sera de quelques minutes pour l’échange de données sensibles à plusieurs heures pour les données non sensibles. Libre à vous de choisir cette durée de validité.
Afin d’éviter à l’utilisateur de devoir se réauthentifier lorsque le JWT expire, il est possible d’utiliser un “refresh token”. Lorsque l’utilisateur s’identifie à l’aide de son couple login/mot de passe, le serveur génère en plus du JWT, un token aléatoire à usage unique attaché à l’utilisateur (généralement sauvegardé en base de données).
Lorsque le JWT est expiré :
- Le client (application ou site) envoie une requête sur une route particulière de notre API avec le “refresh token”;
- Le serveur vérifie que le “refresh token” est valide et que celui-ci est bien associé à un utilisateur;
- Le serveur génère un nouveau JWT, un nouveau “refresh token” (lié à l’utilisateur) et invalide l’ancien “refresh token” (par exemple suppression de la base de données ou un champ indiquant que ce token n’est plus valide);
- Le serveur renvoie le nouveau JWT et “refresh token” au client;
- Le client sauvegarde le JWT et le “refresh token”;
- Le client peut utiliser le nouveau JWT.
Il est également possible d’ajouter une date d’expiration au “refresh token”.
Utilisation de clés API
L’utilisation des JWT est utile lorsque vous souhaitez authentifier un utilisateur, mais lorsque les consommateurs de votre API sont des applications ou des sites, il convient d’utiliser des clés d’API. Généralement, vous devrez fournir à l’application utilisant votre API :
- Une clé d’API permettant d’identifier l’application;
- Une clé secrète, partagée entre l’application et l’API REST, permettant de signer les requêtes et d’authentifier l’application.
Pour chaque requête :
- L’application crée une signature de la requête à l’aide d’un algorithme HMAC et de la clé secrète. La signature est faite sur les en-têtes HTTP de la requête que vous aurez spécifiés (la méthode de construction de la signature est décrite ici : https://tools.ietf.org/html/draft-cavage-http-signatures-09);
- L’application envoie la requête avec sa clé d’API (“keyId”), l’algorithme (“algorithm”) et les en-têtes HTTP (“headers”) utilisés pour créer la signature ainsi que la signature (“signature”) de la requête dans l’en-tête HTTP “Authorization”;
- L’API vérifie l’identité de l’application via la clé d’API et vérifie la signature de la requête à l’aide de la clé secrète afin d’authentifier l’application.
La clé d’API seule n’est pas utilisable, elle permet uniquement d’identifier une application donc dans le cas où un utilisateur malveillant dérobe une clé d’API, celui-ci ne pourra rien faire sans la clé secrète qui permet, elle de garantir la preuve de l’identité de l’application via la signature des requêtes.
⚠️ La clé secrète ne doit donc jamais être transmise dans une requête, l’application doit sauvegarder celle-ci de manière sécurisée.
Conclusion
On a vu tout au long de cet article, différentes manières de sécuriser une API REST. Voici les points importants à retenir :
- L’utilisation du protocole HTTPS pour chiffrer les échanges entre le client et le serveur;
- L’utilisation des JSON Web Token (JWT) pour l’authentification des utilisateurs. Attention à ne pas définir une durée de validité trop élevée pour le JWT dans le cas d’échanges de données sensibles;
- L’utilisation de clé d’API et d’une clé secrète partagée pour les échanges entre une application et votre API REST. La clé secrète permettra de signer vos requêtes et d’authentifier l’application.
Cet article n’est bien entendu pas exhaustif, il existe d’autre manière de sécuriser les échanges d’une API REST (utilisation de OAuth par exemple), mais celui-ci constitue un bon point de départ. Si vous souhaitez que je rédige un article pour l’implémentation en Node.js de ces recommandations de sécurité, n’hésitez pas à me le dire en commentaire ou sur Twitter 😉.
Partage ta réflexion, pose une question ou laisse un retour.