Sécuriser les sessions utilisateur : comment fonctionne l'authentification moderne
La plupart des vulnérabilités d'auth ne sont pas dans le formulaire de connexion — elles sont dans la façon dont les tokens sont émis, stockés et invalidés. Ce guide décortique les flux access/refresh token, les stratégies de blacklisting, et les cas limites de production qui maintiennent les sessions utilisateur vraiment sécurisées.
Les JSON Web Tokens (JWT) sont le standard industriel pour l'authentification sans état dans les applications web modernes. Ce guide complet couvre les fondamentaux des JWT, la génération et validation sécurisées des tokens, les stratégies de refresh token, et les patterns prêts pour la production pour les applications NestJS et React.
Qu'est-ce qu'un JWT ?
Un JWT est un token auto-suffisant, cryptographiquement signé, qui transporte des informations utilisateur (claims) sans nécessiter de stockage de session côté serveur. Contrairement à l'authentification traditionnelle basée sur les sessions, les JWTs permettent une authentification sans état et scalable.
JWT vs Cookies de Session
| Fonctionnalité | Cookies de Session | JWT |
|---|---|---|
| Stockage | Côté serveur (Redis, DB) | Côté client (localStorage, cookies) |
| Scalabilité | ❌ Nécessite des sessions sticky | ✅ Sans état, n'importe quel serveur peut vérifier |
| Révocation | ✅ Immédiate (supprimer du store) | ❌ Nécessite une blacklist ou une courte expiration |
| Cross-Domain | ❌ Même origine uniquement | ✅ Fonctionne sur tous les domaines |
| Apps Mobiles | ⚠️ Gestion complexe des cookies | ✅ En-tête Authorization simple |
| Taille | ✅ Petit ID de session | ⚠️ Payload plus grand (200-500 octets) |
Structure d'un JWT
Un JWT est composé de trois parties séparées par des points : header.payload.signature
STRUCTURE JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└─────────────┬─────────────────┘ └─────────────────────────────────────────────────────────┘ └──────────────────────┬─────────────────────┘
EN-TÊTE PAYLOAD SIGNATURE
1. EN-TÊTE (JSON encodé en Base64URL)
{
"alg": "HS256", // Algorithme : HMAC-SHA256
"typ": "JWT" // Type : JWT
}
2. PAYLOAD (JSON encodé en Base64URL)
{
"userId": "1234567890",
"name": "John Doe",
"iat": 1516239022, // Issued At (horodatage)
"exp": 1516242622 // Expiration (horodatage)
}
3. SIGNATURE (HMACSHA256)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
FLUX DE VÉRIFICATION
┌────────────────────────────────────────────────┐
│ 1. Diviser JWT en header, payload, signature │
│ 2. Décoder header et payload (Base64URL) │
│ 3. Vérifier la signature : │
│ - Calculer HMAC de header + payload │
│ - Comparer avec la signature fournie │
│ 4. Vérifier l'expiration (claim exp) │
│ 5. Extraire les données utilisateur du payload│
└────────────────────────────────────────────────┘
Implémentation Backend (NestJS)
Dépendances
npm install @nestjs/jwt bcrypt
npm install -D @types/bcrypt
Configuration du Module JWT
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
@Module({
imports: [
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '15m' },
}),
],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
AuthService : Génération de Tokens
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { PrismaService } from './prisma.service';
interface JwtPayload {
userId: string;
email: string;
role: string;
}
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private prisma: PrismaService,
) {}
generateAccessToken(user: { id: string; email: string; role: string }): string {
const payload: JwtPayload = { userId: user.id, email: user.email, role: user.role };
return this.jwtService.sign(payload);
}
generateRefreshToken(user: { id: string }): string {
return this.jwtService.sign(
{ userId: user.id, tokenType: 'refresh' },
{ secret: process.env.JWT_REFRESH_SECRET, expiresIn: '7d' }
);
}
async validateUser(email: string, password: string) {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) return null;
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) return null;
const { password: _, ...result } = user;
return result;
}
async login(email: string, password: string) {
const user = await this.validateUser(email, password);
if (!user) throw new UnauthorizedException('Identifiants invalides');
const accessToken = this.generateAccessToken(user);
const refreshToken = this.generateRefreshToken(user);
await this.prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return { accessToken, refreshToken, user };
}
async verifyAccessToken(token: string): Promise<JwtPayload> {
try {
return this.jwtService.verify(token);
} catch {
throw new UnauthorizedException('Token invalide');
}
}
async refreshAccessToken(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken, {
secret: process.env.JWT_REFRESH_SECRET,
});
const storedToken = await this.prisma.refreshToken.findFirst({
where: {
token: refreshToken,
userId: payload.userId,
revoked: false,
expiresAt: { gt: new Date() },
},
include: { user: true },
});
if (!storedToken) throw new UnauthorizedException('Refresh token invalide');
return { accessToken: this.generateAccessToken(storedToken.user) };
} catch {
throw new UnauthorizedException('Refresh token invalide');
}
}
async logout(refreshToken: string) {
await this.prisma.refreshToken.updateMany({
where: { token: refreshToken },
data: { revoked: true },
});
}
}
Guard d'Authentification JWT
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException('Aucun token fourni');
try {
request.user = await this.jwtService.verifyAsync(token);
} catch {
throw new UnauthorizedException('Token invalide');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Contrôle d'Accès Basé sur les Rôles
import { SetMetadata, Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
// Utilisation
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin', 'moderator')
getUsers() {
return 'Route admin uniquement';
}
}
Bonnes Pratiques de Sécurité
1. Utiliser des Secrets Robustes
# Générer un secret aléatoire robuste
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# .env
JWT_SECRET=a8f5f167f44f4964e6c998dee827110c03d3fb8f36f8c1bd3f8c2f8f6f8d8f8e
JWT_REFRESH_SECRET=b9g6g278g55g5075f7d009eef938221d04e4gc9g47g9d2ce4g9d3g9g7g9e9g9f
2. Courte Expiration du Token d'Accès
- Tokens d'accès : 15 minutes (réduit la fenêtre de vol de token)
- Refresh tokens : 7 jours (équilibre sécurité et UX)
3. Stockage Sécurisé
| Méthode de Stockage | Sécurité | Notes |
|---|---|---|
| localStorage | ⚠️ Vulnérable aux XSS | JavaScript peut accéder |
| Cookie HttpOnly | ✅ Protégé contre XSS | ⚠️ Vulnérable aux CSRF (utiliser des tokens CSRF) |
| Mémoire uniquement | ✅ Le plus sécurisé | ❌ Perdu au rafraîchissement de la page |
4. Payload Minimal
// ❌ Mauvais : Données sensibles dans le JWT
const payload = {
userId: user.id,
email: user.email,
password: user.password, // Jamais !
creditCard: user.creditCard, // Jamais !
permissions: user.permissions, // Gonfle le token
};
// ✅ Bon : Claims minimaux
const payload = {
userId: user.id,
email: user.email,
role: user.role,
};
// Récupérer les données supplémentaires depuis la base de données si nécessaire
5. Validation de l'Algorithme
// Empêcher l'attaque "alg: none"
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: {
algorithm: 'HS256', // Définir explicitement l'algorithme
},
verifyOptions: {
algorithms: ['HS256'], // N'autoriser que HS256
},
});
Conclusion
Les JWTs fournissent un mécanisme d'authentification sans état et scalable, parfait pour les microservices modernes et les applications mobiles. Combinés avec des refresh tokens et des pratiques de stockage sécurisé, vous pouvez construire des systèmes d'authentification de niveau production.
Points clés :
- Vérification sans état : N'importe quel serveur peut valider les JWTs sans accès à la base de données
- Pattern Access + Refresh : Tokens d'accès à courte durée avec des refresh tokens à longue durée
- Auto-refresh : Rafraîchir automatiquement les tokens expirés côté client
- Sécurité d'abord : Utiliser des secrets robustes, des payloads minimaux, et des cookies HttpOnly quand c'est possible
- Accès basé sur les rôles : Intégrer les rôles utilisateur dans les JWTs pour l'autorisation
Comprendre les fondamentaux des JWT et les implémenter de manière sécurisée est critique pour construire des systèmes d'authentification qui évoluent des startups aux applications enterprise.
Explorer le Code
Consultez le dépôt ft_transcendence sur GitHub pour voir ces patterns dans un vrai jeu multijoueur.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.