GraphQL se présente comme une alternative de plus en plus prisée à l’architecture traditionnelle des API REST. Ce langage de requête et de manipulation de données offre une flexibilité et une efficacité remarquables. Face à son adoption croissante, il est devenu essentiel de prioriser la sécurité des API GraphQL pour prémunir les applications contre les accès non autorisés et les potentielles fuites de données.
L’implémentation de jetons web JSON (JWT) constitue une approche particulièrement efficace pour sécuriser les API GraphQL. Ces jetons permettent d’accorder un accès sécurisé et efficace aux ressources protégées, tout en autorisant des actions spécifiques. Ils garantissent une communication sécurisée entre les clients et les API.
Authentification et autorisation dans les API GraphQL
Contrairement aux API REST qui utilisent plusieurs points de terminaison, les API GraphQL n’en utilisent généralement qu’un seul. Ce dernier permet aux clients de demander dynamiquement différentes quantités de données dans leurs requêtes. Cette flexibilité, bien que constituant la force de GraphQL, augmente également les risques d’attaques potentielles, notamment les vulnérabilités liées au contrôle d’accès.
Pour atténuer ces risques, il est crucial de mettre en place des processus d’authentification et d’autorisation robustes, en définissant de manière précise les autorisations d’accès. Cette démarche assure que seuls les utilisateurs autorisés puissent accéder aux ressources protégées et réduit ainsi le risque de failles de sécurité et de perte de données.
Le code source de ce projet est disponible sur le dépôt GitHub.
Configuration d’un serveur Apollo avec Express.js
Apollo Server est une implémentation serveur largement utilisée pour les API GraphQL. Il facilite la création de schémas GraphQL, la définition de résolveurs et la gestion de diverses sources de données pour vos API.
Pour configurer un serveur Apollo avec Express.js, commencez par créer et ouvrir un dossier de projet :
mkdir graphql-API-jwt
cd graphql-API-jwt
Ensuite, initialisez un nouveau projet Node.js à l’aide de npm, le gestionnaire de packages Node :
npm init --yes
Installez ensuite les packages suivants :
npm install apollo-server graphql mongoose jsonwebtoken dotenv
Enfin, créez un fichier server.js à la racine de votre projet et configurez votre serveur avec ce code :
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({ req }),
});const MONGO_URI = process.env.MONGO_URI;
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connexion à la base de données établie");
return server.listen({ port: 5000 });
})
.then((res) => {
console.log(`Serveur démarré sur ${res.url}`);
})
.catch(err => {
console.log(err.message);
});
Le serveur GraphQL est configuré avec les paramètres `typeDefs` et `resolvers`, définissant ainsi le schéma et les opérations que l’API peut gérer. L’option `context` configure l’objet `req` en fonction du contexte de chaque résolveur, permettant au serveur d’accéder aux détails spécifiques à la demande tels que les valeurs d’en-tête.
Création d’une base de données MongoDB
Pour établir la connexion à la base de données, créez une base de données MongoDB ou configurez un cluster sur MongoDB Atlas. Copiez ensuite la chaîne URI de connexion à la base de données fournie, créez un fichier `.env` et entrez la chaîne de connexion comme suit :
MONGO_URI="<mongo_connection_uri>"
Définition du modèle de données
Définissez un modèle de données à l’aide de Mongoose. Créez un nouveau fichier `models/user.js` et incluez le code suivant :
const {model, Schema} = require('mongoose');const userSchema = new Schema({
name: String,
password: String,
role: String
});module.exports = model('user', userSchema);
Définition du schéma GraphQL
Dans une API GraphQL, le schéma définit la structure des données qui peuvent être interrogées, ainsi que les opérations disponibles (requêtes et mutations) pour interagir avec ces données. Il permet de structurer la communication avec l’API.
Pour définir un schéma, créez un nouveau dossier dans le répertoire racine de votre projet et nommez-le `graphql`. Dans ce dossier, ajoutez deux fichiers : `typeDefs.js` et `resolvers.js`.
Dans le fichier `typeDefs.js`, insérez le code suivant :
const { gql } = require("apollo-server");const typeDefs = gql`
type User {
id: ID!
name: String!
password: String!
role: String!
}
input UserInput {
name: String!
password: String!
role: String!
}
type TokenResult {
message: String
token: String
}
type Query {
users: [User]
}
type Mutation {
register(userInput: UserInput): User
login(name: String!, password: String!, role: String!): TokenResult
}
`;module.exports = typeDefs;
Création de résolveurs pour l’API GraphQL
Les fonctions de résolveur déterminent la manière dont les données sont récupérées en réponse aux requêtes et mutations des clients, ainsi qu’aux autres champs définis dans le schéma. Lorsqu’un client envoie une requête ou une mutation, le serveur GraphQL active les résolveurs correspondants afin de traiter et de renvoyer les données demandées, en provenance de sources diverses (bases de données, API, etc.).
Pour implémenter l’authentification et l’autorisation à l’aide de jetons web JSON (JWT), définissez des résolveurs pour les mutations d’inscription et de connexion. Ces résolveurs géreront respectivement les processus d’enregistrement et d’authentification des utilisateurs. Ensuite, créez un résolveur de requêtes de récupération de données, accessible uniquement aux utilisateurs authentifiés et autorisés.
Avant toute chose, définissez les fonctions nécessaires pour générer et vérifier les JWT. Dans le fichier `resolvers.js`, commencez par ajouter les importations suivantes :
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
Assurez-vous d’ajouter la clé secrète que vous utiliserez pour signer les jetons web JSON au fichier `.env` :
SECRET_KEY = '<ma_clé_secrète>';
Pour générer un jeton d’authentification, incluez la fonction suivante, qui spécifie des attributs uniques pour le jeton JWT, comme sa durée de validité. Vous pouvez également intégrer d’autres attributs tels que l’heure d’émission, en fonction des besoins spécifiques de votre application.
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);return token;
}
Implémentez maintenant la logique de vérification des jetons pour valider les JWT inclus dans les requêtes HTTP suivantes :
function verifyToken(token) {
if (!token) {
throw new Error('Jeton non fourni');
}try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
return decoded;
} catch (err) {
throw new Error('Jeton invalide');
}
}
Cette fonction prendra un jeton en entrée, vérifiera sa validité à l’aide de la clé secrète spécifiée et renverra le jeton décodé s’il est valide. Dans le cas contraire, elle générera une erreur signalant un jeton invalide.
Définition des résolveurs de l’API
Pour définir les résolveurs de l’API GraphQL, vous devez décrire les opérations spécifiques qu’elle gèrera, en l’occurrence les opérations d’enregistrement et de connexion des utilisateurs. Commencez par créer un objet résolveur qui contiendra les fonctions de résolveur, puis définissez les opérations de mutation suivantes :
const resolvers = {
Mutation: {
register: async (_, { userInput: { name, password, role } }) => {
if (!name || !password || !role) {
throw new Error('Nom, mot de passe et rôle requis');
}const newUser = new User({
name: name,
password: password,
role: role,
});try {
const response = await newUser.save();return {
id: response._id,
...response._doc,
};
} catch (error) {
console.error(error);
throw new Error('Échec de la création de l\'utilisateur');
}
},
login: async (_, { name, password }) => {
try {
const user = await User.findOne({ name: name });if (!user) {
throw new Error('Utilisateur introuvable');
}if (password !== user.password) {
throw new Error('Mot de passe incorrect');
}const token = generateToken(user);
if (!token) {
throw new Error('Échec de la génération du jeton');
}return {
message: 'Connexion réussie',
token: token,
};
} catch (error) {
console.error(error);
throw new Error('Échec de la connexion');
}
}
},
La mutation d’inscription gère le processus d’enregistrement en ajoutant les nouvelles données utilisateur à la base de données. Quant à la mutation de connexion, elle gère les connexions des utilisateurs et, en cas d’authentification réussie, elle génère un jeton JWT et renvoie un message de succès dans la réponse.
Maintenant, incluez le résolveur de requêtes pour récupérer les données utilisateur. Pour garantir que cette requête ne sera accessible qu’aux utilisateurs authentifiés et autorisés, intégrez une logique d’autorisation pour restreindre l’accès aux seuls utilisateurs ayant un rôle d’administrateur.
En pratique, la requête vérifiera d’abord la validité du jeton, puis le rôle de l’utilisateur. Si la vérification d’autorisation est concluante, la requête du résolveur procédera à la récupération et au renvoi des données des utilisateurs depuis la base de données.
Query: {
users: async (parent, args, context) => {
try {
const token = context.req.headers.authorization || '';
const decodedToken = verifyToken(token);if (decodedToken.role !== 'Admin') {
throw new ('Non autorisé. Seuls les administrateurs peuvent accéder à ces données.');
}const users = await User.find({}, { name: 1, _id: 1, role:1 });
return users;
} catch (error) {
console.error(error);
throw new Error('Échec de la récupération des utilisateurs');
}
},
},
};
Enfin, démarrez le serveur de développement :
node server.js
Voilà ! Vous pouvez maintenant tester les fonctionnalités de l’API à l’aide de l’interface de test Apollo Server, disponible dans votre navigateur. Par exemple, vous pouvez utiliser la mutation d’enregistrement pour ajouter de nouvelles données utilisateur dans la base de données, puis la mutation de connexion pour authentifier l’utilisateur.
Enfin, ajoutez le jeton JWT à la section d’en-tête d’autorisation et interrogez la base de données pour obtenir les données utilisateur.
Sécurisation des API GraphQL
L’authentification et l’autorisation sont des éléments essentiels pour la sécurisation des API GraphQL. Cependant, il est important de prendre conscience qu’elles peuvent ne pas suffire à elles seules à garantir une sécurité complète. Il est crucial de mettre en œuvre des mesures de sécurité supplémentaires, telles que la validation des entrées et le cryptage des données sensibles.
En adoptant une approche de sécurité globale, vous pouvez protéger vos API contre diverses attaques potentielles.