SD
NestJSJWTSécuritéTypeScriptRBAC

Guards NestJS : sécurisez vos endpoints avec JWT et des rôles personnalisés

Apprenez à protéger vos APIs NestJS avec des guards JWT et un système de contrôle d'accès par rôles (RBAC). Guide complet avec exemples TypeScript.

AMAlexis Mouchon8 min de lecture

Guards NestJS : sécurisez vos endpoints avec JWT et des rôles personnalisés

Quand on construit une API avec NestJS, la question de la sécurité arrive très vite : comment protéger certaines routes ? Comment s'assurer qu'un utilisateur ne peut accéder qu'aux ressources qui lui appartiennent ? Et comment gérer des niveaux d'accès différents — administrateur, modérateur, utilisateur standard ?

NestJS propose une réponse élégante à travers le concept de guards. Dans cet article, on va voir comment implémenter un système complet d'authentification JWT couplé à un contrôle d'accès par rôles (RBAC), le tout de façon modulaire et réutilisable.

Qu'est-ce qu'un Guard dans NestJS ?

Un guard est une classe qui implémente l'interface CanActivate. Son rôle est simple : décider si une requête entrante doit être traitée ou rejetée, avant même d'atteindre le contrôleur.

Les guards s'exécutent après les middlewares mais avant les interceptors et les pipes. C'est le moment idéal pour vérifier l'identité d'un utilisateur et ses droits d'accès.

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'

@Injectable()
export class MonGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // Retourne true pour laisser passer, false pour bloquer
    return true
  }
}

La méthode canActivate peut retourner un booléen, une Promise<boolean> ou même un Observable<boolean>, ce qui la rend très flexible.

Mettre en place l'authentification JWT

Installation des dépendances

npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt

Configurer le module JWT

On commence par créer un module dédié à l'authentification :

// auth/auth.module.ts
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { AuthService } from './auth.service'
import { JwtStrategy } from './strategies/jwt.strategy'
import { JwtAuthGuard } from './guards/jwt-auth.guard'

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '7d' },
    }),
  ],
  providers: [AuthService, JwtStrategy, JwtAuthGuard],
  exports: [JwtAuthGuard, JwtModule],
})
export class AuthModule {}

⚠️ En production, utilisez JwtModule.registerAsync() pour charger le secret depuis un service de configuration dédié (ConfigService).

Créer la stratégie JWT

La stratégie définit comment le token est extrait et validé :

// auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { UsersService } from '../../users/users.service'

export interface JwtPayload {
  sub: string // userId
  email: string
  roles: string[]
}

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly usersService: UsersService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    })
  }

  async validate(payload: JwtPayload) {
    // Cette méthode est appelée après la vérification de la signature
    const user = await this.usersService.findById(payload.sub)
    if (!user) {
      throw new UnauthorizedException('Utilisateur introuvable')
    }
    return user // Sera disponible via @Req() ou @CurrentUser()
  }
}

Le guard JWT

// auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { AuthGuard } from '@nestjs/passport'
import { IS_PUBLIC_KEY } from '../decorators/public.decorator'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) {
    super()
  }

  canActivate(context: ExecutionContext) {
    // Vérifie si la route est marquée @Public()
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ])

    if (isPublic) {
      return true
    }

    return super.canActivate(context)
  }
}

On ajoute un décorateur @Public() pour marquer les routes accessibles sans authentification :

// auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common'

export const IS_PUBLIC_KEY = 'isPublic'
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true)

Implémenter le contrôle d'accès par rôles (RBAC)

L'authentification valide l'identité, mais le contrôle d'accès détermine ce qu'un utilisateur peut faire. On va créer un RolesGuard qui s'appuie sur des métadonnées déclarées directement sur les contrôleurs.

Définir les rôles

// auth/enums/role.enum.ts
export enum Role {
  USER = 'user',
  MODERATOR = 'moderator',
  ADMIN = 'admin',
}

Le décorateur @Roles()

// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
import { Role } from '../enums/role.enum'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles)

Le guard de rôles

// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Role } from '../enums/role.enum'
import { ROLES_KEY } from '../decorators/roles.decorator'

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

  canActivate(context: ExecutionContext): boolean {
    // Récupère les rôles requis depuis les métadonnées
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ])

    // Si aucun rôle n'est requis, on laisse passer
    if (!requiredRoles || requiredRoles.length === 0) {
      return true
    }

    const { user } = context.switchToHttp().getRequest()

    if (!user?.roles) {
      throw new ForbiddenException('Accès refusé : aucun rôle attribué')
    }

    const hasRole = requiredRoles.some((role) => user.roles.includes(role))

    if (!hasRole) {
      throw new ForbiddenException(`Accès refusé : rôle(s) requis — ${requiredRoles.join(', ')}`)
    }

    return true
  }
}

Utiliser les guards dans un contrôleur

On peut maintenant combiner les deux guards dans nos contrôleurs de façon très lisible :

// articles/articles.controller.ts
import { Controller, Get, Post, Delete, Param, Body, UseGuards } from '@nestjs/common'
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'
import { RolesGuard } from '../auth/guards/roles.guard'
import { Roles } from '../auth/decorators/roles.decorator'
import { Public } from '../auth/decorators/public.decorator'
import { Role } from '../auth/enums/role.enum'
import { CurrentUser } from '../auth/decorators/current-user.decorator'

@Controller('articles')
@UseGuards(JwtAuthGuard, RolesGuard) // Appliqué à tout le contrôleur
export class ArticlesController {
  @Get()
  @Public() // Route publique, pas besoin d'être connecté
  findAll() {
    return this.articlesService.findAll()
  }

  @Post()
  @Roles(Role.USER, Role.ADMIN) // Accessible aux utilisateurs et admins
  create(@Body() dto: CreateArticleDto, @CurrentUser() user: User) {
    return this.articlesService.create(dto, user)
  }

  @Delete(':id')
  @Roles(Role.ADMIN) // Réservé aux admins uniquement
  remove(@Param('id') id: string) {
    return this.articlesService.remove(id)
  }
}

Et voici le décorateur @CurrentUser() pour récupérer facilement l'utilisateur depuis la requête :

// auth/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'

export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest()
  return request.user
})

Enregistrer les guards globalement

Plutôt que d'ajouter @UseGuards() sur chaque contrôleur, on peut enregistrer JwtAuthGuard et RolesGuard globalement dans AppModule et n'utiliser @Public() que pour les exceptions :

// app.module.ts
import { APP_GUARD } from '@nestjs/core'
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard'
import { RolesGuard } from './auth/guards/roles.guard'

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

Cette approche "sécurisé par défaut" est recommandée : toutes les routes sont protégées et on déclare explicitement les exceptions publiques. C'est bien plus sûr qu'oublier d'ajouter un guard sur une route sensible.

En résumé

Les guards NestJS sont un outil puissant pour centraliser la logique de sécurité de vos APIs :

Combiner ces éléments donne une architecture de sécurité claire, testable et facilement extensible — par exemple pour ajouter de l'ABAC (Attribute-Based Access Control) ou des permissions granulaires par ressource.


Tu travailles sur une API NestJS et tu veux aller plus loin dans la sécurisation de tes endpoints, ou intégrer ce système à une stack GraphQL avec des guards Apollo ? N'hésite pas à me contacter — c'est exactement le genre de sujet que j'aime creuser.