Lorsqu'on sécurise une API NestJS avec un guard JWT enregistré globalement, toutes les routes deviennent par défaut protégées. C'est une excellente pratique de sécurité (secure by default), mais cela pose immédiatement un problème : comment exposer publiquement les endpoints qui doivent rester accessibles sans authentification, comme /auth/login, /auth/register, ou encore certaines pages de contenu public ?
Dans ce tutoriel, nous allons construire et utiliser un décorateur @Public() qui permet de marquer explicitement les routes ouvertes, tout en conservant un guard JWT global. Cette technique repose sur les métadonnées de NestJS et sur l'API Reflector.
Le problème : un guard global trop strict
Dans une application NestJS bien sécurisée, on enregistre généralement le guard d'authentification au niveau global, par exemple dans AppModule :
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
}Avantage : aucune route n'est oubliée. Inconvénient : absolument toutes les routes exigent désormais un token JWT valide, y compris celles dont le rôle est précisément… de fournir ce token (login, register).
Plusieurs approches existent pour résoudre ce problème :
- Ne pas utiliser de guard global et l'appliquer manuellement avec
@UseGuards()sur chaque controller protégé. - Maintenir une liste de chemins exclus dans le guard.
- Utiliser des métadonnées pour marquer explicitement les routes publiques.
La troisième solution est la plus élégante et la plus idiomatique en NestJS. C'est celle que nous allons mettre en œuvre.
Créer le décorateur @Public()
Le principe est simple : nous allons définir un décorateur personnalisé qui attache une métadonnée booléenne au handler ou au controller. Le guard pourra ensuite lire cette métadonnée pour décider s'il doit valider le token ou laisser passer la requête.
Créons le fichier src/common/decorators/public.decorator.ts :
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);Décortiquons ce code :
SetMetadataest une factory fournie par NestJS qui crée un décorateur capable d'attacher une paire clé/valeur à la cible (méthode ou classe).IS_PUBLIC_KEYest une constante exportée. L'externaliser dans une constante évite les fautes de frappe et garantit que le décorateur et le guard utilisent rigoureusement la même clé.Publicest une simple fonction qui retourne le décorateur configuré avec la valeurtrue.
À l'usage, cela ressemble à un décorateur natif : @Public(). Mais sous le capot, il ne fait qu'écrire la métadonnée isPublic = true dans le système de réflexion de NestJS.
Adapter le JwtAuthGuard
Le décorateur seul ne sert à rien : il faut maintenant que le guard lise cette métadonnée et adapte son comportement. C'est ici qu'intervient l'API Reflector de NestJS.
Voici notre JwtAuthGuard mis à jour :
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../../modules/auth/auth.service';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private jwtService: JwtService,
private configService: ConfigService,
private authService: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. Vérifier si la route est marquée comme publique
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
// 2. Sinon, valider le token JWT
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const user = await this.authService.validateToken(token);
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}Le point central de cette implémentation est l'appel à reflector.getAllAndOverride. Cette méthode mérite qu'on s'y attarde.
Pourquoi getAllAndOverride ?
L'API Reflector propose plusieurs méthodes pour lire les métadonnées :
get(): lit la métadonnée sur une seule cible.getAll(): retourne un tableau avec les valeurs de toutes les cibles.getAllAndOverride(): parcourt les cibles dans l'ordre fourni et retourne la première valeur trouvée.getAllAndMerge(): fusionne toutes les valeurs (utile pour les tableaux comme@Roles).
Ici, nous passons [context.getHandler(), context.getClass()]. Cela signifie : « Regarde d'abord la méthode décorée ; si elle ne porte pas la métadonnée, regarde alors la classe (le controller) ». Cette priorité est importante car elle permet une utilisation flexible :
- Marquer un endpoint précis comme public, même dans un controller protégé.
- Marquer un controller entier comme public, sans avoir à répéter le décorateur sur chaque méthode.
Utilisation concrète dans un controller
Voyons maintenant le décorateur à l'œuvre dans un véritable AuthController. Les routes login et register doivent être accessibles sans token, contrairement à me et refresh qui nécessitent un utilisateur authentifié :
import { Controller, Post, Body, Get, Request } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { Public } from '../../common/decorators/public.decorator';
import { ApiResponseDto } from '../../common/dto/api-response.dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@Public()
@Throttle({ default: { limit: 5, ttl: 60000 } })
@ApiOperation({ summary: 'Login user' })
async login(@Body() loginDto: LoginDto): Promise<ApiResponseDto<any>> {
const result = await this.authService.login(loginDto);
return ApiResponseDto.success(result, 'Login successful');
}
@Post('register')
@Public()
@Throttle({ default: { limit: 5, ttl: 3600000 } })
@ApiOperation({ summary: 'Register new admin user' })
async register(@Body() registerDto: RegisterDto): Promise<ApiResponseDto<any>> {
const user = await this.authService.register(registerDto);
return ApiResponseDto.success(user, 'User registered successfully');
}
@Get('me')
@ApiBearerAuth('JWT-auth')
async getProfile(@Request() req): Promise<ApiResponseDto<any>> {
return ApiResponseDto.success(req.user, 'User retrieved successfully');
}
@Post('refresh')
@ApiBearerAuth('JWT-auth')
async refresh(@Request() req): Promise<ApiResponseDto<any>> {
const result = await this.authService.refreshToken(req.user);
return ApiResponseDto.success(result, 'Token refreshed successfully');
}
}Notez bien le couplage avec @Throttle() : marquer une route comme publique ne signifie pas qu'elle doit être laissée sans protection. Les endpoints login et register sont des cibles privilégiées pour les attaques par force brute, d'où la limitation à 5 requêtes par minute (et par heure pour le register).
Marquer un controller entier comme public
Pour un controller exposant uniquement du contenu public (par exemple un blog, des pages statiques, un catalogue produit), il est plus pratique d'appliquer le décorateur au niveau de la classe :
@Public()
@Controller('articles')
export class ArticlesPublicController {
@Get()
findAll() { /* ... */ }
@Get(':id')
findOne(@Param('id') id: string) { /* ... */ }
}Grâce à getAllAndOverride, toutes les méthodes du controller héritent automatiquement de la métadonnée. Et si jamais une méthode spécifique doit redevenir protégée, il suffit (en suivant le même pattern) de créer un décorateur inverse comme @Protected().
Quelles routes rendre publiques ?
L'application du @Public() doit rester un acte conscient. Voici les cas d'usage typiques :
- Authentification :
login,register,forgot-password,reset-password, vérification d'email. - Contenu public : pages d'accueil, articles de blog, catalogues, fiches produits visibles sans compte.
- Endpoints utilitaires :
health-check, statut applicatif, webhook entrants validés par signature plutôt que par JWT. - Documentation : routes Swagger si vous l'exposez en production (avec parcimonie).
À l'inverse, soyez très prudent avec les endpoints qui retournent ne serait-ce qu'une partie de données utilisateur : un endpoint « public » mal conçu est l'une des sources les plus fréquentes de fuites de données.
Pièges courants à éviter
Quelques erreurs reviennent souvent lors de l'implémentation de ce pattern :
- Oublier d'enregistrer le guard globalement. Sans
APP_GUARD, le décorateur@Public()n'a aucun effet visible (les routes restent toutes ouvertes). - Dupliquer la constante
IS_PUBLIC_KEY. Toujours l'importer depuis le fichier du décorateur. Une chaîne de caractères copiée-collée et mal orthographiée fait silencieusement échouer la vérification. - Utiliser
get()au lieu degetAllAndOverride(). Vous perdez alors la possibilité de marquer un controller entier comme public. - Croire que
@Public()dispense de toute protection. Throttling, validation des inputs et journalisation restent indispensables.
Conclusion
Le décorateur @Public() est un petit composant de quelques lignes, mais il résout proprement un problème récurrent : combiner un guard JWT global (sécurité par défaut) avec la nécessité d'exposer certaines routes ouvertes. En s'appuyant sur SetMetadata côté décorateur et Reflector.getAllAndOverride côté guard, on obtient un mécanisme déclaratif, lisible, et flexible aussi bien au niveau d'une méthode que d'un controller entier.
Pour aller plus loin, vous pouvez explorer les pistes suivantes : créer un décorateur @Roles() sur le même modèle pour gérer les autorisations fines, combiner les métadonnées avec un RolesGuard dédié, ou encore consulter la documentation officielle sur l'ExecutionContext et le Reflector pour maîtriser pleinement le système de métadonnées de NestJS.

Commentaires