Cours NestJS · Module 4: Authentification
Hachage de mot de passe - hacher les codes des coffres
Dans cette leçon4
Imagine que quelqu'un emporte hors du fort une copie du registre des légionnaires. Si les mots de passe y figurent en clair, l'ennemi les connaît tous sur-le-champ - et comme les gens réutilisent les mêmes mots de passe partout, leurs messageries et leurs comptes sur d'autres services tombent aussi. L'architecte Vitruvius a une règle simple pour cela : le coffre ne garde pas les clés, seulement leurs empreintes. C'est exactement ce qu'est le hachage de mot de passe.
Qu'est-ce que le hachage de mot de passe ?
Le hachage transforme un mot de passe en une chaîne de caractères (un hachage) à l'aide d'une fonction à sens unique. Cela fonctionne comme une empreinte digitale : le même mot de passe produit toujours une empreinte correspondante, mais on ne peut pas reconstituer le doigt à partir de l'empreinte. À la connexion, tu ne récupères donc pas le mot de passe dans la base : tu calcules le hachage de ce que l'utilisateur a saisi et tu compares les résultats.
Pourquoi pas un simple chiffrement ?
Le chiffrement est réversible - quiconque détient la clé peut tout lire. Compare les deux approches (les fonctions encrypt et decrypt sont fictives et servent seulement à illustrer la différence) :
1// MAUVAISE APPROCHE - simple chiffrement
2const encryptedPassword = encrypt('my-secret-password', 'encryption-key');
3// Peut être déchiffré : decrypt(encryptedPassword, 'encryption-key')
4
5// BONNE APPROCHE - hachage
6const hashedPassword = await bcrypt.hash('my-secret-password', 12);
7// Le processus ne peut pas être inversé !Une fuite de la clé de chiffrement signifie la fuite de tous les mots de passe à la fois. Un hachage n'a aucune clé à voler. Pourquoi pas un simple SHA-256, alors ? Parce qu'il est trop rapide : une carte graphique teste des milliards de candidats par seconde. bcrypt est lent exprès et ajoute un sel à chaque mot de passe.
Le sel et le coût
Le sel est une valeur aléatoire générée séparément pour chaque mot de passe. Grâce à lui, deux légionnaires qui ont le mot de passe « Roma123 » obtiennent des hachages complètement différents, et les tables précalculées de mots de passe déjà cassés deviennent inutiles. Le coût (saltRounds) indique la quantité de travail nécessaire : bcrypt effectue 2^coût itérations, donc chaque cran supplémentaire double le temps. Voici à quoi ressemble un hachage terminé de 60 caractères :
1$2b$12$FSazelkmW3GJ/HK7ig5K9.1rc20s7WQMF0UJoC58nf5eHWniFUUrmLa partie $2b$ est la version de l'algorithme, 12 est le coût, les 22 caractères suivants sont le sel, et les 31 derniers le hachage proprement dit. Le sel est stocké dans le résultat lui-même, tu n'as donc pas besoin d'une colonne séparée pour lui dans la base.
Implémentation avec bcrypt
Tu installes le paquet bcrypt et les types @types/bcrypt, puis tu crées un service qui sera le seul endroit où l'on manipule les mots de passe :
1// npm install bcrypt
2// npm install -D @types/bcrypt
3
4import { Injectable } from '@nestjs/common';
5import * as bcrypt from 'bcrypt';
6
7@Injectable()
8export class PasswordService {
9 private readonly saltRounds = 12; // Coût du hachage : 2^12 itérations
10
11 async hashPassword(plainPassword: string): Promise<string> {
12 console.log("Création d'un hachage de mot de passe sécurisé...");
13
14 // bcrypt génère lui-même le sel et le stocke dans le résultat
15 const hashedPassword = await bcrypt.hash(plainPassword, this.saltRounds);
16
17 console.log(`Mot de passe haché : ${hashedPassword.substring(0, 20)}...`);
18
19 return hashedPassword;
20 }Un seul appel à bcrypt.hash() exécute tout le processus : tu fournis le coût, la bibliothèque génère un sel aléatoire, le combine avec le mot de passe et effectue 2^12 tours, puis tu enregistres la chaîne obtenue dans la base. Le mot de passe lui-même n'est conservé nulle part. La version asynchrone calcule dans le pool de threads de libuv, elle ne bloque donc pas la boucle d'événements du serveur.
La vérification à la connexion ressemble à ceci :
1 async verifyPassword(
2 plainPassword: string,
3 hashedPassword: string
4 ): Promise<boolean> {
5 console.log('Vérification du mot de passe...');
6
7 const isValid = await bcrypt.compare(plainPassword, hashedPassword);
8
9 console.log(`Résultat de la vérification : ${isValid ? 'mot de passe correct' : 'mot de passe incorrect'}`);
10
11 return isValid;
12 }bcrypt.compare() lit le sel et le coût dans le hachage enregistré, hache le mot de passe saisi et compare les deux résultats. Rien n'est déchiffré ici - c'est impossible.
Quand, dans quelques années, tu passeras le coût à 13, les anciens hachages indiqueront toujours 12. Une méthode de vérification t'aide à les repérer :
1 // Vérifier si le mot de passe doit être re-haché (changement de saltRounds)
2 async needsRehash(hashedPassword: string): Promise<boolean> {
3 try {
4 // Le coût (saltRounds) est stocké dans le hachage lui-même
5 const currentRounds = this.extractSaltRounds(hashedPassword);
6 return currentRounds < this.saltRounds;
7 } catch (error) {
8 return true; // Si cela ne peut être déterminé, il vaut mieux re-hacher
9 }
10 }
11
12 private extractSaltRounds(hash: string): number {
13 // Format bcrypt : $2b$12$...
14 const parts = hash.split('$');
15 return parseInt(parts[2], 10);
16 }
17}Découper la chaîne à la main au signe $ fonctionne, mais la bibliothèque propose bcrypt.getRounds(hash) tout prêt - et c'est ce que je te recommande. Génère le nouveau hachage lors de la prochaine connexion réussie, car c'est le seul moment où tu as le vrai mot de passe en main.
Les limites de bcrypt
bcrypt ne lit que les 72 premiers octets de son entrée - des octets UTF-8, pas des caractères - et ignore le reste sans rien dire. Pour des mots de passe, c'est rarement un problème, mais retiens cette limite : avec les refresh tokens, elle se révélera décisive. Un coût de 12 est un bon point de départ ; la documentation de NestJS présente aussi l'alternative argon2.
Dans la prochaine leçon, tu découvriras les tokens JWT, les laissez-passer qu'un légionnaire reçoit après une connexion réussie.
Souviens-toi : on ne chiffre pas les mots de passe, on les hache - le coffre garde l'empreinte de la clé, jamais la clé elle-même.
Code de cette leçon : src/auth/password.service.ts
1// Hachage de mot de passe - chiffrer les codes des coffres
2import { Injectable } from '@nestjs/common';
3import * as bcrypt from 'bcrypt';
4
5@Injectable()
6export class PasswordService {
7 private readonly saltRounds = 12;
8
9 // TODO: Implémente le hachage du mot de passe
10 // Utilise bcrypt.hash() avec this.saltRounds
11 async hashPassword(plainPassword: string): Promise<string> {
12 // TODO: Génère le hachage du mot de passe
13 return '';
14 }
15
16 // TODO: Implémente la vérification du mot de passe
17 // Utilise bcrypt.compare() pour comparer
18 async verifyPassword(
19 plainPassword: string,
20 hashedPassword: string,
21 ): Promise<boolean> {
22 // TODO: Compare le mot de passe avec le hachage
23 return false;
24 }
25
26 // Vérifie si le mot de passe nécessite un re-hachage
27 async needsRehash(hashedPassword: string): Promise<boolean> {
28 try {
29 const parts = hashedPassword.split('$');
30 const currentRounds = parseInt(parts[2], 10);
31 return currentRounds < this.saltRounds;
32 } catch {
33 return true;
34 }
35 }
36}
37
38// Démonstration
39async function demo() {
40 const service = new PasswordService();
41
42 const password = 'GloriaRomae2024';
43 console.log('Mot de passe :', password);
44
45 const hashed = await service.hashPassword(password);
46 console.log('Hash:', hashed);
47
48 const isValid = await service.verifyPassword(password, hashed);
49 console.log('Correct :', isValid);
50
51 const isWrong = await service.verifyPassword('ZleHaslo', hashed);
52 console.log('Incorrect :', isWrong);
53}
54
55demo();
56
57console.log('bcrypt - hachage à sens unique des mots de passe');
58console.log('Salt rounds = force du hachage (plus élevé = plus sûr mais plus lent)');
59Tu 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. Pourquoi les mots de passe doivent-ils être hachés plutôt que chiffrés ?
Tâches pratiques dans le jeu
- Clique dans l'ordre
Placez les étapes du hachage de mot de passe avec bcrypt dans l'ordre :