Cours NestJS · Module 2: Routing et cycle de requête

Guards - qui franchit la porte

6 min de lecture
Dans cette leçon6

La sentinelle du poste avancé compte tous ceux qui entrent et note l'heure. Elle fait de même pour un marchand, un courrier et un légionnaire - car au poste avancé, on ignore encore où chacun se rend. C'est seulement à la porte du trésor que se tient quelqu'un qui sait à quelle salle frappe le visiteur, et qui peut donc dire « pas toi ».

Voilà toute la différence entre un middleware et un guard, et cette leçon va la refermer.

Dans la leçon précédente, nous avons rattaché le middleware dans un module via configure(consumer) et .forRoutes('tributes'). Note l'argument 'tributes' : le middleware reçoit un motif de chemin, non le nom d'un contrôleur ou d'une méthode - et c'est le premier indice de ce qui lui manque.

Ce que le middleware ignore

Le middleware s'exécute avant le guard et ne connaît pas la route cible. Ce n'est pas une négligence des auteurs du framework, mais une conséquence du moment : le middleware agit au niveau d'Express, avant que NestJS n'ait déterminé quel contrôleur et quelle méthode traiteront la requête. Il reçoit req, res et next - trois objets HTTP, et rien de plus.

Le middleware convient donc parfaitement à ce qui ne dépend pas de la destination : la journalisation, les en-têtes CORS, l'analyse des cookies. Il ne convient pas aux décisions du genre « cet endpoint exige un rôle d'administrateur », car à l'instant où il agit, la notion de « cet endpoint » n'existe pas encore.

CanActivate - l'interface de la sentinelle

Un guard est une classe implémentant l'interface CanActivate. Non NestMiddleware - celle-ci appartient au middleware de la leçon précédente ; non NestInterceptor - ce sont les interceptors, qui viennent ensuite ; non PipeTransform - ce sont les pipes, plus loin encore. Chacun des quatre mécanismes du cycle de requête a sa propre interface et son unique méthode :

1@Injectable()
2export class AuthGuard implements CanActivate {
3  canActivate(context: ExecutionContext): boolean {
4    const request = context.switchToHttp().getRequest();
5    const token = request.headers.authorization;
6
7    return !!token;
8  }
9}

Le corps de la méthode se lit en quatre temps, toujours dans cet ordre : canActivate(context: ExecutionContext) { reçoit le contexte, const request = context.switchToHttp().getRequest(); en extrait l'objet de requête, const token = request.headers.authorization; va chercher l'en-tête, et return !!token; le convertit en décision.

Cette décision est le booléen true lorsque l'accès doit être accordé - non la chaîne 'allowed', non un objet avec des rôles, non null. Le double point d'exclamation de !!token fait précisément cela : il change « l'en-tête est là ou il n'y est pas » en true ou false. Quand false revient, NestJS rejette la requête par un 403 Forbidden, et la méthode du contrôleur ne s'exécute pas du tout.

ExecutionContext - ce qui manquait

L'argument context est la raison pour laquelle un guard peut plus qu'un middleware. ExecutionContext fournit des informations sur le contrôleur et la méthode cibles (le handler) - non l'accès à la base de données, non l'accès au système de fichiers, non la configuration du serveur. Ces choses-là s'obtiennent autrement : la base par l'injection d'un dépôt, la configuration par ConfigService.

Deux méthodes suffisent pour commencer. context.switchToHttp().getRequest() descend à la couche HTTP chercher l'objet de requête familier - « switch », parce que NestJS sert aussi les WebSockets et les microservices, et que le contexte leur est commun à tous. context.getHandler() renvoie la méthode du contrôleur elle-même, celle qui doit traiter la requête, et context.getClass() renvoie la classe du contrôleur.

Et c'est exactement ce dont le middleware est dépourvu. Un guard, tenant le handler, peut demander : « qu'exige cette méthode-ci ? ».

Le branchement d'un guard

Un guard se branche par le décorateur @UseGuards - sur une seule méthode ou sur tout un contrôleur :

1@Controller('treasury')
2@UseGuards(AuthGuard)
3export class TreasuryController {
4  @Get()
5  findAll() {
6    return this.treasuryService.findAll();
7  }
8}

L'écriture comporte trois parties : @UseGuards( ouvre le décorateur, AuthGuard nomme la classe de la sentinelle - la classe, non son instance, car la création revient à l'injection de dépendances - et ) referme. On peut en énumérer plusieurs, séparés par des virgules ; ils sont vérifiés tour à tour, et un seul false suffit pour que la requête tombe.

Quand une sentinelle doit veiller sur toute l'application, tu l'enregistres globalement : app.useGlobalGuards(new AuthGuard()) dans main.ts, ou comme provider avec le token APP_GUARD. La seconde voie vaut mieux dès que le guard a lui-même besoin de quelque chose - un provider passe par l'injection de dépendances, new non.

Reflector - un guard qui lit les exigences

Puisqu'un guard connaît le handler, il peut lire les exigences inscrites auprès de lui. Les exigences s'épinglent par SetMetadata et se relisent par la classe Reflector :

1export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
2
3@Injectable()
4export class RolesGuard implements CanActivate {
5  constructor(private reflector: Reflector) {}
6
7  canActivate(context: ExecutionContext): boolean {
8    const requiredRoles = this.reflector.get<string[]>(
9      'roles',
10      context.getHandler(),
11    );
12
13    if (!requiredRoles) {
14      return true;
15    }
16
17    const request = context.switchToHttp().getRequest();
18
19    return requiredRoles.includes(request.user?.role);
20  }
21}

SetMetadata('roles', roles) colle sur la méthode une étiquette sous la clé 'roles'. this.reflector.get<string[]>('roles', context.getHandler()) la relit - le premier argument est la clé, le second l'endroit où chercher, à savoir notre handler. Pas d'étiquette signifie un endpoint sans exigences : nous renvoyons donc true et laissons passer.

Un seul guard dessert ainsi toute l'application : @Roles('senator') au-dessus d'une méthode, @Roles('centurion', 'tribune') au-dessus d'une autre, et la logique de comparaison écrite une fois pour toutes. C'est le même SetMetadata que nous retrouverons avec les décorateurs personnalisés - tu y verras comment replier @Roles et @UseGuards en un seul sceau.

Récapitulatif

Le poste avancé compte les entrants, la sentinelle du trésor décide :

  • le middleware s'enregistre dans un module implémentant NestModule : configure(consumer: MiddlewareConsumer) { → consumer.apply(LoggerMiddleware) → .forRoutes('tributes') → },
  • le middleware s'exécute avant le guard et ne connaît pas la route cible - il agit avant que NestJS n'ait déterminé quel handler servira la requête,
  • un guard implémente l'interface CanActivate - non NestMiddleware, non NestInterceptor, non PipeTransform,
  • canActivate() renvoie true pour autoriser l'accès - non la chaîne 'allowed', non un objet avec des rôles, non null ; false donne un 403 et le handler ne s'exécute pas,
  • l'ordre dans la méthode : canActivate(context: ExecutionContext) { → const request = context.switchToHttp().getRequest(); → const token = request.headers.authorization; → return !!token;,
  • ExecutionContext fournit des informations sur le contrôleur et la méthode cibles (le handler) - non la base de données, non le système de fichiers, non la configuration du serveur,
  • context.getHandler() renvoie la méthode, context.getClass() le contrôleur, switchToHttp().getRequest() l'objet de requête,
  • le branchement : @UseGuards( → AuthGuard → ), globalement par useGlobalGuards ou le token APP_GUARD,
  • SetMetadata('roles', roles) inscrit les exigences auprès du handler, et Reflector les relit : this.reflector.get<string[]>('roles', context.getHandler()).

Dans la prochaine leçon, tu découvriras les interceptors - le troisième mécanisme du cycle, et le premier à toucher la réponse et non la seule requête. En attendant, retiens la différence : le middleware demande « qu'est-ce qui arrive », le guard demande « où cela va-t-il, et est-ce permis ».

Code de cette leçon : src/guards.ts
1// Guards dans NestJS - Les gardiens d'élite du trésor
2import {
3  Injectable, CanActivate, ExecutionContext,
4  SetMetadata, UnauthorizedException, ForbiddenException,
5} from '@nestjs/common';
6import { Reflector } from '@nestjs/core';
7
8console.log("Guards - gardiens d'élite qui décident de l'accès !");
9
10// ===========================================
11// 1. AuthGuard simple
12// ===========================================
13
14@Injectable()
15export class RomanAuthGuard implements CanActivate {
16  canActivate(context: ExecutionContext): boolean {
17    const request = context.switchToHttp().getRequest();
18    const authHeader = request.headers['authorization'];
19
20    if (!authHeader || !authHeader.startsWith('Bearer ')) {
21      throw new UnauthorizedException("Token d'autorisation manquant !");
22    }
23
24    const token = authHeader.split(' ')[1];
25
26    // Dans une vraie application : vérification JWT
27    if (token !== 'roman-secret-token') {
28      throw new UnauthorizedException('Token invalide !');
29    }
30
31    // Ajouter les données de l'utilisateur à la requête
32    request.user = { id: 1, name: 'Marcus', roles: ['Centurio'] };
33    return true;
34  }
35}
36
37// ===========================================
38// 2. RolesGuard avec métadonnées
39// ===========================================
40
41export const ROLES_KEY = 'roles';
42export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
43
44@Injectable()
45export class RolesGuard implements CanActivate {
46  constructor(private reflector: Reflector) {}
47
48  canActivate(context: ExecutionContext): boolean {
49    const requiredRoles = this.reflector.get<string[]>(
50      ROLES_KEY,
51      context.getHandler(),
52    );
53
54    // Aucun rôle requis - autoriser l'accès
55    if (!requiredRoles || requiredRoles.length === 0) {
56      return true;
57    }
58
59    const request = context.switchToHttp().getRequest();
60    const user = request.user;
61
62    if (!user) {
63      throw new ForbiddenException('Utilisateur non connecté !');
64    }
65
66    const hasRole = requiredRoles.some(role => user.roles?.includes(role));
67
68    if (!hasRole) {
69      throw new ForbiddenException('Rang requis manquant : ' + requiredRoles.join(', '));
70    }
71
72    return true;
73  }
74}
75
76// ===========================================
77// 3. Utilisation dans le contrôleur
78// ===========================================
79
80// @Controller('senate')
81// @UseGuards(RomanAuthGuard, RolesGuard)
82// export class SenateController {
83//   @Roles('Senator', 'Consul')
84//   @Get('secret-decrees')
85//   getSecretDecrees() {
86//     return { decrees: ['Décret secret...'] };
87//   }
88// }
89
90console.log("\n=== RÉSUMÉ SUR LES GUARDS ===");
91console.log("CanActivate - interface des guards");
92console.log("canActivate() retourne true/false");
93console.log("@SetMetadata - définition de métadonnées (par ex. rôles)");
94console.log("Reflector - lecture des métadonnées");
95console.log("@UseGuards() - application d'un guard sur un contrôleur/méthode");
96

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. En quoi un middleware diffère-t-il d'un guard dans NestJS ?

  2. 2. Quelle interface un guard implémente-t-il dans NestJS ?

  3. 3. Que retourne la méthode canActivate() d'un guard pour autoriser l'accès à un endpoint ?

  4. 4. Que fournit ExecutionContext dans un guard, et que le middleware n'a pas ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Range les éléments de configuration du middleware dans le bon ordre :

  • Éditeur de code

    Implémente NestModule dans AppModule avec une méthode configure() qui enregistre LoggerMiddleware pour les routes 'tributes'

  • Éditeur de code

    Écris un AuthGuard implémentant CanActivate qui vérifie la présence d'un token dans l'en-tête authorization de la requête

  • Tri horizontal

    Range les éléments de l'utilisation du décorateur @UseGuards sur un contrôleur :

  • Clique dans l'ordre

    Range les éléments de l'implémentation du guard d'autorisation dans le bon ordre :

  • Éditeur de code

    Écris un RolesGuard qui utilise Reflector pour lire les rôles définis par SetMetadata et les comparer au rôle de l'utilisateur