Une base de données qui fuite ne devrait jamais livrer les mots de passe de vos utilisateurs en clair. C’est exactement le rôle de bcrypt : transformer un mot de passe en une empreinte lente à calculer, salée et impossible à inverser. En 2026, alors que les fuites de données se multiplient en Europe, hacher correctement les mots de passe reste la première ligne de défense côté serveur. Ce tutoriel vous montre, en 12 étapes concrètes, comment intégrer bcrypt en Node.js pour inscrire, vérifier et migrer des mots de passe dans une application Express réelle.
Vous repartirez avec un projet complet et fonctionnel, du choix du cost factor jusqu’au rehachage automatique, en passant par les pièges classiques (limite des 72 octets, comparaison naïve, blocage de l’event loop) et un guide de dépannage couvrant les erreurs les plus fréquentes. Tout le code est testé sur Node.js 22 LTS et 24 LTS.
Pourquoi hacher un mot de passe avec bcrypt en 2026
Un mot de passe ne doit jamais être stocké tel quel. Si votre base est compromise, des identifiants en clair donnent un accès immédiat à tous les comptes, et la plupart des utilisateurs réemploient le même mot de passe ailleurs. Le hachage casse cette chaîne : à la place du mot de passe, vous conservez une empreinte cryptographique dont on ne peut pas remonter au texte d’origine.
Tous les algorithmes de hachage ne se valent pas pour cette tâche. SHA-256 ou SHA-3 sont rapides par conception, ce qui les rend excellents pour vérifier l’intégrité d’un fichier mais catastrophiques pour les mots de passe : un attaquant équipé d’une carte graphique teste des milliards de SHA-256 par seconde. bcrypt prend le contre-pied. Il a été conçu en 1999 par Niels Provos et David Mazières pour être volontairement lent et configurable, afin que le coût d’une attaque par force brute reste prohibitif même quand le matériel progresse.
Trois propriétés rendent bcrypt adapté au stockage de mots de passe. D’abord, il intègre un sel unique dans chaque empreinte, ce qui neutralise les tables arc-en-ciel et empêche deux utilisateurs ayant le même mot de passe d’obtenir le même hash. Ensuite, son cost factor ajustable permet d’augmenter le temps de calcul d’année en année sans changer de code. Enfin, sa robustesse est éprouvée depuis plus de vingt ans, et l’OWASP le classe toujours parmi les fonctions de hachage de mots de passe acceptables en 2026.
Concrètement, bcrypt protège trois moments clés du cycle de vie d’un compte. À l’inscription, le mot de passe est haché avant d’atteindre la base. À la connexion, le mot de passe saisi est comparé au hash stocké sans jamais déchiffrer quoi que ce soit. Et lors d’une fuite, l’attaquant ne récupère que des empreintes salées, dont le craquage prend des années pour un mot de passe correct. C’est exactement ce que nous allons construire.
bcrypt face à Argon2 et scrypt : que choisir
Trois familles d’algorithmes dominent le hachage de mots de passe moderne : bcrypt, scrypt et Argon2. L’OWASP, dans son Password Storage Cheat Sheet, recommande Argon2id en premier choix lorsque vous le pouvez, puis scrypt et bcrypt comme alternatives parfaitement acceptables. bcrypt garde un avantage de taille : sa maturité, sa disponibilité partout et l’absence de paramètres mémoire compliqués à régler.
| Critère | bcrypt | scrypt | Argon2id |
|---|---|---|---|
| Année de création | 1999 | 2009 | 2015 (lauréat PHC) |
| Base algorithmique | Blowfish | PBKDF2 + mémoire | BLAKE2 + mémoire |
| Résistance GPU/ASIC | Bonne (CPU) | Élevée (mémoire) | Très élevée (mémoire + parallélisme) |
| Paramètres à régler | 1 (cost) | 3 (N, r, p) | 3 (mémoire, temps, parallélisme) |
| Sel intégré au hash | Oui | Oui | Oui |
| Limite d’entrée | 72 octets | Aucune pratique | Aucune pratique |
| Recommandation OWASP 2026 | Acceptable | Acceptable | Préféré |
Quand choisir bcrypt plutôt qu’Argon2 ? Quand vous voulez une solution simple, sans réglage mémoire, compatible avec tout l’écosystème Node.js et déjà présente dans des millions de bases en production. Si vous démarrez un projet neuf à fortes exigences de sécurité et que vous maîtrisez le réglage mémoire, Argon2id est le meilleur choix. Pour la grande majorité des applications web, bcrypt avec un cost factor bien calibré reste un choix sûr et défendable. Si vous hésitez, lisez notre dossier dédié au stockage sécurisé des mots de passe.
Comment fonctionne bcrypt en interne
Comprendre le mécanisme évite la plupart des erreurs d’implémentation. bcrypt repose sur trois piliers : l’algorithme Blowfish, un sel intégré et un facteur de coût exponentiel.
Le moteur Blowfish et le key schedule coûteux
bcrypt dérive du chiffrement Blowfish, mais détourne son initialisation de clé. La fonction interne, baptisée EksBlowfishSetup (Expensive key schedule), répète l’initialisation des clés un grand nombre de fois. C’est cette répétition qui rend le calcul lent et réglable. Là où SHA-256 produit une empreinte en quelques microsecondes, bcrypt vise délibérément des centaines de millisecondes. Cette lenteur n’est pas un défaut : c’est la fonctionnalité de sécurité elle-même.
Le sel intégré, généré automatiquement
Le sel est une valeur aléatoire de 16 octets (128 bits) ajoutée au mot de passe avant hachage. bcrypt le génère pour vous et l’inscrit directement dans l’empreinte finale. Vous n’avez donc pas à gérer une colonne « sel » séparée dans votre base : le sel voyage avec le hash. Deux utilisateurs avec le mot de passe « motdepasse123 » obtiennent ainsi deux empreintes totalement différentes, ce qui rend les tables précalculées inutiles.
Le cost factor, ou nombre de tours
Le cost factor (aussi appelé work factor ou salt rounds) contrôle le nombre d’itérations : le nombre de tours réels vaut 2 puissance le cost. Un cost de 10 équivaut à 1 024 itérations, un cost de 12 à 4 096, un cost de 14 à 16 384. Chaque incrément double le temps de calcul. L’OWASP recommande un cost d’au moins 10, et la valeur de référence couramment retenue en 2025-2026 est 12, à calibrer pour viser entre 250 ms et 1 seconde par hachage sur votre matériel de production. Nous verrons à l’étape 9 comment mesurer et choisir cette valeur.
Anatomie d’un hash bcrypt : décortiquer le format $2b$
Une empreinte bcrypt n’est pas une chaîne opaque : elle encode tout ce dont la vérification a besoin. Prenons cet exemple réel produit avec un cost de 12 :
$2b$12$Eq8X9q1mZ3oQpL5vK7nJ0u8Y2cF4dG6hR1tS3wV5xZ7aB9cD1eFq
| | | |
| | | +-- hash (31 caracteres Base64)
| | +-- sel (22 caracteres Base64, soit 16 octets)
| +-- cost factor (ici 12, soit 4096 iterations)
+-- identifiant de version de l'algorithme ($2b$)
Le tableau suivant détaille chaque segment. C’est parce que le sel et le cost sont stockés dans l’empreinte que bcrypt.compare() peut revérifier un mot de passe sans information supplémentaire.
| Segment | Valeur exemple | Longueur | Rôle |
|---|---|---|---|
| Préfixe de version | $2b$ | 4 caractères | Identifie la variante de l’algorithme |
| Cost factor | 12 | 2 chiffres | Exposant du nombre d’itérations (2^12) |
| Sel | 22 caractères | 16 octets | Aléa unique encodé en Base64 |
| Empreinte | 31 caractères | 23 octets | Résultat final du hachage |
| Total | chaîne complète | 60 caractères | Tient dans un VARCHAR(60) ou plus large |
Le préfixe mérite une note. Les versions historiques $2a$ et $2y$ existent encore dans d’anciennes bases ; $2b$ est la variante recommandée aujourd’hui, qui corrige un bug de gestion de la longueur présent dans certaines vieilles implémentations. La bibliothèque Node.js produit du $2b$ par défaut, mais sait vérifier les anciens formats, ce qui facilite les migrations. Pour réserver la colonne en base, prévoyez au moins 60 caractères, voire 72 par sécurité.
Prérequis : versions et outils nécessaires
Ce tutoriel suppose des bases en JavaScript et en ligne de commande. Voici l’environnement exact utilisé. Les numéros de version marqués « latest » signifient que vous devez installer la dernière version stable au moment de la lecture.
| Outil | Version recommandée | Vérification |
|---|---|---|
| Node.js | 22 LTS ou 24 LTS | node -v |
| npm | livré avec Node.js | npm -v |
| bcrypt (npm) | branche 5.x (latest) | npm ls bcrypt |
| Express | 5.x (ou 4.x) | npm ls express |
| Éditeur | VS Code (latest) ou équivalent | – |
| OS | Linux, macOS ou Windows | – |
La bibliothèque bcrypt native compile un module C++ à l’installation. Sur la plupart des systèmes, npm télécharge un binaire précompilé et tout fonctionne. Si la compilation échoue (Windows sans outils de build, conteneurs Alpine minimalistes), vous avez deux options : installer les outils de compilation, ou utiliser bcryptjs, une réimplémentation 100 % JavaScript sans dépendance native. Nous y reviendrons en détail.
Étapes 1 à 4 : projet, dépendances et premier hash
Étape 1 : initialiser le projet Node.js
Créez un dossier de travail et initialisez un projet. Nous activons les modules ES pour utiliser la syntaxe import moderne.
mkdir bcrypt-demo && cd bcrypt-demo
npm init -y
npm pkg set type="module"
node -v # verifiez 22.x ou 24.x
Étape 2 : installer bcrypt
Installez la bibliothèque native bcrypt. Express et la gestion de session viendront plus tard.
npm install bcrypt
npm ls bcrypt # confirme la version installee
Si l’installation échoue avec une erreur node-gyp, passez directement à la section sur bcryptjs ou consultez le dépannage en fin d’article.
Étape 3 : générer un sel explicitement
Pour bien comprendre le mécanisme, générons d’abord un sel à la main avant de le combiner au mot de passe. Créez un fichier hash.js :
// hash.js
import bcrypt from "bcrypt";
const COST = 12;
const motDePasse = "MonSuperMotDePasse!2026";
const sel = await bcrypt.genSalt(COST);
console.log("Sel genere :", sel);
const empreinte = await bcrypt.hash(motDePasse, sel);
console.log("Empreinte :", empreinte);
Exécutez avec node hash.js. Sortie attendue (les valeurs varient à chaque exécution) :
Sel genere : $2b$12$Eq8X9q1mZ3oQpL5vK7nJ0u
Empreinte : $2b$12$Eq8X9q1mZ3oQpL5vK7nJ0u8Y2cF4dG6hR1tS3wV5xZ7aB9cD1eFq
Étape 4 : hacher en une seule ligne
En production, vous n’avez pas besoin de générer le sel séparément. Passez directement le cost à bcrypt.hash() : la bibliothèque crée le sel pour vous. C’est l’approche recommandée car elle réduit les risques d’erreur.
// hash-simple.js
import bcrypt from "bcrypt";
const COST = 12;
const empreinte = await bcrypt.hash("MonSuperMotDePasse!2026", COST);
console.log(empreinte);
// $2b$12$... (60 caracteres, sel inclus)
Notez l’usage de await : l’API asynchrone est obligatoire en production pour ne pas bloquer l’event loop. Nous y revenons à l’étape 12.
Étapes 5 à 8 : vérification, Express et stockage
Étape 5 : vérifier un mot de passe avec bcrypt.compare
Pour vérifier un mot de passe à la connexion, n’essayez jamais de re-hacher puis comparer les chaînes vous-même. Utilisez bcrypt.compare(), qui extrait le sel et le cost de l’empreinte stockée, recalcule le hash et effectue une comparaison sûre.
// verify.js
import bcrypt from "bcrypt";
const empreinteStockee = await bcrypt.hash("secret123", 12);
const ok = await bcrypt.compare("secret123", empreinteStockee);
console.log("Bon mot de passe :", ok); // true
const ko = await bcrypt.compare("mauvais", empreinteStockee);
console.log("Mauvais mot de passe :", ko); // false
bcrypt.compare() renvoie un booléen et gère la comparaison en interne, ce qui réduit l’exposition aux attaques par analyse de temps. Ne comparez jamais deux empreintes avec ===.
Étape 6 : construire la route d’inscription Express
Installez Express, puis créez un serveur avec une route d’inscription. Pour rester focalisé sur bcrypt, nous simulons la base avec un simple tableau en mémoire.
npm install express
// server.js
import express from "express";
import bcrypt from "bcrypt";
const app = express();
app.use(express.json());
const COST = 12;
const utilisateurs = []; // base simulee
app.post("/inscription", async (req, res) => {
const { email, motDePasse } = req.body;
if (!email || !motDePasse) {
return res.status(400).json({ erreur: "Champs manquants" });
}
const empreinte = await bcrypt.hash(motDePasse, COST);
utilisateurs.push({ email, empreinte });
res.status(201).json({ message: "Compte cree" });
});
app.listen(3000, () => console.log("API sur http://localhost:3000"));
Étape 7 : ajouter la route de connexion
La connexion récupère l’empreinte stockée et la compare au mot de passe saisi. Renvoyez toujours le même message d’erreur, que l’email soit inconnu ou le mot de passe faux, pour ne pas révéler quels comptes existent.
// a ajouter dans server.js
app.post("/connexion", async (req, res) => {
const { email, motDePasse } = req.body;
const user = utilisateurs.find((u) => u.email === email);
// message identique dans les deux cas d'echec
const echec = () => res.status(401).json({ erreur: "Identifiants invalides" });
if (!user) return echec();
const valide = await bcrypt.compare(motDePasse, user.empreinte);
if (!valide) return echec();
res.json({ message: "Connexion reussie" });
});
Étape 8 : tester l’API avec curl
Lancez le serveur avec node server.js et testez les deux routes.
curl -X POST http://localhost:3000/inscription \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","motDePasse":"Tr3sB0nMdp!"}'
# {"message":"Compte cree"}
curl -X POST http://localhost:3000/connexion \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","motDePasse":"Tr3sB0nMdp!"}'
# {"message":"Connexion reussie"}
curl -X POST http://localhost:3000/connexion \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","motDePasse":"faux"}'
# {"erreur":"Identifiants invalides"}
Pour ajouter ensuite des jetons de session, combinez bcrypt avec une couche d’authentification stateless comme dans notre tutoriel sur l’authentification JWT en Node.js.
Étapes 9 à 12 : cost adaptatif, rehash, migration et performance
Étape 9 : mesurer et choisir le bon cost factor
Le bon cost dépend de votre matériel. L’objectif est de viser environ 250 ms à 1 seconde par hachage. Mesurez sur votre serveur réel avec ce banc d’essai :
// bench.js
import bcrypt from "bcrypt";
for (const cost of [10, 11, 12, 13, 14]) {
const debut = process.hrtime.bigint();
await bcrypt.hash("motDePasseDeTest", cost);
const ms = Number(process.hrtime.bigint() - debut) / 1e6;
console.log(`cost ${cost} : ${ms.toFixed(0)} ms`);
}
Exemple de sortie sur une machine de développement récente :
cost 10 : 65 ms
cost 11 : 130 ms
cost 12 : 260 ms
cost 13 : 520 ms
cost 14 : 1040 ms
Ici, le cost 12 ou 13 atteint la cible. Choisissez la valeur la plus élevée que votre serveur tolère sans dégrader l’expérience utilisateur à la connexion. Réévaluez-la chaque année : le matériel s’accélère, le cost doit suivre.
Étape 10 : rehacher quand le cost a changé
Quand vous augmentez le cost, les empreintes existantes restent au vieux coût. Profitez de la connexion (le seul moment où vous avez le mot de passe en clair) pour rehacher silencieusement les comptes obsolètes. Lisez le cost stocké dans l’empreinte avec bcrypt.getRounds().
// rehash a la connexion
const valide = await bcrypt.compare(motDePasse, user.empreinte);
if (!valide) return echec();
const COST_CIBLE = 13;
if (bcrypt.getRounds(user.empreinte) < COST_CIBLE) {
user.empreinte = await bcrypt.hash(motDePasse, COST_CIBLE);
// persister user.empreinte en base
}
res.json({ message: "Connexion reussie" });
Étape 11 : migrer depuis SHA-256 ou MD5
Si vous reprenez une base où les mots de passe sont stockés en SHA-256 ou MD5 non salé, ne demandez pas à tous vos utilisateurs de réinitialiser leur mot de passe d'un coup. La technique du double hachage permet une migration transparente : enveloppez l'ancien hash dans bcrypt.
import bcrypt from "bcrypt";
import { createHash } from "node:crypto";
// migration de masse : bcrypt(sha256(ancienMdp))
function envelopper(ancienHashSha256) {
return bcrypt.hash(ancienHashSha256, 12);
}
// a la connexion d'un compte migre
const sha = createHash("sha256").update(motDePasse).digest("hex");
const valide = await bcrypt.compare(sha, user.empreinteEnveloppee);
Marquez ces comptes comme « migrés » et, à la première connexion réussie, remplacez l'empreinte enveloppée par un bcrypt.hash(motDePasse, COST) direct. La base se nettoie d'elle-même au fil des connexions. Pour les fondamentaux du hachage, voyez notre guide sur les fonctions de hachage cryptographiques.
Étape 12 : préserver l'event loop avec l'API asynchrone
bcrypt expose des fonctions synchrones (hashSync, compareSync) et asynchrones. En production, utilisez toujours les versions asynchrones : la variante native exécute le calcul lourd dans un thread du pool libuv, ce qui laisse votre serveur répondre à d'autres requêtes pendant le hachage.
// MAUVAIS : bloque l'event loop sur ~260 ms
const h = bcrypt.hashSync(motDePasse, 12);
// BON : libere le thread principal pendant le calcul
const h = await bcrypt.hash(motDePasse, 12);
Sous forte charge, le hachage synchrone fige le serveur : chaque connexion attend la fin du calcul précédent. L'API asynchrone, elle, parallélise sur plusieurs threads et garde la latence stable. Le projet complet est désormais en place : initialisation, hachage, vérification, rehash et migration.
bcrypt ou bcryptjs : quelle bibliothèque choisir
Deux paquets dominent l'écosystème npm. bcrypt est une liaison native vers une implémentation C++, plus rapide. bcryptjs est une réimplémentation pure JavaScript, plus lente mais sans aucune dépendance native, donc immédiatement installable partout, y compris dans des environnements où la compilation est impossible (certains conteneurs, fonctions serverless, navigateur).
| Critère | bcrypt (natif) | bcryptjs (pur JS) |
|---|---|---|
| Implémentation | C++ via node-gyp | JavaScript pur |
| Performance | Plus rapide | Plus lente (souvent 2 à 3x) |
| Installation | Compilation possible | Aucune compilation |
| Serverless / Alpine | Parfois problématique | Toujours compatible |
| Compatibilité des hash | Format $2b$ standard | Format $2b$ standard |
| API | hash, compare, genSalt | Identique |
Bonne nouvelle : les deux produisent des empreintes interopérables. Un hash créé par bcrypt se vérifie avec bcryptjs et inversement. Vous pouvez donc développer avec l'un et déployer avec l'autre sans casser vos comptes. Règle simple : privilégiez bcrypt pour la performance sur un serveur classique, et basculez sur bcryptjs dès que la compilation native pose problème.
La limite des 72 octets et le pré-hachage SHA-256
bcrypt ignore tout ce qui dépasse 72 octets dans le mot de passe d'entrée. Pour de l'ASCII, cela fait 72 caractères, mais un mot de passe riche en accents ou en émojis atteint cette limite bien plus vite, car un caractère UTF-8 peut occuper plusieurs octets. Au-delà, les octets supplémentaires sont silencieusement ignorés. Deux mots de passe qui ne diffèrent qu'après le 72e octet produisent alors le même hash.
Un problème connexe est la troncature au caractère NUL dans d'anciennes implémentations. Pour les phrases de passe longues ou les jetons, la parade recommandée par l'OWASP est de pré-hacher en SHA-256 puis d'encoder en Base64 avant de passer à bcrypt. Cela ramène toute entrée à une longueur fixe et sûre.
import bcrypt from "bcrypt";
import { createHash } from "node:crypto";
function preHacher(motDePasse) {
// SHA-256 -> Base64 : longueur fixe de 44 caracteres, < 72 octets
return createHash("sha256").update(motDePasse, "utf8").digest("base64");
}
const empreinte = await bcrypt.hash(preHacher(motDePasseTresLong), 12);
const ok = await bcrypt.compare(preHacher(saisieUtilisateur), empreinte);
Appliquez le pré-hachage des deux côtés, à l'inscription comme à la vérification, sinon les empreintes ne correspondront pas. Si tous vos mots de passe restent courts et en ASCII, ce pré-hachage est facultatif, mais il ne coûte rien et protège l'avenir.
Ajouter un pepper : un secret applicatif en plus du sel
Le sel est stocké avec l'empreinte ; le pepper, lui, est un secret unique à votre application, conservé en dehors de la base de données (variable d'environnement, gestionnaire de secrets, HSM). Si seule la base fuite, l'attaquant n'a pas le pepper et le crackage devient impraticable. L'OWASP recommande le pepper comme défense complémentaire, jamais en remplacement du sel.
import bcrypt from "bcrypt";
import { createHmac } from "node:crypto";
const PEPPER = process.env.PEPPER; // jamais en base, jamais dans le code
function avecPepper(motDePasse) {
return createHmac("sha256", PEPPER).update(motDePasse).digest("base64");
}
const empreinte = await bcrypt.hash(avecPepper(motDePasse), 12);
const ok = await bcrypt.compare(avecPepper(saisie), empreinte);
Combiner pepper et pré-hachage via HMAC-SHA256 fait d'une pierre deux coups : vous contournez la limite des 72 octets et vous ajoutez le secret applicatif. Attention toutefois : changer de pepper invalide toutes les empreintes existantes. Prévoyez une rotation gérée (versionnez le pepper, rehachez à la connexion).
Stocker l'empreinte en base : PostgreSQL et MongoDB
Le tableau en mémoire des exemples précédents disparaît au redémarrage. En production, l'empreinte va dans une base de données. Le point crucial reste le même quelle que soit la base : réservez une colonne assez large pour 60 caractères, et ne stockez jamais le mot de passe en clair à côté. Voici le schéma minimal côté PostgreSQL.
-- schema PostgreSQL
CREATE TABLE utilisateurs (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
empreinte VARCHAR(72) NOT NULL, -- 60 suffit, 72 par securite
cree_le TIMESTAMPTZ DEFAULT now()
);
Côté Node.js, l'inscription devient une insertion paramétrée. Utilisez toujours des requêtes paramétrées pour éviter l'injection SQL, jamais de concaténation de chaînes. L'empreinte produite par bcrypt ne contient que des caractères sûrs, mais l'email, lui, vient de l'utilisateur.
// inscription avec pg
import bcrypt from "bcrypt";
import pg from "pg";
const pool = new pg.Pool();
const COST = 12;
async function inscrire(email, motDePasse) {
const empreinte = await bcrypt.hash(motDePasse, COST);
await pool.query(
"INSERT INTO utilisateurs (email, empreinte) VALUES ($1, $2)",
[email, empreinte]
);
}
Avec MongoDB et Mongoose, le réflexe le plus propre consiste à hacher dans un hook pre("save"), de sorte que le mot de passe soit toujours haché avant toute écriture, même si plusieurs routes créent des comptes. Pensez à ne hacher que si le champ a changé, sinon une simple mise à jour de profil re-hacherait inutilement l'empreinte.
// modele Mongoose
import mongoose from "mongoose";
import bcrypt from "bcrypt";
const schema = new mongoose.Schema({
email: { type: String, unique: true, required: true },
empreinte: { type: String, required: true },
});
schema.pre("save", async function () {
if (!this.isModified("empreinte")) return;
this.empreinte = await bcrypt.hash(this.empreinte, 12);
});
export const Utilisateur = mongoose.model("Utilisateur", schema);
Dans les deux cas, n'exposez jamais le champ empreinte dans une réponse API. Avec Mongoose, marquez-le select: false ou retirez-le explicitement avant de sérialiser l'objet utilisateur. Une empreinte n'est pas un secret aussi sensible qu'un mot de passe en clair, mais la divulguer ne sert qu'à faciliter une attaque hors ligne.
Écrire un test automatisé pour le hachage
Un test garantit que votre logique d'inscription et de connexion ne régresse pas. Node.js intègre désormais un lanceur de tests natif, ce qui évite toute dépendance externe. Créez un fichier auth.test.js et vérifiez les trois invariants essentiels : une empreinte ne ressemble pas au mot de passe, un bon mot de passe valide, un mauvais échoue.
// auth.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import bcrypt from "bcrypt";
test("l'empreinte differe du mot de passe", async () => {
const mdp = "MonMotDePasse!2026";
const empreinte = await bcrypt.hash(mdp, 12);
assert.notEqual(empreinte, mdp);
assert.ok(empreinte.startsWith("$2b$12$"));
});
test("compare valide le bon mot de passe", async () => {
const empreinte = await bcrypt.hash("secret", 12);
assert.equal(await bcrypt.compare("secret", empreinte), true);
});
test("compare rejette le mauvais mot de passe", async () => {
const empreinte = await bcrypt.hash("secret", 12);
assert.equal(await bcrypt.compare("intrus", empreinte), false);
});
Lancez la suite avec node --test. Sortie attendue :
$ node --test
# tests 3
# pass 3
# fail 0
Ces trois tests couvrent le cœur de votre sécurité d'authentification. Ajoutez-en un quatrième pour le rehash et un cinquième pour le pré-hachage si vous l'utilisez. Un cost réduit (par exemple 8) accélère les tests sans toucher au cost de production, externalisé en variable d'environnement.
5 pièges courants à éviter avec bcrypt
Ces erreurs reviennent constamment dans les revues de code. Les connaître à l'avance vous épargne des failles silencieuses.
- Comparer avec
===au lieu debcrypt.compare(). Re-hacher puis comparer les chaînes ne fonctionne pas, car chaque hachage génère un sel différent. Utilisez toujourscompare(). - Utiliser un cost trop bas (8 ou moins). Le hachage devient quasi instantané pour l'attaquant. Visez au moins 10, idéalement 12, calibré sur votre matériel.
- Bloquer l'event loop avec
hashSync. Sous charge, le serveur se fige. Utilisez les versions asynchrones avecawait. - Tronquer la colonne en base. Une empreinte bcrypt fait 60 caractères ; un
VARCHAR(50)coupe le hash et rend la vérification impossible. Prévoyez au moins 60, voire 72. - Oublier la limite des 72 octets. Pour des phrases de passe longues ou non ASCII, pré-hachez en SHA-256, sinon une partie du mot de passe est ignorée.
Astuces avancées pour la production
Au-delà du code de base, quelques pratiques distinguent une intégration robuste d'un prototype. Première astuce : imposez une limitation de débit sur la route de connexion. Même avec bcrypt, un attaquant qui peut tester sans limite finira par deviner les mots de passe faibles. Un middleware de rate limiting bloque les attaques par bourrage d'identifiants.
Deuxième astuce : externalisez le cost dans une variable d'environnement (process.env.BCRYPT_COST) pour l'ajuster sans redéployer. Troisième astuce : enregistrez la durée moyenne de hachage dans vos métriques. Si elle chute sous 200 ms après un changement de serveur, c'est le signal qu'il faut augmenter le cost. Quatrième astuce : ne logguez jamais le mot de passe ni l'empreinte, même en niveau debug ; un log compromis annule tous vos efforts.
Enfin, complétez bcrypt par une politique de mots de passe sensée : longueur minimale de 12 caractères, vérification contre les listes de mots de passe compromis, et surtout proposez l'authentification à deux facteurs. Le hachage protège la base ; l'hygiène globale protège les comptes. Pour aller plus loin sur l'authentification déléguée, comparez les approches dans notre guide OAuth2 et OpenID Connect en Node.js.
Dépannage : 8 erreurs fréquentes et leurs solutions
Voici les problèmes que vous rencontrerez le plus souvent en intégrant bcrypt, avec leur cause et leur remède.
| Symptôme | Cause probable | Solution |
|---|---|---|
node-gyp échoue à l'installation | Outils de compilation absents | Installer les build tools ou utiliser bcryptjs |
compare() renvoie toujours false | Empreinte tronquée en base (VARCHAR trop court) | Élargir la colonne à 60+ caractères |
| Erreur « data and hash arguments required » | Empreinte undefined (mauvais champ) | Vérifier le nom de colonne et la requête SQL |
| Mots de passe longs acceptés à tort | Limite des 72 octets | Pré-hacher en SHA-256 + Base64 |
| Connexion très lente sous charge | Usage de hashSync/compareSync | Passer aux versions asynchrones |
Hash $2a$ ne se vérifie pas | Ancien format issu d'une autre lib | bcrypt vérifie $2a$/$2b$/$2y$ : revérifier le champ |
| Cannot find module 'bcrypt' | Module non installé ou mauvais runtime | npm install bcrypt et vérifier type:module |
| Hash différent à chaque exécution | Comportement normal (sel aléatoire) | Ne pas comparer les hash entre eux, utiliser compare() |
Le huitième cas mérite insistance car il déroute les débutants : oui, hacher deux fois le même mot de passe donne deux empreintes différentes. C'est voulu, grâce au sel aléatoire. La seule façon correcte de vérifier reste bcrypt.compare(), qui sait extraire le sel de l'empreinte stockée.
Related Coverage
- Sécurité des mots de passe : longueur, hachage et gestionnaires
- Authentification JWT en Node.js : 12 étapes
- OAuth2 en Node.js : flux sécurisé en 12 étapes
- Les fonctions de hachage cryptographiques expliquées
- Comparatif des gestionnaires de mots de passe 2026
- SHA-256 expliqué : l'empreinte de 256 bits
- Dossier : sécurité en ligne
Foire aux questions sur bcrypt en Node.js
Quel cost factor bcrypt utiliser en 2026 ?
Visez un cost d'au moins 10, et 12 comme valeur de référence. Le critère réel n'est pas le chiffre mais le temps : calibrez le cost pour que chaque hachage prenne entre 250 ms et 1 seconde sur votre serveur de production. Mesurez avec le banc d'essai de l'étape 9 et réévaluez chaque année.
bcrypt ou Argon2, lequel est le plus sûr ?
L'OWASP recommande Argon2id en premier choix pour les nouveaux projets, car sa résistance mémoire complique les attaques par GPU et ASIC. bcrypt reste néanmoins parfaitement acceptable, plus simple à régler et éprouvé depuis plus de vingt ans. Pour la plupart des applications web, bcrypt bien configuré suffit.
Faut-il stocker le sel séparément avec bcrypt ?
Non. bcrypt intègre le sel directement dans l'empreinte de 60 caractères. Vous n'avez besoin que d'une seule colonne pour stocker le hash. bcrypt.compare() extrait automatiquement le sel et le cost de cette chaîne pour vérifier le mot de passe.
Pourquoi bcrypt ignore-t-il les mots de passe de plus de 72 octets ?
C'est une limite de l'algorithme issu de Blowfish. Au-delà de 72 octets, les caractères supplémentaires ne sont pas pris en compte. Pour gérer des phrases de passe longues ou riches en caractères accentués, pré-hachez le mot de passe en SHA-256 puis encodez-le en Base64 avant de le passer à bcrypt.
Dois-je utiliser bcrypt ou bcryptjs ?
Utilisez bcrypt (natif) pour la performance sur un serveur classique. Basculez sur bcryptjs (pur JavaScript) si la compilation native échoue, par exemple en serverless ou sur des images Alpine minimalistes. Les deux produisent des empreintes interopérables au format $2b$.
Comment migrer mes mots de passe MD5 ou SHA-256 vers bcrypt ?
Enveloppez les anciens hash dans bcrypt sans demander à vos utilisateurs de réinitialiser leur mot de passe. À chaque connexion réussie d'un compte migré, remplacez l'empreinte enveloppée par un hash bcrypt direct du mot de passe. La base se met à jour progressivement, comme détaillé à l'étape 11.
bcrypt est-il résistant à l'informatique quantique ?
Le hachage de mots de passe n'est pas menacé de la même façon que le chiffrement asymétrique par les ordinateurs quantiques. L'algorithme de Grover offrirait au mieux une accélération quadratique des recherches, que l'on compense en augmentant légèrement le cost. bcrypt avec un cost adéquat reste pertinent face à cette menace.
Sources et références : OWASP Password Storage Cheat Sheet, bcrypt (Wikipedia), node.bcrypt.js, Node.js Releases, Express. Article publié le 13 juin 2026.




