Cours NestJS · Module 4: Authentification
Session Management et Cookie Auth - les autres routes vers le fort
Dans cette leçon5
Les laissez-passer JWT ont un défaut que nous avons tu jusqu'ici. Un légionnaire se révèle traître et tu veux le chasser du camp sur-le-champ - mais son laissez-passer reste valable encore vingt heures. Le serveur n'a aucun moyen de l'annuler, car il ne l'a enregistré nulle part. Tout ce qu'il sait du document se trouve dans le document.
Il existe une route plus ancienne vers le fort : la liste des invités à la porte. La sentinelle ne lit pas les laissez-passer - elle consulte le registre et vérifie si le visiteur figure sur la liste. Rayer un nom agit immédiatement. Cette route s'appelle l'authentification par session.
Sans état contre avec état
La différence entre ces deux routes tient à une seule question : le serveur retient-il quelque chose ?
JWT est sans état (stateless). Tout le savoir sur le légionnaire voyage dans le token, le serveur ne stocke rien. L'avantage est la mise à l'échelle - dresse dix serveurs et chacun lira le même token, car la clé secrète lui suffit pour vérifier. L'inconvénient est justement ce traître : il n'y a rien à rayer.
Une session est avec état (stateful). Après la connexion, le serveur inscrit chez lui une entrée - qui est cette personne - et ne renvoie au client qu'un identifiant de session dans un cookie. Le cookie ne signifie rien en soi ; c'est un ticket de vestiaire. À chaque requête, le serveur retrouve l'entrée grâce à ce ticket. La déconnexion supprime l'entrée - et le visiteur cesse d'exister immédiatement.
Garde cette paire de notions avec sa conséquence, car c'est là-dessus qu'on t'interroge : stateless = rien à révoquer, mais mise à l'échelle facile ; stateful = révocation instantanée, mais le serveur doit retenir.
Configuration - quatre étapes
Nous introduisons les sessions dans un ordre fixé. Commençons par les paquets :
1npm install express-session
2npm install -D @types/express-sessionLe second paquet ne contient que des types TypeScript - d'où le drapeau -D, puisque l'exécution en production n'en a pas besoin.
La deuxième étape consiste à brancher le middleware. Nous le faisons dans main.ts, car les sessions doivent être prêtes avant qu'une requête n'atteigne les contrôleurs :
1app.use(
2 session({
3 secret: process.env.SESSION_SECRET,
4 resave: false,
5 saveUninitialized: false,
6 cookie: {
7 httpOnly: true,
8 secure: true,
9 maxAge: 3600000,
10 },
11 }),
12);Passons ces options en revue, car deux d'entre elles décident de la sécurité de tout le dispositif. secret signe le cookie pour que personne ne remplace le ticket par celui d'un autre - nous le lisons depuis une variable d'environnement, comme la clé JWT.
httpOnly: true est la ligne la plus importante de ce bloc. Elle rend le cookie illisible depuis le JavaScript du navigateur. Si quelqu'un injectait un script étranger dans ta page, sans ce drapeau il déroberait l'identifiant de session par un simple document.cookie. secure: true ajoute une seconde condition : le cookie ne voyage que par HTTPS, il ne peut donc pas être intercepté en chemin.
maxAge est la durée de vie en millisecondes - ici une heure. resave: false et saveUninitialized: false évitent les écritures inutiles : ne réécris pas l'entrée si rien n'a changé, et ne crée pas de session pour un visiteur qui n'a encore rien fait.
La troisième étape consiste à récupérer la session dans un contrôleur :
1@Post('login')
2login(@Body() dto: LoginDto, @Session() session: Record<string, any>) {
3 const user = this.authService.validate(dto);
4 session.userId = user.id;
5 return { message: 'Connexion réussie' };
6}
7
8@Get('profile')
9getProfile(@Session() session: Record<string, any>) {
10 if (!session.userId) {
11 throw new UnauthorizedException();
12 }
13 return this.usersService.findOne(session.userId);
14}Le décorateur @Session() injecte l'objet de session - un objet ordinaire dans lequel tu inscris ce que tu veux retenir. Note ce qui est absent : nous ne renvoyons aucun token. Le client reçoit le cookie automatiquement, dans les en-têtes de réponse, et le renvoie tout aussi automatiquement à chaque requête suivante. D'où l'impression que "cela marche tout seul" - et d'où le piège, car cela marche aussi quand la requête vient de la page de quelqu'un d'autre. Cette menace s'appelle CSRF et exige une protection distincte, dont un JWT placé dans un en-tête n'a pas besoin.
Quatrième étape : où vivent les sessions
Par défaut, express-session conserve les entrées dans la mémoire du processus Node. Cela suffit sur ton portable et échoue en production pour deux raisons : redémarrer l'application déconnecte tout le monde, et avec deux serveurs, un utilisateur connecté sur le premier est inconnu du second.
En production, nous désignons donc un magasin externe :
1app.use(
2 session({
3 store: new RedisStore({ client: redisClient }),
4 secret: process.env.SESSION_SECRET,
5 resave: false,
6 saveUninitialized: false,
7 }),
8);Redis convient le mieux ici, car il garde les données en mémoire et supprime lui-même les entrées expirées. Tous les serveurs interrogent le même magasin, le ticket de vestiaire fonctionne donc quelle que soit la sentinelle qui l'examine. Et voilà le prix de l'état dont nous parlions : retenir coûte de l'infrastructure.
Que choisir
La règle pratique est brève. JWT pour les API servant des applications mobiles, des microservices et partout où le client n'est pas un navigateur - il voyage dans un en-tête et n'exige aucun magasin partagé. Les sessions pour les applications web classiques rendues côté serveur, surtout quand tu dois déconnecter quelqu'un immédiatement ou voir la liste des sessions actives.
Il n'y a pas ici de meilleure et de pire solution - il y a un choix entre mise à l'échelle facile et contrôle immédiat. Et c'est la question par laquelle je te recommande de commencer : dois-je pouvoir révoquer un accès en une seconde ?
Récapitulatif
Le fort a deux portes et tu sais laquelle ouvrir, et quand :
- JWT est sans état : le serveur ne retient rien, monte en charge facilement, mais n'a rien à révoquer avant l'expiration du token,
- une session est avec état : le serveur garde une entrée, le client ne reçoit qu'un identifiant dans un cookie - un ticket de vestiaire,
- la configuration compte quatre étapes : installer les paquets, le middleware dans
main.ts,@Session()dans le contrôleur, choisir un magasin, httpOnly: truecoupe le cookie du JavaScript,secure: trueimpose HTTPS - les deux drapeaux sont obligatoires,- le cookie voyage automatiquement, les sessions exigent donc une protection CSRF distincte,
- le magasin en mémoire par défaut ne convient pas à la production : un redémarrage déconnecte tout le monde, et un second serveur ignore les sessions du premier,
- Redis résout les deux problèmes, car tous les serveurs interrogent le même magasin.
Dans la prochaine leçon, tu affronteras un projet : bâtir un système d'authentification complet pour la légion. En attendant, retiens ceci : un JWT est un laissez-passer que l'on porte sur soi, une session est un ticket de vestiaire - le document parle de lui-même, le ticket ne vaut qu'aussi longtemps que la sentinelle garde l'entrée dans son registre.
Code de cette leçon : src/auth/session-auth.ts
1// Session Management et authentification par cookie dans NestJS
2import { Controller, Post, Get, Body, Session, HttpCode } from '@nestjs/common';
3import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
4
5// ============================================
6// 1. Session Auth Controller
7// ============================================
8@Controller('auth')
9class SessionAuthController {
10 @Post('login')
11 @HttpCode(200)
12 async login(
13 @Body() body: { username: string; password: string },
14 @Session() session: Record<string, any>,
15 ) {
16 // TODO: Valide l'utilisateur
17 // TODO: Enregistre userId, username, role dans la session
18 session.userId = 1;
19 session.username = body.username;
20 session.role = 'MILES';
21
22 return { message: 'Connecté au fort !' };
23 }
24
25 @Get('profile')
26 async getProfile(@Session() session: Record<string, any>) {
27 // TODO: Vérifie si session.userId existe
28 // TODO: Sinon - lève UnauthorizedException
29 if (!session.userId) {
30 throw new UnauthorizedException('Aucune session active !');
31 }
32
33 return {
34 userId: session.userId,
35 username: session.username,
36 role: session.role,
37 };
38 }
39
40 @Post('logout')
41 @HttpCode(200)
42 async logout(@Session() session: Record<string, any>) {
43 // TODO: Détruis la session
44 const username = session.username;
45 session.destroy(() => {});
46 return { message: 'Déconnecté : ' + username };
47 }
48}
49
50// ============================================
51// 2. Session Guard
52// ============================================
53@Injectable()
54class SessionGuard implements CanActivate {
55 canActivate(context: ExecutionContext): boolean {
56 const request = context.switchToHttp().getRequest();
57 const session = request.session;
58
59 // TODO: Vérifie si la session contient userId
60 if (!session || !session.userId) {
61 throw new UnauthorizedException('Session expirée !');
62 }
63
64 request.user = {
65 id: session.userId,
66 username: session.username,
67 role: session.role,
68 };
69
70 return true;
71 }
72}
73
74// ============================================
75// 3. Comparaison JWT vs Session
76// ============================================
77console.log('=== Session vs JWT ===');
78console.log('Session : état sur le serveur, petit cookie avec un Session ID');
79console.log("JWT : stateless, le token contient les données de l'utilisateur");
80console.log('');
81console.log('Session : invalidation facile (suppression en base)');
82console.log("JWT : invalidation difficile (token valide jusqu'à expiration)");
83console.log('');
84console.log("Session : nécessite un store partagé (Redis) lors de la mise à l'échelle");
85console.log("JWT : mise à l'échelle facile - chaque serveur vérifie le token");
86Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la principale différence entre l'authentification par session et par JWT ?
Tâches pratiques dans le jeu
- Tri vertical
Placez les étapes de configuration de l'auth par session dans NestJS :