Accueil / Articles

Contrôle d’accès basé sur les rôles dans Express avec JWT et deux middlewares

Apprenez à mettre en œuvre un accès basé sur les rôles dans une API Express en associant le middleware d’authentification JWT à un mécanisme authorize() réutilisable, ainsi que le moment de renvoyer une réponse 401 ou 403.

Éditeur : REACTAPP.TOP4 min de lecture1024 mots

Sources: What is RBAC in Node.js?

Dans la plupart des backends, différents utilisateurs ont des droits distincts : un administrateur peut supprimer des comptes, un gestionnaire peut les modifier, tandis qu’un utilisateur ordinaire ne peut que lire. La dispersion de vérifications du type if (user.role === ...) dans les contrôleurs devient rapidement difficile à maintenir et sujette aux erreurs. Ce guide montre comment exprimer ces règles sous forme de contrôle d’accès basé sur les rôles (RBAC) dans Express à l’aide de deux petits middlewares, ainsi que comment choisir le bon code d’état lorsqu’une demande est refusée.

Une matrice de permissions typique ressemble à ceci :

Admin   → Create, Read, Update, Delete
Manager → Read, Update
User    → Read

Comment le RBAC s’intègre dans le pipeline des requêtes

Le RBAC est un modèle d’autorisation : l’accès à une extrémité est accordé ou refusé en fonction du rôle associé à l’utilisateur. Dans une API Node.js basée sur JWT, les éléments s’organisent dans un ordre fixe :

Login
  ↓
JWT Token
  ↓
Authentication Middleware
  ↓
Check User Role
  ↓
Authorization Middleware
  ↓
Controller

Deux questions distinctes sont abordées au cours de cet exposé. L’authentification permet de déterminer qui est l’appelant, tandis que l’autorisation décide de ce que cet appelant peut faire. En les plaçant dans des middlewares séparés, on rend chacun d’eux simple et testable. Cette distinction est traitée plus en détail dans l’endroit où se situent l’authentification et l’autorisation dans votre code.

Mappage des rôles sur les points de terminaison

Considérons un ensemble standard de routes de gestion des utilisateurs :

GET    /api/users
POST   /api/users
PUT    /api/users/:id
DELETE /api/users/:id

La politique souhaitée pour elles :

Admin   → All APIs
Manager → GET, PUT
User    → GET only

Au lieu de vérifier à nouveau le rôle dans chaque contrôleur, la politique peut être associée de manière déclarative à chaque route.

Inclusion du rôle dans le token

Chaque enregistrement d’utilisateur contient un rôle :

{
  name: "Ash",
  email: "ash@example.com",
  role: "admin"
}

Lors de la connexion, le rôle est inclus dans le JWT avec l’ID utilisateur, ce qui permet d’autoriser les demandes ultérieures sans recherche dans la base de données :

const token = jwt.sign(
  {
    userId: user._id,
    role: user.role
  },
  process.env.JWT_SECRET,
  { expiresIn: "1h" }
);

Le compromis : le rôle reste figé dans le jeton jusqu’à son expiration. Si un administrateur est dégradé, l’ancien jeton indique toujours admin pendant jusqu’une heure. Des durées d’expiration courtes réduisent cette fenêtre de temps ; pour des opérations sensibles, il est plus sûr de relire le rôle depuis la base de données.

Authentification de la demande

Le premier middleware extrait le jeton de l’en-tête Authorization: Bearer <token>, vérifie sa signature et son expiration, puis stocke la charge utile décodée dans req.user. Les jetons manquants ou invalides entraînent l’échec de la demande avec un code 401 :

const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
if (!token) {
    return res.status(401).json({
      message: "Authentication required"
    });
  }
  try {
    const decoded = jwt.verify(
      token,
      process.env.JWT_SECRET
    );
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({
      message: "Invalid or expired token"
    });
  }
};

Une fois exécuté, chaque gestionnaire ultérieur peut faire confiance à req.user. Lors de la vérification, il est également bon de spécifier les algorithmes acceptés via l’option algorithms de jwt.verify au lieu d’accepter tout ce que l’en-tête du token indique.

Autorisation par rôle

Le deuxième middleware est une usine. Il prend les rôles autorisés et renvoie un gestionnaire qui rejette toute personne dont le rôle n’apparaît pas dans la liste en renvoyant une erreur 403 :

const authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        message: "Access denied"
      });
    }
next();
  };
};

Notez qu’il lit req.user.role, il doit donc toujours s’exécuter après authenticate ; sinon, il générerait une erreur en raison de req.user non défini.

Protéger une route consiste désormais à combiner ces deux éléments :

router.delete(
  "/users/:id",
  authenticate,
  authorize("admin"),
  deleteUser
);

Seul un administrateur authentifié peut accéder à deleteUser. Permettre plusieurs rôles signifie simplement passer davantage d’arguments :

router.put(
  "/users/:id",
  authenticate,
  authorize("admin", "manager"),
  updateUser
);

Ici, les administrateurs et les gestionnaires peuvent tous deux mettre à jour les utilisateurs.

Choix entre 401 et 403

Ces deux codes sont souvent confondus, mais ils indiquent des problèmes différents.

401 : l’appelant n’est pas authentifié

Le serveur ne sait pas qui effectue l’appel, généralement pour l’une des raisons suivantes :

No token
Invalid token
Expired token

Le client doit se connecter à nouveau ou renouveler son jeton.

403 : l’appelant est identifié mais n’a pas les droits

L’identité a été établie, mais le rôle associé ne permet pas l’accès :

User role = user
Required role = admin

Essayer à nouveau avec les mêmes identifiants ne servira à rien. Règle simple : 401 signifie « nous ne savons pas qui vous êtes » ; 403 signifie « nous le savons, et la réponse est non ».

Aperçu rapide

  • Qu’est-ce que le RBAC ? Un contrôle d’accès où les permissions API proviennent du rôle attribué à l’appelant.
  • Comment est-ce implémenté dans Express ? Un middleware vérifie le JWT, un second contrôle le rôle par rapport à la liste des rôles autorisés pour cette route.
  • Où doit-ce être appliqué ? Au backend. Cacher des boutons dans l’interface utilisateur n’a qu’un effet esthétique, car n’importe qui peut appeler directement l’API.
  • Pourquoi utiliser des middleware ? Ils permettent de réutiliser les mécanismes d’autorisation en les séparant de la logique métier, de sorte qu’un seul mécanisme de protection peut servir pour plusieurs routes.
  • Qu’est-ce que RBAC ne couvre pas ? Les règles de propriété, comme « les utilisateurs ne peuvent modifier que leur propre profil », nécessitent une vérification supplémentaire directement sur la ressource elle-même.
  • En résumé

    Le flux global est simple :

    Client
      ↓
    JWT
      ↓
    Authentication
      ↓
    Authorization / RBAC
      ↓
    Controller
      ↓
    Database
    

    Authentifiez l’appelant, lisez son rôle, comparez-le avec ce que la route autorise, puis autorisez ou refusez l’accès. À mesure que l’application se développe, envisagez de relier les rôles à des permissions plus détaillées telles que user:create, user:update et user:delete, et de vérifier ces permissions directement dans les routes au lieu des noms de rôles. Ajouter un nouveau rôle signifie alors modifier une seule correspondance plutôt que toutes les routes.