Développement

Améliorez vos endpoints API - Découvrez les Décorateurs et Guards NestJS

Boostez la sécurité et la lisibilité de vos endpoints API avec NestJS ! Découvrez comment utiliser les Décorateurs et Guards pour simplifier la gestion des autorisations et simplifier votre code.

Par

Intro

Lorsque vous développez des APIs, il est essentiel de suivre certaines règles pour garantir que les endpoints ne soient pas accessibles par des utilisateurs non autorisés. Plusieurs approches sont possibles :

  • Vérifier manuellement les autorisations nécessaires au début de chaque endpoint.
  • Créer des guards et utiliser des custom decorators pour rendre ces vérifications répétitives plus lisibles et moins verbeuses.

Quelques explications

Un Guard dans NestJS c’est une classe qui implémente l'interface CanActivate avec une méthode appelée canActivate(context: ExecutionContext). Cette méthode reçoit le contexte d'exécution comme paramètre et retourne un booléen indiquant si l'accès au contrôleur ou endpoint est autorisé.

Un Custom Decorator dans NestJS est une expression qui retourne une fonction et peut recevoir des arguments comme une cible (target), un nom et un descripteur de propriété (property descriptor). Vous appliquez un decorator en le préfixant avec le caractère @ et en le plaçant au-dessus de ce que vous souhaitez décorer.

Les Decorators dans NestJS peuvent être appliqué sur une classe, une méthode ou même une propriété. NestJS met à disposition des Decorators comme @Param(myParam) pour récupérer un paramètre d'une requête URL par exemple. Vous pourrez trouver la liste complète des Decorators disponibles juste ici.

Un Guard  est un Custom Decorator qui peut être utilisé pour protéger un contrôleur ou un endpoint spécifique. Voici un exemple d'utilisateur essayant d'accéder à un module additionnel qu'il n'a pas payé.

Commençons d’abord par créer un auth guard pour injecter les informations de l'utilisateur dans la requête.

Nous supposons que l'objet utilisateur (user) contient une propriété nommée modules, qui est une liste des modules auxquels l'utilisateur a accès.

Pour associer certains endpoints à un module spécifique, nous allons créer un decorator @UserModule permettant de spécifier à quel module un contrôleur ou endpoint est lié.

Le code de ce décorateur est assez simple. On utilise la méthode createDecorator de Reflector pour ajouter une metadata au contrôleur auquel il sera assigné pour spécifier si les endpoints de ce contrôleur seront protégés par notre guard ou non.

user-modules.decorator.ts

import { SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

// Définition des types de modules
export type UserModule = 'management' | 'subscriptions' | 'roles';

// Création du decorator
export const UserModules = (modules: UserModule | UserModule[]) => SetMetadata('user-modules', modules);

// Ces 2 lignes sont identiques d'un point de vue fonctionnel, la seule chose qui change sera la façon de déclarer et de récupérer la valeur

export const UserModules = (modules: UserModule | UserModule[]) =>
  Reflector.createDecorator<UserModule[]>('user-modules', modules);

Une fois que le décorateur est créé, nous devons créer le guard associé.

En ce qui concerne le code du guard, c’est relativement simple également. Nous allons récupérer les rôles qui sont injectés dans le décorateur et vérifier que l’utilisateur qui essaye d’accéder à la ressource possède les bons rôles.

user-modules.guard.ts

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserModules } from '[votre-architecture]/user-modules';

@Injectable()
export class UserModulesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // Si vous avez choisi SetMetadata
	const modules = this.reflector.getAllAndOverride('user-modules', [context.getHandler(), context.getClass()]);
	// Si vous avez choisi Reflector.createDecorator
    const modules = this.reflector.getAllAndOverride<UserModule[]>('user-modules', [
      context.getHandler(),
      context.getClass(),
    ]);

		// Si aucun module n'a été injecté, alors on laisse passer par défaut
    if (!modules) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const userModules = user.modules;

    const canAccess = Array.isArray(modules)
      ? modules.every((module) => userModules.includes(module))
      : userModules.includes(modules);

    if (!canAccess) {
      throw new UnauthorizedException("Vous ne possédez pas les droits nécessaires pour accéder à ce module.");
    }

    return true;
  }
}

N'oubliez pas de déclarer le guard dans le module concerné. Vous pouvez l'appliquer à l'échelle d'un contrôleur, d'une méthode ou globalement.

Application au niveau du contrôleur

Ajoutez @UseGuards(UserModulesGuard) au-dessus du contrôleur :

@UseGuards(UserModulesGuard)
export class YourController { ... }

Application globale

Ajoutez le guard dans module App :

app.module.ts

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { UserModulesGuard } from '[votre-architecture]/user-modules.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: UserModulesGuard,
    },
  ],
})
export class AppModule{}

Vous pouvez maintenant utiliser le decorator pour spécifier les modules nécessaires pour accéder à un endpoint :

@UserModules('subscriptions')
public getSubscriptionsList() { ... }

Et voilà ! Vous avez maintenant des endpoints sécurisés qui vérifient automatiquement si un utilisateur dispose des permissions nécessaires pour accéder aux données. N'hésitez pas à ajuster le code pour répondre à vos besoins spécifiques.

Les autres articles à explorer