Skip to main content

Créer une API GraphQL sécurisée avec Node.js

Écrit par
Headshot of Lawrence Eagles

Lawrence Eagles

blog banner node js

29 mars 2022

0 minutes de lecture

GraphQL intègre des mécanismes de sécurité dès le départ, avec la validation et la vérification des types. Toutefois, il ne répond pas entièrement aux enjeux de sécurité des API. Dans cet article, nous allons apprendre à sécuriser des API GraphQL en créant une application Node.js simple avec Fastify et GraphQL.

Selon sa documentation officielle, GraphQL est un langage de requête graphique pour les API et un environnement d’exécution permettant de traiter ces requêtes avec nos données. GraphQL décrit clairement les données de notre API et nous permet de créer des API rapides et flexibles, tout en laissant aux clients le contrôle total des données dont ils ont besoin. Comme REST, GraphQL fonctionne sur HTTP : il est donc indépendant des bases de données et compatible avec tout langage backend ou client.

Fastify est un framework Node.js basé sur des plugins, très efficace et extrêmement performant, adapté à la création de serveurs HTTP rapides. Inspiré de Hapi et Express, Fastify offre une alternative plus conviviale pour les développeurs et plus performante, avec une faible surcharge.

Fastify prend en charge GraphQL grâce au plugin Mercurius. Ce plugin est un adaptateur GraphQL configurable pour Fastify, que nous allons découvrir plus en détail dans les sections suivantes.

Commençons par les prérequis.

Prérequis

Voici les prérequis pour cet article :

  • Node.js version 12 ou ultérieure

  • Connaissances de base en JavaScript

  • Connaissances de base en GraphQL

Premiers pas

Pour commencer, nous devons créer un serveur Node.js de base.

Créez un projet de démarrage en créant un dossier de projet, puis exécutez le code ci-dessous dans l’interface de ligne de commande (CLI), depuis ce dossier. Cela initialisera notre application et installera les dépendances nécessaires.

// bootstrap npm project
npm init -y

// install dependencies
npm i fastify nodemon fastify-plugin mercurius-auth jsonwebtoken

Ce projet utilise une version spécifique de Mercurius. Pour l’installer, exécutez la commande suivante :

npm i mercurius@7.9.1

Ensuite, nous activons les modules ES6 pour utiliser le système de modules JavaScript standard plutôt que commonJS, en ajoutant "type": "module" à notre fichier package.json.

Ensuite, nous mettons à jour les scripts NPM avec une commande de démarrage du serveur Node.js. Pour cela, ouvrez le fichier package.json et modifiez la section des scripts comme suit :

"scripts": {
    // start the Node.js server in production
    "start": "node --es-module-specifier-resolution=node ./src/index.js",
    // use nodemon to restart development server when code is compiled
    "dev": "nodemon --es-module-specifier-resolution=node ./src/index.js"
}

Notez que l’extrait --es-module-specifier-resolution=node est nécessaire pour assurer l’interopérabilité entre les modules ES et les modules commonJS de Node.

Créez maintenant un répertoire src à la racine du projet. Dans le répertoire src, créez un dossier graphql contenant les fichiers schema.js et resolvers.js. Ajoutez le code suivant au fichier schema.js :

const schema =`
  type Query {
    users: [User]!
  }

  type User {
    id: ID!
  }
  `;
  export default schema;

Ajoutez maintenant le code suivant au fichier resolvers.js :

const resolvers = {};
export default resolvers;

Nous mettrons à jour le fichier schema.js et ajouterons du code au fichier resolvers.js dans la section suivante. Mais nous devons les créer dès maintenant avec du code passe-partout, car ils sont nécessaires au bon fonctionnement de notre serveur.

Dans le répertoire src, créez un fichier index.js contenant le code suivant :

import fastify from 'fastify';
import mercurius from 'mercurius';
import jwt from 'jsonwebtoken';
import mercuriusAuth from 'mercurius-auth';
import schema from './graphql/schema.js'; 
import resolvers from './graphql/resolvers.js';

const port = process.env.PORT || 4500;
const app = fastify({ logger: true });

// Activate plugins below:
app.register(
  mercurius, { 
      schema, 
      resolvers, 
      graphiql: 'playground', 
      queryDepth: 7 
});

// register auth policy

// create server
const start = async () => {
  try {
    await app.listen(port);
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};
start();

Le code ci-dessus crée un serveur Fastify principal et enregistre le plugin Mercurius avec les options suivantes : schema, resolvers, graphiql et queryDepth.

Nous pouvons maintenant démarrer le serveur en exécutant npm run dev. Voici le résultat :

{"level":30,"time":1620202591072,"pid":11775,"hostname":"pc-name","msg":"Server listening at http://127.0.0.1:4500"}

Nous pouvons constater que notre serveur fonctionne. Dans la section suivante, nous allons commencer à créer nos API de blog avec GraphQL.

Créer une API de blog sécurisée avec Fastify et GraphQL

Il existe plusieurs stratégies pour sécuriser une API, notamment :

  • Authentification et autorisation : l’authentification consiste à vérifier qu’un utilisateur est bien celui qu’il prétend être, tandis que l’autorisation concerne les permissions. L’authentification détermine si un utilisateur peut se connecter et permet ensuite de le reconnaître. L’autorisation définit les permissions attribuées à un utilisateur identifié et détermine s’il peut effectuer des opérations telles que créer, lire, mettre à jour ou supprimer.

  • Masquage des erreurs : ne pas divulguer les informations précises d’une erreur serveur afin d’éviter de révéler involontairement au client des détails susceptibles d’exposer les vulnérabilités du serveur.

  • Limitation de la profondeur des requêtes : définir une profondeur maximale pour les requêtes GraphQL. Les requêtes profondément imbriquées sont dangereuses, car elles mobilisent beaucoup de ressources et sont coûteuses à calculer. Elles peuvent donc faire tomber nos API.

  • Nettoyage et validation des entrées : utiliser des techniques standard de sécurité Web pour empêcher les utilisateurs d’envoyer des données malveillantes. Nous tirerons parti de la validation GraphQL intégrée à notre application.

Dans cet article, nous allons créer et sécuriser nos API à l’aide des stratégies ci-dessus, avec Mercurius et le plugin Mercurius Auth.

Pour nos besoins, le plugin Mercurius Auth offre deux fonctionnalités principales. Premièrement, il nous permet de définir des directives d’authentification personnalisées sur les champs de notre schéma. Ces directives sont des chaînes qui servent d’identifiants pour les champs protégés de notre schéma.

Il permet également d’appliquer des politiques d’authentification personnalisées à ces champs protégés lors d’une requête GraphQL.

Commençons par créer des données fictives. Dans le répertoire src, créez un dossier data contenant un fichier index.js avec le code ci-dessous :

export default {
    users: [
        { id: 1, username: 'JohnDoe', email: 'John_doe@gmail.com', password: '12345', role: 'admin' },
        { id: 2, username: 'JaneDoe', email: 'Jane_doe@gmail.com', password: '12345', role: 'user' },
        { id: 3, username: 'JoeDoe', email: 'Joe_doe@gmail.com', password: '12345', role: 'user' }
    ]
};

Ensuite, configurons notre schema en remplaçant le code passe-partout du fichier schema.js par le code suivant :

const schema = `

directive @auth(
    requires: Role = ADMIN,
  ) on OBJECT | FIELD_DEFINITION

  enum Role {
    ADMIN
    USER
  }

type Query {
    user(id: ID!): User! @auth(requires: ADMIN)
    users: [User]! @auth(requires: ADMIN)
    login(username:String!, password:String!): String
}

type User {
    id: ID!
    username: String!
    email: String!
    password: String!
    role: String!
}
`;

export default schema;

Dans le code ci-dessus, nous avons créé notre schéma GraphQL et défini des directives d’authentification pour les champs user et users. Nous allons bientôt appliquer des politiques personnalisées à ces champs protégés.

Ajoutons maintenant nos résolveurs en remplaçant le code passe-partout du fichier resolvers.js par le code suivant :

import jwt from 'jsonwebtoken';
import Data from '../data';

const resolvers = {
    Query: {
        users: async (_, obj) => Data.users,

        user: async (_, { id }) => {
            let user = Data.users.find((user) => user.id == id);
            if (!user) {
                throw new Error('unknown user');
            }
            return user;
        },

        login: async (_, { username, password }) => {
            let user = Data.users.find((user) => user.username === username && user.password === password);
            if (!user) {
                throw new Error('unknown user!');
            }

            const token = jwt.sign({ username: user.username, password: user.password, role: user.role }, 'mysecrete');
            return token;
        }
    }
};

export default resolvers;

Le code ci-dessus contient des résolveurs qui gèrent les requêtes user, users et login.

Enfin, nous devons ajouter une politique d’authentification personnalisée en enregistrant le plugin Mercurius Auth. Pour cela, dans le fichier index.js du répertoire src, ajoutons le code suivant sous le commentaire register auth policy, à la ligne 19 :

app.register(mercuriusAuth, {
    authContext(context) {
        return { identity: context.reply.request.headers['x-user'] };
    },
    async applyPolicy(authDirectiveAST, parent, args, context, info) {
        const token = context.auth.identity;
        try {
            const claim = jwt.verify(token, 'mysecrete');
        } catch (error) {
            throw new Error(`An error occurred. Try again!`);
        }

        return true;
    },
    authDirective: 'auth'
});

Dans notre politique personnalisée ci-dessus, la méthode authContext récupère le jeton utilisateur dans les en-têtes, tandis que la méthode applyPolicy contient les politiques personnalisées d’authentification et d’autorisation.

De plus, si l’authentification ou l’autorisation d’un utilisateur échoue, nous renvoyons une erreur accompagnée d’un message générique, par exemple « Une erreur s’est produite. Réessayez ! ». Ce message remplace un message d’erreur serveur détaillé, qui pourrait exposer d’éventuelles vulnérabilités du serveur.

Et voilà, notre travail est terminé. Nous allons tester nos API dans la section suivante.

Tester l’API

Commencez par démarrer le serveur en exécutant npm run dev depuis le répertoire racine. Ensuite, accédez à l’interface GraphQL Playground à l’adresse http://localhost:4500/playground.

Lorsque nous interrogeons une API protégée, comme users ou user, nous obtenons l’erreur illustrée ci-dessous :

Interface GraphQL affichant une requête users demandant des informations sur un utilisateur et un message d’erreur : « Une erreur s’est produite. Réessayez ! »

Pour que notre requête aboutisse, nous devons donc nous authentifier. Connectons-nous pour obtenir un jeton.

Pour vous connecter, ouvrez un nouvel onglet dans Playground et exécutez la requête suivante :

query {
  login(username: "JohnDoe", password: "12345")
}

Si la requête aboutit, un jeton est généré et renvoyé, comme illustré ci-dessous.

Interface GraphQL affichant une requête de connexion avec des champs de nom d’utilisateur et de mot de passe, ainsi qu’un jeton d’authentification renvoyé

Notez que les données utilisateur utilisées pour la connexion figurent déjà dans le fichier index.js du répertoire data.

Si vous tentez de vous connecter avec des données utilisateur absentes de ce fichier (par exemple, le nom d’utilisateur « John1Doe »), une erreur s’affiche, comme illustré ci-dessous :

Interface GraphQL affichant une requête de connexion avec des champs nom d’utilisateur et mot de passe, qui renvoie l’erreur « utilisateur inconnu ! »

En transmettant notre jeton dans les en-têtes sous la forme x-user, nous pouvons désormais interroger nos API protégées, comme illustré ci-dessous. Veillez à copier le jeton reçu après la requête de connexion et à utiliser sa valeur pour x-user.

Requête users

Voici un exemple de requête users :

query {
  users {
    id
    username
    password
    email
    role
  }
}

Ajoutez votre jeton comme valeur du paramètre d’en-tête HTTP x-user, comme illustré ci-dessous :

{
  "x-user": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG5Eb2UiLCJwYXNzd29yZCI6IjEyMzQ1Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjQ0NTA3MDE5fQ.faslGjI6x-ODO2LGYOaTHClGs2MXCBOoMlWPYnwoH18"
}

Voici le résultat obtenu :

Interface de requête GraphQL affichant une requête users, un en-tête d’autorisation HTTP et les données utilisateur renvoyées, notamment les noms d’utilisateur, mots de passe, adresses e-mail et rôles.

Requête user

Voici un exemple de requête user :

query {
  user(id: "1") {
    id
    username
    email
    password
    role
  }
}

Ajoutez votre jeton comme valeur du paramètre d’en-tête HTTP x-user, comme illustré ci-dessous :

{
  "x-user": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG5Eb2UiLCJwYXNzd29yZCI6IjEyMzQ1Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjQ0NTA3MDE5fQ.faslGjI6x-ODO2LGYOaTHClGs2MXCBOoMlWPYnwoH18"
}

Voici le résultat obtenu :

Interface GraphQL Playground affichant une requête utilisateur, un en-tête d’autorisation HTTP et les données utilisateur renvoyées, notamment le nom d’utilisateur, l’adresse e-mail, le mot de passe et le rôle d’administrateur

Conclusion

Dans cet article, nous avons découvert à quel point il est facile de sécuriser des API GraphQL en créant une application Node.js simple avec Fastify et GraphQL.

Comme nous l’avons vu, GraphQL intègre certains mécanismes de sécurité, tels que la validation et la vérification des types. Toutefois, la flexibilité et la puissance qui permettent aux utilisateurs de demander librement des données impliquent que la sécurité doit toujours être une priorité.

Cet article a également présenté plusieurs stratégies pour sécuriser les API GraphQL : l’authentification et l’autorisation, la limitation de la profondeur des requêtes, le masquage des erreurs, ainsi que le nettoyage et la validation des entrées.

Ces stratégies de sécurité sont très efficaces, mais nous pouvons renforcer encore la sécurité en mettant en œuvre d’autres méthodes, comme les délais d’expiration des requêtes et la limitation du débit, qui définissent la fréquence à laquelle un client peut interroger une API pendant une période donnée.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Essayez gratuitement notre outil en ligne de vérification du code JavaScript pour découvrir comment le moteur Snyk Code analyse votre code afin d’identifier les problèmes de sécurité et de qualité.