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 :
JwtAuthGuardvérifie la validité du token JWT et hydraterequest.userRolesGuardlit les métadonnées des décorateurs pour vérifier les permissions@Public()permet d'exclure des routes du guard global sans casser l'architecture@CurrentUser()offre un accès propre à l'utilisateur dans les contrôleurs
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.