Cours NestJS · Module 4: Authentification
Authentication - identifier les légionnaires
Dans cette leçon5
Un homme en armure romaine s'approche de la porte du camp. L'armure ne prouve rien - elle s'achète, ou se prend sur un mort. Avant que la sentinelle ne le laisse entrer, il doit répondre à une question : qui es-tu au juste ?
Cette question, nous l'appelons authentification. Ne la confonds pas avec celle qui viendra plus tard, à la porte du trésor : qu'as-tu le droit de faire ? - c'est l'autorisation. L'ordre est irréversible : nous établissons d'abord l'identité, ensuite seulement les privilèges. On ne peut pas vérifier le grade de quelqu'un qu'on n'a pas encore reconnu.
Dans ce module, nous bâtirons la porte entière. Cette leçon en est le fondement : le registre de la légion et l'admission sûre des recrues.
Les paquets - équiper le corps de garde
Commençons par les outils dont nous aurons besoin tout au long du module :
1npm install @nestjs/passport passport passport-jwt @nestjs/jwtpassport est la bibliothèque d'authentification elle-même, @nestjs/passport la relie à NestJS. @nestjs/jwt délivre les laissez-passer, et passport-jwt apprend à la sentinelle à les lire. Pour l'instant, nous ne faisons que les installer - tu les découvriras un par un dans les leçons suivantes.
Le registre de la légion - l'entité User
Chaque légionnaire doit avoir une entrée dans le registre :
1@Entity('users')
2export class User {
3 @PrimaryGeneratedColumn()
4 id: number;
5
6 @Column({ unique: true })
7 username: string;
8
9 @Column({ unique: true })
10 email: string;
11
12 @Column()
13 @Exclude()
14 password: string;
15}Tu lis désormais les entités sans effort - c'est le savoir du module sur TypeORM. unique: true sur username et email garantit que deux légionnaires ne peuvent pas s'enrôler sous le même nom.
Arrêtons-nous sur @Exclude(), car c'est le seul élément nouveau ici et en même temps la faille de sécurité la plus fréquente des applications débutantes. Ce décorateur ne supprime pas le champ de la base et ne chiffre rien - le mot de passe reste dans sa colonne. Il agit à la sortie : quand NestJS transforme une entité en réponse JSON, un champ marqué @Exclude() est omis.
Sans lui, un simple GET /users/1 renverrait au client le hash du mot de passe avec le reste des données. Personne ne le remarque en test, car l'application fonctionne correctement - et les mots de passe fuitent à chaque requête. Retiens cette règle : un champ qui ne doit jamais quitter le serveur reçoit @Exclude() au moment où tu le crées, et non plus tard.
Contrôler les papiers à l'entrée - le DTO
Avant que les données n'atteignent le registre, il faut les examiner. Un DTO muni de règles de validation s'en charge :
1export class RegisterDto {
2 @IsNotEmpty()
3 username: string;
4
5 @IsNotEmpty()
6 @IsEmail()
7 email: string;
8
9 @IsNotEmpty()
10 @MinLength(8)
11 password: string;
12}Note l'ordre des contrôles - il va du plus élémentaire au plus précis. D'abord @IsNotEmpty() demande si le champ a seulement été rempli. Ensuite @IsEmail() examine si ce qui a été saisi a la forme d'une adresse. Enfin @MinLength(8) impose une exigence sur le contenu.
Cet ordre a un intérêt pratique : inutile d'examiner le format d'un champ vide. Et encore une distinction, car elle prête à confusion. Tous ces décorateurs inspectent la requête seule - ils ne regardent que ce qui est arrivé et ne savent rien de la base. La question "ce nom d'utilisateur est-il déjà pris ?" exige de consulter le registre, aucun décorateur ne peut donc la poser. Ce contrôle appartient au service, et tu vas le voir à l'instant.
Admettre une recrue
L'inscription se déroule en quatre étapes dans un ordre fixé :
1async register(dto: RegisterDto): Promise<User> {
2 const existing = await this.userRepository.findOne({
3 where: { username: dto.username },
4 });
5
6 if (existing) {
7 throw new ConflictException('Un légionnaire de ce nom sert déjà');
8 }
9
10 const hashedPassword = await bcrypt.hash(dto.password, 10);
11
12 return this.userRepository.save({
13 ...dto,
14 password: hashedPassword,
15 });
16}Parcourons-les. La validation a déjà eu lieu, automatiquement - les données du DTO sont arrivées ici vérifiées. Le contrôle d'unicité est la requête en base qu'aucun décorateur ne pouvait exécuter ; en cas de collision, nous levons ConflictException, soit une réponse 409. Le hachage du mot de passe - et seulement maintenant l'enregistrement.
L'ordre des deux dernières étapes n'est pas négociable : le mot de passe devient un hash avant l'enregistrement, jamais après. Rien de lisible n'atteint jamais la base.
Qu'est-ce au juste que ce bcrypt.hash(dto.password, 10) ? Le hachage est une transformation à sens unique - une chaîne naît du mot de passe, et aucun chemin ne ramène à l'original. Ce n'est pas du chiffrement, car le chiffrement peut par définition être inversé avec une clé. À la connexion, nous ne déchiffrons donc rien ; nous hachons à nouveau le mot de passe fourni et comparons les résultats avec bcrypt.compare(). Même toi, avec un accès complet à la base, tu ne peux pas lire le mot de passe de ton utilisateur - et c'est précisément le but.
Le nombre 10 correspond aux salt rounds, qui règlent le coût du calcul. Nous y reviendrons, ainsi qu'au reste de la mécanique de bcrypt, dans une leçon consacrée au hachage.
Récapitulatif
La porte est en place, le registre fonctionne, les recrues s'enrôlent en sécurité :
- l'authentification répond à qui est le visiteur ; l'autorisation - à ce qu'il peut faire ; la première précède toujours la seconde,
- le module repose sur quatre paquets :
passport,@nestjs/passport,@nestjs/jwt,passport-jwt, @Exclude()ne touche pas à la base - il empêche seulement l'envoi d'un champ dans une réponse d'API ; sans lui, les mots de passe fuitent à chaqueGET,- la validation du DTO va du général au particulier :
@IsNotEmpty, puis@IsEmail, puis@MinLength, - les décorateurs ne voient que le contenu de la requête - un contrôle d'unicité exige une requête en base et relève du service,
- l'inscription compte quatre étapes : valider, vérifier l'existence, hacher, enregistrer,
- un hash est à sens unique : nous ne déchiffrons jamais les mots de passe, nous hachons de nouveau et comparons avec
bcrypt.compare().
Dans la prochaine leçon, nous démonterons le hachage pièce par pièce - tu apprendras ce qu'est un salt et pourquoi un hachage lent peut être une vertu. En attendant, retiens ceci : l'authentification demande "qui es-tu", et le mot de passe n'existe pas dans le registre de la légion - seule son empreinte à sens unique s'y trouve.
Code de cette leçon : src/auth/auth-basics.ts
1// Authentification - identification des légionnaires
2// Imperium romain - système de vérification d'identité
3import { Injectable, UnauthorizedException } from '@nestjs/common';
4import { JwtService } from '@nestjs/jwt';
5import * as bcrypt from 'bcrypt';
6
7// Interface utilisateur dans le système de l'Imperium
8interface Legionary {
9 id: string;
10 username: string;
11 passwordHash: string;
12 rank: 'miles' | 'centurion' | 'legatus' | 'consul';
13}
14
15@Injectable()
16export class AuthService {
17 // Simulation de la base des légionnaires
18 private legionaries: Legionary[] = [];
19
20 constructor(private jwtService: JwtService) {}
21
22 // Inscription d'un nouveau légionnaire
23 async register(username: string, password: string) {
24 // Hachage du mot de passe - comme un sceau de cire sur un document
25 const saltRounds = 10;
26 const passwordHash = await bcrypt.hash(password, saltRounds);
27
28 const newLegionary: Legionary = {
29 id: Date.now().toString(),
30 username,
31 passwordHash,
32 rank: 'miles', // Nouvelle recrue
33 };
34
35 this.legionaries.push(newLegionary);
36 console.log('Nouveau légionnaire enregistré :', username);
37
38 return { message: 'Inscription terminée avec succès' };
39 }
40
41 // Validation du légionnaire - vérification de l'identité
42 async validateUser(username: string, password: string) {
43 const legionary = this.legionaries.find(l => l.username === username);
44
45 if (!legionary) {
46 throw new UnauthorizedException('Légionnaire inconnu !');
47 }
48
49 // Comparaison du mot de passe avec le hachage
50 const isPasswordValid = await bcrypt.compare(
51 password,
52 legionary.passwordHash
53 );
54
55 if (!isPasswordValid) {
56 throw new UnauthorizedException('Mot de passe incorrect !');
57 }
58
59 return legionary;
60 }
61
62 // Connexion - délivrance du laissez-passer (token)
63 async login(username: string, password: string) {
64 const legionary = await this.validateUser(username, password);
65
66 // Payload du token JWT
67 const payload = {
68 sub: legionary.id,
69 username: legionary.username,
70 rank: legionary.rank,
71 };
72
73 return {
74 access_token: this.jwtService.sign(payload),
75 legionary: {
76 id: legionary.id,
77 username: legionary.username,
78 rank: legionary.rank,
79 },
80 };
81 }
82}
83
84console.log('Authentification : register -> validate -> login');
85console.log('Les mots de passe sont hachés avec bcrypt - ne jamais les stocker en clair !');
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. Qu'est-ce que l'authentification dans le contexte d'une application web ?
2. Quelle est la différence entre l'authentification et l'autorisation ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complétez la commande d'installation de @nestjs/passport, passport, passport-jwt et @nestjs/jwt dans un projet NestJS
- Clique dans l'ordre
Placez les éléments de la définition de l'entité User dans le bon ordre
- Tri vertical
Ordonnez les étapes de validation des données d'inscription, de la plus basique à la plus spécifique
- Éditeur de code
Complétez le AuthService avec une méthode hashPassword qui prend un mot de passe et renvoie sa version hachée à l'aide de bcrypt
- Tri vertical
Ordonnez les étapes du processus d'inscription de l'utilisateur dans NestJS