Comment créer une passerelle API sécurisée avec Node.js
Florian Rappl
28 décembre 2022
0 minutes de lectureLes microservices offrent des avantages considérables par rapport aux architectures monolithiques. Ils facilitent la mise à l’échelle du développement et vous donnent un contrôle précis sur celle de l’infrastructure. De plus, la possibilité d’effectuer de nombreuses petites mises à jour et des déploiements progressifs réduit considérablement le délai de mise sur le marché.
Malgré ces avantages, l’architecture en microservices pose un problème : l’impossibilité d’accéder à ses services depuis l’extérieur. Heureusement, une passerelle API permet de résoudre ce problème.
Les passerelles API offrent à vos applications front-end (par exemple, les sites web et les applications natives) un accès clair à toutes les fonctionnalités back-end. Elles peuvent servir d’agrégateurs pour les microservices et de middleware pour gérer des problématiques courantes, comme l’authentification et l’autorisation. Elles facilitent également le regroupement et l’unification des services, en combinant des formats de sortie comme XML et JSON en un seul format.
Les passerelles API sont des outils essentiels pour la sécurité et la fiabilité. Elles assurent la gestion des sessions, le nettoyage des entrées, la protection contre les attaques par déni de service distribué (DDoS), la limitation du débit et la journalisation des transactions.
Dans cet article, nous allons créer de toutes pièces une passerelle API sécurisée en utilisant uniquement Node.js et quelques packages open source. Vous avez simplement besoin de connaître les bases du terminal, de disposer de Node.js version 14 ou ultérieure et de connaître JavaScript. Vous trouverez le code final du projet ici.
C’est parti !
Créer notre passerelle API
Même si nous pourrions écrire un serveur web de toutes pièces avec Node.js, nous allons ici nous appuyer sur un framework existant pour faire le gros du travail. Express est sans doute le choix le plus populaire pour Node.js : léger, relativement rapide et facilement extensible. De plus, vous pouvez intégrer presque tous les packages nécessaires à Express.
Pour commencer, créons un nouveau projet Node.js et installons Express :
Avec ces deux commandes, nous avons créé un projet Node.js en utilisant les paramètres par défaut. Nous avons également installé le package express. À ce stade, notre répertoire de projet devrait contenir un fichier package.json et un répertoire node_modules.
Nous devons maintenant créer un fichier nommé index.js, que nous placerons à côté de package.json. Il doit contenir le code initial suivant :
Cette configuration suffit pour exécuter un serveur web simple avec un point de terminaison. Vous pouvez le démarrer en exécutant la commande suivante dans votre terminal :
Dans votre navigateur, accédez ensuite à http://localhost:3000. Le message « Hello user! » devrait s’afficher à l’écran. En ajoutant un paramètre à l’URL, par exemple ?name=King, vous devriez voir « Hello King! ».
L’étape suivante consiste à configurer l’authentification. Pour cela, nous utilisons le package express-session. Celui-ci s’appuie sur un cookie pour protéger nos points de terminaison. Les utilisateurs qui ne possèdent pas de cookie de session valide recevront une réponse appropriée avec le code d’état HTTP 401 (Unauthorized) ou 403 (Forbidden).
Nous allons également mieux protéger notre mot de passe en le stockant dans une variable d’environnement. Le package dotenv nous permettra de charger la variable SESSION_SECRET depuis un fichier .env dans process.env.
Commencez par arrêter le processus précédent en saisissant Ctrl + C dans votre console. Installez ensuite express-session et dotenv à l’aide de la commande suivante :
Pour savoir comment configurer et établir une session sécurisée, consultez la documentation d’express-session sur GitHub. Vous y trouverez notamment comment définir l’indicateur secure du cookie sur true afin qu’il soit transmis uniquement via des connexions HTTPS. Il convient également de mettre en place d’autres contrôles de sécurité pour la gestion des sessions, par exemple régénérer l’identifiant de session lors d’opérations sensibles comme la connexion ou le changement de mot de passe.
Créez ensuite un fichier .env à la racine de votre projet et ajoutez-y le code suivant :
Par convention, les variables d’environnement s’écrivent en majuscules. Veillez à utiliser un secret unique et aléatoire : un mot de passe robuste fera l’affaire.
Voici à quoi doivent ressembler les ajouts apportés à index.js (nous réutiliserons cet extrait plus loin pour assembler la solution) :
Cette configuration met en place une gestion des sessions en mémoire, que vous pouvez utiliser pour protéger des points de terminaison. Ajoutons un nouveau point de terminaison et créons des points de terminaison dédiés à la connexion et à la déconnexion. Ajoutez le code suivant à la fin de index.js :
Relancez le code pour le tester en exécutant la commande node index.js.
Comment cela fonctionne-t-il ? Voici un parcours d’utilisation simple :
L’accès à
/fonctionne.L’accès à
/protectedrenvoie le code d’état HTTP401 (Unauthorized).L’accès à
/loginvous connecte automatiquement et affiche « Successfully authenticated. »Un nouvel accès à
/loginaffiche « Already authenticated. »Vous pouvez alors accéder à
/protected.L’accès à
/logoutréinitialise la session et affiche « Successfully logged out. »Le point de terminaison
/protectedredevient alors inaccessible et affiche le code d’état401.Un nouvel accès à
/logoutrenvoie également le code d’état401.
En utilisant le middleware protect avant le gestionnaire de requêtes, nous pouvons protéger un point de terminaison en vérifiant que l’utilisateur est connecté. Nous avons également les points de terminaison /login et /logout pour compléter ces fonctionnalités.
Arrêtons de nouveau le processus avec Ctrl + C. Avant d’aborder le cœur de notre passerelle API, examinons la journalisation et la limitation du débit.
Limitation du débit
La limitation du débit garantit que votre API ne peut être appelée qu’un nombre donné de fois pendant une période définie. Elle la protège ainsi contre l’épuisement de la bande passante dû au trafic normal ou aux attaques par déni de service (DoS). Vous pouvez configurer des limites de débit pour le trafic provenant de sources spécifiques. Il existe par ailleurs de nombreuses façons de calculer et d’appliquer la fenêtre temporelle pendant laquelle les requêtes sont traitées.
Commençons par installer un package de limitation du débit :
Configurons ensuite le package. Insérez ce code dans le fichier index.js, avant les routes auxquelles vous souhaitez appliquer une limite :
Pour tester, redémarrez le serveur avec la commande node index.js et appelez plusieurs fois notre point de terminaison initial. Lorsque vous aurez atteint la limite de cinq requêtes en 15 minutes, le message « Too many requests, please try again later. » s’affichera. Par défaut, express-rate-limit renvoie également le code d’état HTTP 429 (Too Many Requests).
Avant de passer à l’étape suivante, arrêtez le processus pour réinitialiser votre limite.
Journalisation
Pour mettre en place la journalisation, nous pouvons utiliser winston, qui propose également un middleware dédié à Express : express-winston. Nous pouvons aussi journaliser les temps de réponse des points de terminaison pour les examiner de plus près par la suite. Le package response-time est particulièrement utile pour cela. Installons ces packages.
L’intégration de ces packages est simple. Ajoutez le code suivant à index.js, avant tout code que vous souhaitez journaliser. Il est donc préférable de l’insérer avant le code de limitation du débit :
Cette configuration génère des informations supplémentaires. Au démarrage du serveur, si vous accédez à /, la console devrait afficher ce qui suit :
Lorsque vous êtes prêt à continuer, arrêtez le processus avec Ctrl + C.
Partage des ressources entre origines (CORS)
Notre passerelle API servant d’intermédiaire entre les services front-end et back-end, nous allons y gérer le partage des ressources entre origines (CORS). CORS est un mécanisme de sécurité du navigateur qui garantit que le back-end accepte certaines requêtes interorigines (par exemple, des requêtes de www.company.com vers api.company.com).
Pour cela, nous envoyons une requête spéciale avant la requête principale. Cette requête utilise la méthode OPTION HTTP et attend des en-têtes spécifiques dans la réponse, qui autorisent ou refusent les requêtes suivantes.
Pour activer CORS dans notre passerelle, nous pouvons installer le package cors. Nous pouvons également ajouter des en-têtes de sécurité supplémentaires à l’aide d’un package utile appelé helmet. Installez les deux packages avec la commande suivante :
Nous pouvons les intégrer comme suit :
Cette configuration permet à tous les domaines d’accéder à l’API. Nous pourrions définir des paramètres plus précis, mais ceux-ci suffisent pour le moment.
Proxy
Une passerelle API transfère principalement les requêtes vers d’autres microservices dédiés, afin de router les requêtes liées à la logique métier et les autres requêtes HTTP. Nous avons donc besoin d’un package pour gérer ce transfert : un proxy. Nous allons utiliser http-proxy-middleware, que vous pouvez installer avec la commande suivante :
Ajoutons-le maintenant, ainsi qu’un nouveau point de terminaison :
Pour tester, démarrez le serveur et accédez à /search?q=x&format=json. Vous obtiendrez les résultats de la requête transmise à http://api.duckduckgo.com/. Si vous exécutez le code, arrêtez le processus une fois le test terminé pour pouvoir appliquer les dernières modifications.
Configuration
Maintenant que nous avons tous les éléments, configurons-les pour qu’ils fonctionnent ensemble. Pour cela, créons un fichier nommé config.js dans le même répertoire que les autres fichiers :
Nous avons défini ici la configuration essentielle de notre passerelle API : son port, sa clé de chiffrement des sessions par cookie et les différents points de terminaison à utiliser comme proxy. Les options de chaque proxy correspondent à celles de la fonction createProxyMiddleware, auxquelles s’ajoute la clé protected.
Modifions également les déclarations de secret et de port, ainsi que l’appel à rateLimit dans index.js, pour qu’ils utilisent les valeurs définies dans notre fichier de configuration :
Nous devons également supprimer le code dont les fonctionnalités ont été déplacées dans config.js, ainsi que le code utilisé pour tester notre point de terminaison initial. Supprimez le code suivant de index.js :
Application finale
Passons en revue le contenu du fichier index.js final, qui comporte quelques petits ajouts :
Ajoutons une logique pour vérifier les valeurs de la propriété protected des proxys répertoriés dans config.js. Si elles sont définies sur false, la fonction alwaysAllow que nous définissons ici transmet la requête au gestionnaire suivant :
Certaines technologies de serveur plus anciennes ajoutent également à l’en-tête HTTP des informations descriptives non fonctionnelles sur le serveur. Pour sécuriser notre API, nous allons les supprimer afin de divulguer moins d’informations à d’éventuels acteurs malveillants :
Nous parcourons ensuite les proxys répertoriés dans config.js, vérifions la valeur de leur paramètre protected, appelons la fonction protect ou alwaysAllow définie précédemment, puis ajoutons un nouveau proxy pour chaque entrée configurée :
Ce code est déjà assez flexible, mais il utilise HTTP standard plutôt que HTTPS. Cela peut suffire dans de nombreux cas. Par exemple, dans Kubernetes, le code peut se trouver derrière une entrée NGINX qui gère TLS et s’occupe du certificat.
Il peut toutefois arriver que nous souhaitions exposer directement le code en cours d’exécution sur Internet. Dans ce cas, nous aurions besoin d’un certificat valide.
Une autorité de certification comme Let’s Encrypt propose plusieurs méthodes pour activer HTTPS. Vous pouvez utiliser un client comme Greenlock Express pour gérer automatiquement vos certificats, ou adopter une approche plus précise avec un client comme Publishlab acme-client.
Notez que cette opération nécessite sudo, mais c’est de toute façon prévu : HTTPS n’est utile ici que si le serveur est directement exposé. Nous avons donc besoin du port 443 plutôt que d’un port public comme 8443.
Conclusion
Nous avons terminé la création, à partir de zéro, d’une passerelle API sécurisée avec Node.js. Nous avons implémenté des fonctionnalités telles que la gestion des sessions, la limitation du débit, les proxys, la journalisation et CORS. Nous avons également appris à configurer Node.js pour utiliser TLS et imposer l’accès via HTTPS.
L’utilisation d’une passerelle API présente des avantages considérables pour les infrastructures back-end de grande taille. En plaçant les services back-end derrière une passerelle API, nous pouvons facilement appliquer des protocoles de sécurité communs, ce qui simplifie considérablement nos services et les protège contre les vulnérabilités courantes.


