Construire Une API RESTful Sécurisée Avec Fastify Et JWT En TypeScript

Une API RESTful bien conçue doit protéger ses ressources, valider les données entrantes et rester suffisamment rapide pour accompagner la croissance d’une application. Fastify répond efficacement à ces exigences grâce à son architecture légère, son système de plugins et son intégration avec les schémas JSON.

L’ajout de JSON Web Tokens permet ensuite de gérer une authentification sans état côté serveur. Le client reçoit un jeton signé après connexion, puis le transmet dans l’en-tête Authorization pour accéder aux routes privées. TypeScript complète l’ensemble en sécurisant les contrats entre les contrôleurs, les services et les données manipulées.

L’objectif est de construire une base réaliste : une API Node.js modulaire, protégée contre les entrées invalides, les secrets mal gérés et les permissions excessives. L’organisation du code peut aussi s’inspirer d’une architecture propre Node.js, particulièrement utile lorsque le projet dépasse quelques routes.

Préparer Le Projet Fastify Et TypeScript

Commencez par installer Fastify, son adaptateur JWT, les outils TypeScript et une bibliothèque de hachage comme argon2. Une structure simple peut séparer les routes, les contrôleurs, les cas d’usage, les plugins et les types :

src/
  app.ts
  server.ts
  plugins/
    auth.ts
    env.ts
  routes/
    auth.routes.ts
    users.routes.ts
  services/
    auth.service.ts

Le fichier d’application doit créer une instance Fastify et enregistrer les plugins avant les routes. Le plugin JWT peut être configuré avec une clé provenant exclusivement des variables d’environnement. En développement, un fichier .env est pratique, mais il ne doit jamais être ajouté au dépôt Git.

import Fastify from "fastify";
import jwt from "@fastify/jwt";

export function buildApp() {
  const app = Fastify({ logger: true });

  app.register(jwt, {
    secret: process.env.JWT_SECRET as string,
    sign: { expiresIn: "15m" }
  });

  return app;
}

Dans une application destinée à la production, vérifiez que JWT_SECRET existe au démarrage et qu’il possède une entropie suffisante. Un gestionnaire de secrets fourni par le cloud ou l’orchestrateur est préférable à une valeur écrite dans le code. Ajoutez également une gestion centralisée des erreurs afin de ne pas exposer les traces internes aux clients.

Définir L’Authentification Et Les Jetons

La route POST /auth/login doit rechercher l’utilisateur, comparer le mot de passe fourni avec son empreinte Argon2, puis générer un JWT contenant uniquement les informations nécessaires. Un identifiant utilisateur et un rôle peuvent suffire. Évitez d’insérer l’adresse complète, le mot de passe ou des données personnelles dans le payload : le contenu d’un JWT est encodé, mais pas chiffré.

type TokenPayload = {
  sub: string;
  role: "user" | "admin";
};

app.post("/auth/login", async (request, reply) => {
  const { email, password } = request.body as {
    email: string;
    password: string;
  };

  const user = await findUserByEmail(email);
  if (!user || !(await argon2.verify(user.passwordHash, password))) {
    return reply.code(401).send({ message: "Identifiants invalides" });
  }

  const token = app.jwt.sign({
    sub: user.id,
    role: user.role
  } satisfies TokenPayload);

  return reply.send({ accessToken: token });
});

La durée de vie courte réduit l’impact d’un jeton volé. Pour des sessions persistantes, utilisez un mécanisme de renouvellement séparé : un refresh token stocké de façon sécurisée, révocable et associé à une session en base. Le stockage dans une cookie HttpOnly, Secure et SameSite limite l’exposition au JavaScript du navigateur, tandis qu’une application mobile peut employer un stockage sécurisé du système.

Ajoutez une validation stricte du format d’adresse électronique, de la longueur du mot de passe et du corps de requête. Les réponses d’échec doivent rester suffisamment génériques pour éviter de révéler si une adresse existe dans la base.

Protéger Les Routes Avec Un Hook

Fastify permet de créer une fonction réutilisable qui vérifie la présence et la validité du jeton. request.jwtVerify() contrôle la signature, l’expiration et la structure générale du JWT. Le résultat peut être typé pour que TypeScript reconnaisse request.user dans les contrôleurs.

declare module "@fastify/jwt" {
  interface FastifyJWT {
    payload: TokenPayload;
    user: TokenPayload;
  }
}

app.decorate("authenticate", async function (request, reply) {
  try {
    await request.jwtVerify();
  } catch {
    return reply.code(401).send({ message: "Authentification requise" });
  }
});

app.get("/profile", {
  onRequest: [app.authenticate]
}, async (request) => {
  return { userId: request.user.sub };
});

L’authentification et l’autorisation sont deux contrôles différents. Le premier confirme l’identité du demandeur ; le second vérifie qu’il possède le droit d’exécuter une action. Une route de suppression pourra donc exiger un utilisateur connecté, puis contrôler son rôle avant d’appeler le service concerné.

app.delete("/users/:id", {
  onRequest: [app.authenticate]
}, async (request, reply) => {
  if (request.user.role !== "admin") {
    return reply.code(403).send({ message: "Accès interdit" });
  }

  return reply.code(204).send();
});

Ne faites jamais confiance à un identifiant envoyé par le client pour déterminer ses droits. Le serveur doit utiliser le sub signé dans le jeton, puis vérifier les règles métier en base. Pour les permissions complexes, une politique dédiée est plus lisible qu’une succession de conditions dans chaque route.

Valider Les Entrées Et Les Réponses

Les schémas Fastify décrivent les paramètres, les requêtes et les réponses attendues. Ils améliorent les performances grâce à la compilation de la validation et produisent une documentation OpenAPI lorsqu’ils sont associés aux plugins appropriés. Ils empêchent aussi qu’un champ inattendu atteigne directement la couche de persistance.

const userSchema = {
  body: {
    type: "object",
    required: ["email", "password"],
    additionalProperties: false,
    properties: {
      email: { type: "string", format: "email" },
      password: { type: "string", minLength: 12 }
    }
  },
  response: {
    201: {
      type: "object",
      properties: {
        id: { type: "string" },
        email: { type: "string" }
      }
    }
  }
} as const;

La validation doit couvrir les paramètres d’URL, les filtres, la pagination et les corps JSON. Limitez également la taille des requêtes, normalisez les valeurs lorsque c’est nécessaire et encodez correctement les données affichées dans un contexte HTML. Une API REST n’est pas automatiquement protégée contre l’injection SQL : les requêtes paramétrées et un ORM correctement configuré restent indispensables.

La couche de données mérite une décision adaptée au volume, aux relations et aux exigences opérationnelles. Une comparaison entre DynamoDB et Firestore peut aider à évaluer les compromis entre modèle de données, indexation, coûts et cohérence avant de figer les services applicatifs.

Renforcer La Sécurité En Production

Le chiffrement TLS doit être terminé par le serveur ou par un proxy de confiance. Ajoutez une limitation du nombre de tentatives sur la connexion, des en-têtes HTTP de sécurité et une journalisation sans informations sensibles. Les logs doivent pouvoir relier une requête à un identifiant de corrélation, sans enregistrer le mot de passe ni le JWT complet.

Contrôlez les dépendances avec un outil d’audit, appliquez rapidement les correctifs et verrouillez les versions dans le fichier de dépendances. Les tests doivent couvrir les scénarios d’accès sans jeton, de jeton expiré, de rôle insuffisant, de données invalides et de tentative de lecture d’une ressource appartenant à un autre utilisateur.

Contrôles À Vérifier Avant Le Déploiement

Erreurs Courantes À Éviter

L’intelligence artificielle peut accélérer l’écriture de schémas ou de tests, à condition de vérifier chaque résultat. Des ressources sur l’IA pour générer du code sont utiles pour produire des variantes, mais un extrait généré ne doit jamais décider seul d’une politique de sécurité.

Une API Fastify sécurisée repose finalement sur plusieurs couches cohérentes : configuration maîtrisée, mots de passe correctement protégés, JWT à durée limitée, validation déclarative, permissions explicites et tests automatisés. Commencez par un petit domaine métier, ajoutez ces garde-fous dès les premières routes, puis mesurez les performances et les erreurs avant d’élargir l’architecture.