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éthode | URI |
---|---|---|
Récupérer la liste des articles | GET | /api/posts |
Récupérer un article en particulier | GET | /api/posts/8 |
Récupérer le détail 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 :
1 2 3 4 | { "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 :
1 2 3 4 5 6 7 8 | { "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 :
1 | 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 .
Je suis lead developer dans une boîte spécialisée dans l’univers du streaming/gaming, et en parallèle, je m’éclate en tant que freelance. Passionné par l’écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J’adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs.
Si vous avez envie de papoter, n’hésitez pas à me retrouver sur Twitter, m’envoyer un petit email ou même laisser un commentaire.
Je serais très intéressé par un article avec une implémentation en Node.js 🙂
Pas de soucis 😉
Merci pour l’article, une implémentation en node.js serais super intéressant 😁
Pas de soucis, c’est prévu dès que je trouve un peu de temps 😉
Merci pour l’article. Très instructif
Salut,
Je cherchais un article dans ce genre depuis longtemps, c’était très instructif.
Je trouve un peu dommage et « court » la partie dédiée au api-key utilisables avec une application.
Je m’interroge d’ailleurs sur la sécurité effective via la clé secrète, puisqu’on a vite besoin de communiquer cette clé secrète et l’api key lorsqu’un client veut utiliser notre api pour son application.
Si quelqu’un intercepte les informations à ce moment précis, on ne sécurise pas plus qu’avec le simple usage de l’api key, non ?
Donc il doit me manquer une subtilité que je n’ai pas encore comprise…
Merci !
Salut,
Le client doit garder précieusement sa clé secrète et ne jamais la transmettre. La clé secrète va servir à signer la requête côté client et vérifier la signature de la requête côté serveur (pour rappel la clé API permet d’identifier le client et la signature de la requête d’authentifier le client). Maintenant, comment transmettre cette clé secrète de manière sécurisée ? ça c’est à toi de voir, mais si on part du principe que c’est le serveur qui l’a transmet via le protocole HTTP, tu dois sécuriser la requête en y ajoutant une couche de chiffrement (HTTPS). Tu as la même problématique lorsque tu te connectes à un site internet, ton mot de passe doit bien être transmis au serveur. Après rien ne t’empêche de ne transmettre la clé secrète qu’une seule fois, si le client perd cette clé secrète il devra redemander un couple clé API/clé secrète. Tu peux également mettre en place l’algorithme de Diffie-Hellman qui permettra au client et au serveur de se mettre d’accord sur la clé secrète sans jamais transmettre celle-ci: Lien wikipédia et Vidéo explicative mais personnellement je me contente d’utiliser le protocole HTTPS. J’espère avoir répondu à ta question 😉
@Arkerone,
Article intéressant.
Je cherche aussi à authentifier une application, et le partage de la clef privée me pose problème.
J’ai pensé à l’utilisation de certificat TLS en mode two-way authentification. Mais comment garantir (tous comme pour l’api-key) que la clef privée n’a pas été compromise ?
Normalement il devrait il y avoir une clef privée par application déployé. Pour moi le problème n’est toujours pas résolu.
Merci pour ton commentaire. La clé privée est transmise qu’une seule fois (lors de sa génération) et celle-ci transite via le protocole HTTPS donc elle est chiffrée sur le réseau. C’est ensuite à la responsabilité du client de stocker cette clé privée de manière sécurisée. Par contre, tu peux associer une liste d’adresses IP autorisées aux clés API et vérifier la provenance des requêtes. Après comme je l’ai dit dans un précédent commentaire, tu as la même problématique lorsque tu te connectes à un site internet, ton mot de passe doit bien être transmis au serveur. Et sinon oui il y a bien une clé privée par application déployée.
Merci, c’est ce que j’ai cherché a comprendre exactement
Très bien expliqué
Bonjour et merci pour l’article, j’ai justement créer une api pour un client que je doit sécurisé avec une clé , mon api est écrite en php et je dois avoué que je vois pas de tout comment faire .je code juste depuis 3 mois .
avez vous des ressources ou des exemples en php ?
merci
Bravo à toi , on peut pas faire plus claire .
justement j’ai créé une api en php , il me manque juste de passé une clé pour api
je sais pas ce que ça donne avec php , j’ai vu ton article avec node.js
c’est vraiment très propre
Hello,
Merci pour tes articles, ça fait du bien d’avoir quelques infos en Français 🙂
j’ai parcouru différents site concernant JWT et notamment l’histoire du refresh Token, mais je n’ai pas encore saisie pourquoi, pourquoi un refresh token alors qu’un token fait l’affaire.
1/ je comprend bien que l’AccessToken doit avoir une expiration courte (fonction des données)
2/ je comprend aussi le fait qu’une fois l’AccessToken expiré, l’utilisateur devra s’authentifier de nouveau et ce n’est pas top.
Admettons que notre AccessToken ai une validité de 1 minute et notre RefreshToken 10 minutes.
Si on utilise pas de RefreshToken, l’utilisateur devra s’authentifier chaque minute …
A l’aide du RefreshToken, l’utilisateur devra s’authentifier toutes les 10 minutes ; alors pourquoi ne pas passer directement l’AccessToken à 10 minutes ?
On est d’accord que 10 minutes c’est un exemple et on mettra plus tôt 24 heures que 10 minutes.
J’ai pensé aussi à avoir un AccessToken avec une courte durée, genre 10 minute, puis comme mon application effectue un ping à l’API sur chaque page / contenu sécurisé (afin de vérifier si le user est connecté et le token valide), l’API peut à se moment générer un nouveau AccessToken qu’elle transmettra à l’APP de la même façon qu’un RefreshToken.
Il y a un truc qui m’échappe, tu pourrais m’éclairer ?
la finalité de la chose, c’est que l’on ne veut pas déconnecter l’utilisateur tant qu’il est actif sur l’APP qu’importe le temps définis sur l’AccessToken ; ce temps doit correspondre selon moi à une durée d’inactivité.
L’access token ne doit pas être utilisé pour récupérer un autre access token. Je te donne un exemple, imagine que ton access token ait été intercepté par un pirate (je rappelle que l’access token est envoyé lors de chaque appel API), si celui-ci permet de récupérer un autre access token, comme tu l’as décrit, le pirate pourra avoir accès à ton compte sans limite de temps. Le fait d’avoir une durée de vie courte pour un access token limite ce risque car une fois arrivé à expiration, les seuls moyens d’en récupérer un nouveau sont soit se réauthentifier avec le couple login/password soit justement utiliser un refresh token à usage unique. L’access token est utilisé uniquement pour accéder aux ressources, tandis que le refresh token est quant à lui uniquement utilisé pour récupérer un nouveau access token.
Hello,
Super tuto merci !
Juste une question bête, les tokens : AccessToken & RefreshToken peuvent être stockés côté serveur en base de données ?
Faut-il les stocker en clair ou peut-on ajouter un chiffrement (Blowfish en php) ?
Merci !
Bonjour, dans le cas d’une appli dispo pour le public, je ne vois pas comment se prémunir du spam si un hacker decompile l’appli. Merci
Si tu parles des clés d’API tu ne les utilises pas côté front mais plus côté back 😉