L'authentification est un pilier de toute API moderne. Avec NestJS, le standard de fait pour sécuriser les endpoints est le JSON Web Token (JWT). Dans ce tutoriel, nous allons explorer la théorie derrière les JWT, puis mettre en place une authentification complète en nous appuyant sur l'AuthModule du projet site-voyance. À la fin de cet article, vous serez capable de générer, signer, vérifier et consommer des tokens JWT dans vos applications NestJS.
Prérequis
- Node.js ≥ 18 et un projet NestJS opérationnel (≥ v10)
- Connaissance de base de TypeScript et des modules NestJS
- Une entité
User(TypeORM ici) avec au minimum unid, unemail, unpasswordhashé et unrole
Qu'est-ce qu'un JWT ?
Un JSON Web Token est une chaîne compacte composée de trois parties séparées par un point : header.payload.signature. Chaque partie est encodée en Base64URL.
Le header
Il décrit le type de token et l'algorithme de signature utilisé. Exemple :
{
"alg": "HS256",
"typ": "JWT"
}
Dans notre AuthModule, nous forçons explicitement HS256 via signOptions.algorithm pour éviter les attaques de type algorithm confusion (par exemple un attaquant qui tenterait de passer en none).
Le payload
C'est la partie qui contient les claims, c'est-à-dire les informations transportées par le token. On y trouve des claims standards (sub, iat, exp) et des claims personnalisés (email, role, etc.). Voici le payload utilisé dans notre service :
const payload = {
sub: user.id, // subject : identifiant unique de l'utilisateur (UUID)
email: user.email, // claim custom
role: user.role, // pour la gestion fine des permissions
tokenVersion: user.tokenVersion, // permet de révoquer un token côté serveur
};
Attention : le payload est uniquement encodé, pas chiffré. Toute personne possédant le token peut le décoder. Ne stockez jamais de données sensibles (mot de passe, numéro de carte, données bancaires…) dans le payload.
La signature
La signature est calculée à partir du header, du payload et d'une clé secrète : HMACSHA256(base64(header) + "." + base64(payload), secret). Elle garantit que le token n'a pas été altéré. Sans la clé secrète, impossible de produire un token valide.
Pourquoi JWT pour les APIs ?
- Stateless : le serveur n'a pas besoin de stocker de session. Toute l'info nécessaire est dans le token.
- Scalable : pas de session partagée entre instances, idéal en architecture distribuée ou serverless.
- Interopérable : un token JWT peut être consommé par n'importe quel service capable de vérifier la signature (microservices, frontend, mobile).
- Court à transporter : transmis dans l'en-tête
Authorization: Bearer <token>.
Le revers de la médaille : un token valide est difficile à invalider avant son expiration. Nous verrons plus loin que l'astuce du tokenVersion permet de pallier ce problème.
Installation des dépendances
Le package officiel @nestjs/jwt encapsule la librairie jsonwebtoken. On y ajoute @nestjs/config pour gérer proprement les variables d'environnement et bcrypt pour hasher les mots de passe.
npm install @nestjs/jwt @nestjs/config bcrypt
npm install -D @types/bcrypt
Variables d'environnement
La clé secrète JWT ne doit jamais être committée dans le code. On la définit dans un fichier .env :
JWT_SECRET=une-cle-tres-longue-et-aleatoire-de-64-caracteres-minimum
JWT_EXPIRES_IN=30m
Puis on les expose via la configuration centralisée (src/config/configuration.ts). Notez qu'on valide la présence de JWT_SECRET au démarrage : si elle est absente, l'application refuse de booter, ce qui évite tout fonctionnement en mode dégradé.
function validateEnv() {
const required: string[] = ['JWT_SECRET'];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Variables d'environnement manquantes : ${missing.join(', ')}.`,
);
}
}
validateEnv();
export default () => ({
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '30m',
},
});
Configuration du JwtModule
On utilise registerAsync pour injecter le ConfigService et lire dynamiquement les variables. C'est le pattern recommandé en production :
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { User } from '../../database/entities/user.entity';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const secret = configService.get<string>('jwt.secret');
if (!secret) {
throw new Error("JWT_SECRET est obligatoire.");
}
const expiresIn = configService.get<string>('jwt.expiresIn') || '30m';
return {
secret,
signOptions: {
expiresIn: expiresIn as any,
algorithm: 'HS256' as const,
},
};
},
}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
Trois points clés :
- Double sécurité sur le secret : on valide à la config et à l'instanciation du module.
- expiresIn court (30 minutes) : limite l'impact d'un token volé.
- Export de
JwtModuleetAuthServicepour pouvoir les utiliser dans les guards globaux.
Générer un token avec jwtService.sign()
La méthode sign() prend le payload et retourne le token signé. Elle est utilisée dans le flux de login, après vérification du mot de passe avec bcrypt :
async login(loginDto: LoginDto) {
const user = await this.validateUser(loginDto.email, loginDto.password);
const payload = {
sub: user.id,
email: user.email,
role: user.role,
tokenVersion: user.tokenVersion,
};
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
},
};
}
NestJS ajoute automatiquement les claims iat (issued at) et exp (expiration) calculés à partir de signOptions.expiresIn. Vous n'avez donc pas à les positionner manuellement.
Vérifier un token avec jwtService.verify()
À chaque requête authentifiée, il faut décoder le token et s'assurer qu'il est valide (signature correcte, non expiré). Notre AuthService expose une méthode validateToken qui combine la vérification cryptographique et un contrôle métier :
async validateToken(token: string) {
try {
const payload = this.jwtService.verify(token, {
secret: this.configService.get<string>('jwt.secret'),
});
const user = await this.userRepository.findOne({
where: { id: payload.sub },
select: ['id', 'email', 'firstName', 'lastName', 'role', 'isActive', 'tokenVersion'],
});
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
// Révocation : si tokenVersion a changé en BDD, le token est invalide
if (user.tokenVersion !== payload.tokenVersion) {
throw new UnauthorizedException();
}
return user;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
Le bloc try/catch est essentiel car verify() lance une exception si le token est expiré (TokenExpiredError), malformé ou mal signé. On enveloppe le tout dans une UnauthorizedException uniforme pour ne pas leaker d'information sur la cause exacte de l'échec.
Le JwtAuthGuard : protéger les routes
Pour appliquer cette vérification à toutes les routes (sauf celles marquées @Public()), on utilise un guard. Il extrait le token du header Authorization: Bearer ..., le valide, puis attache l'utilisateur à la requête :
@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> {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()],
);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException('No token provided');
const user = await this.authService.validateToken(token);
request.user = user;
return true;
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Côté contrôleur, @Public() ouvre l'accès aux endpoints login et register, tandis que les autres routes (comme GET /auth/me) sont automatiquement protégées :
@Get('me')
@ApiBearerAuth('JWT-auth')
async getProfile(@Request() req): Promise<ApiResponseDto<any>> {
return ApiResponseDto.success(req.user, 'User retrieved successfully');
}
Bonnes pratiques de sécurité
- Ne jamais stocker de données sensibles dans le payload : il est lisible par tous.
- Secret robuste : minimum 64 caractères aléatoires, généré avec
openssl rand -hex 64. - Expiration courte (15-30 min) avec un mécanisme de refresh token, comme l'endpoint
/auth/refresh. - Algorithme explicite : on impose
HS256à la signature ET à la vérification. - Rate limiting sur le login (
@Throttle({ default: { limit: 5, ttl: 60000 } })) pour limiter le brute force. - Révocation : le champ
tokenVersionsur l'entité User permet d'invalider tous les tokens existants en l'incrémentant (utile en cas de changement de mot de passe ou de logout global). - Transport HTTPS uniquement en production.
Conclusion
Vous avez maintenant une authentification JWT complète et sécurisée dans NestJS : compréhension du format (header/payload/signature), configuration du JwtModule, génération via jwtService.sign(), vérification via jwtService.verify(), et protection des routes via un JwtAuthGuard global. L'astuce du tokenVersion apporte un mécanisme de révocation simple, palliant la principale faiblesse des JWT stateless.
Pour aller plus loin, vous pouvez explorer : la mise en place de refresh tokens en base de données avec rotation, l'utilisation de passport-jwt pour bénéficier de l'écosystème Passport, la signature asymétrique RS256 (pratique en microservices où seul un service détient la clé privée), ou encore l'ajout de Role Guards qui s'appuient sur le claim role du payload pour des autorisations fines. Bonne sécurisation !

Commentaires