Password Hashing with Salt using Bcrypt for our CloudNoteBook App ๐๐
Right now, when a user signs up on CloudNoteBook, their password is stored directly
in MongoDB as plain text. That's a serious security risk โ if our database is ever
leaked or accessed by someone with bad intentions, every single user's password is exposed
as-is. In this tutorial, we'll fix that using bcrypt, an industry-standard hashing
algorithm that scrambles passwords with a unique salt before they're ever saved.
In this tutorial, we will learn:
bcryptjs in our Express backendbcrypt.hashbcrypt.compareWhat is Password Hashing?
Hashing converts a readable password like MyPassword123 into a fixed-length,
irreversible string of characters โ something like $2a$10$N9qo8uLOickgx2Zm....
Unlike encryption, hashing cannot be reversed back into the original password. When a user
logs in later, we don't "decrypt" the stored hash โ instead we hash the entered password
again and compare the two hashes.
What is a Salt, and Why Does It Matter?
A salt is a random string added to a password before it's hashed. Without a salt,
two users with the same password (e.g. 123456) would end up with the exact same
hash โ making it easy for attackers to use precomputed "rainbow tables" to crack many
passwords at once. Bcrypt automatically generates a unique salt for every password, so
even identical passwords produce completely different hashes.
Step 1: Install bcryptjs
In your backend folder, install the bcryptjs package. It's a pure JavaScript
implementation of bcrypt, so it works without any native build tools.
npm install bcryptjs
Step 2: Import bcryptjs in the Auth Routes
Open your routes/auth.js file (or wherever your signup route lives) and import
bcryptjs along with your existing User model.
const express = require("express");
const router = express.Router();
const User = require("../models/User");
const bcrypt = require("bcryptjs");
const { body, validationResult } = require("express-validator");
Step 3: Generate a Salt and Hash the Password
Inside the create-user (signup) route, instead of saving req.body.password directly,
we first generate a salt using bcrypt.genSalt, then hash the password with that salt
using bcrypt.hash.
// ROUTE 1: Create a User using: POST "/api/auth/createuser"
router.post(
"/createuser",
[
body("name", "Enter a valid name").isLength({ min: 3 }),
body("email", "Enter a valid email").isEmail(),
body("password", "Password must be at least 5 characters").isLength({ min: 5 }),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
let user = await User.findOne({ email: req.body.email });
if (user) {
return res.status(400).json({ error: "A user with this email already exists" });
}
// Generate a salt with 10 rounds
const salt = await bcrypt.genSalt(10);
// Hash the password using the generated salt
const secPassword = await bcrypt.hash(req.body.password, salt);
user = await User.create({
name: req.body.name,
email: req.body.email,
password: secPassword,
});
res.json({ success: true, user: { id: user.id, name: user.name } });
} catch (error) {
console.error(error.message);
res.status(500).send("Internal Server Error");
}
}
);
Step 4: Compare Passwords on Login
When a user logs in, we never re-hash and compare strings manually. Instead, we use
bcrypt.compare, which takes the plain text password entered and the stored hash,
and returns true or false depending on whether they match.
// ROUTE 2: Authenticate a User using: POST "/api/auth/login"
router.post(
"/login",
[
body("email", "Enter a valid email").isEmail(),
body("password", "Password cannot be blank").exists(),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
try {
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ error: "Please try to login with correct credentials" });
}
const passwordCompare = await bcrypt.compare(password, user.password);
if (!passwordCompare) {
return res.status(400).json({ error: "Please try to login with correct credentials" });
}
res.json({ success: true, message: "Login successful" });
} catch (error) {
console.error(error.message);
res.status(500).send("Internal Server Error");
}
}
);
Step 5: Full Updated auth.js
Here is the complete auth.js file with password hashing and comparison wired
into the signup and login routes.
const express = require("express");
const router = express.Router();
const User = require("../models/User");
const bcrypt = require("bcryptjs");
const { body, validationResult } = require("express-validator");
// ROUTE 1: Create a User using: POST "/api/auth/createuser"
router.post(
"/createuser",
[
body("name", "Enter a valid name").isLength({ min: 3 }),
body("email", "Enter a valid email").isEmail(),
body("password", "Password must be at least 5 characters").isLength({ min: 5 }),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
let user = await User.findOne({ email: req.body.email });
if (user) {
return res.status(400).json({ error: "A user with this email already exists" });
}
const salt = await bcrypt.genSalt(10);
const secPassword = await bcrypt.hash(req.body.password, salt);
user = await User.create({
name: req.body.name,
email: req.body.email,
password: secPassword,
});
res.json({ success: true, user: { id: user.id, name: user.name } });
} catch (error) {
console.error(error.message);
res.status(500).send("Internal Server Error");
}
}
);
// ROUTE 2: Authenticate a User using: POST "/api/auth/login"
router.post(
"/login",
[
body("email", "Enter a valid email").isEmail(),
body("password", "Password cannot be blank").exists(),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
try {
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ error: "Please try to login with correct credentials" });
}
const passwordCompare = await bcrypt.compare(password, user.password);
if (!passwordCompare) {
return res.status(400).json({ error: "Please try to login with correct credentials" });
}
res.json({ success: true, message: "Login successful" });
} catch (error) {
console.error(error.message);
res.status(500).send("Internal Server Error");
}
}
);
module.exports = router;
Key Bcrypt Methods Explained
| Method | Purpose | Used In |
|---|---|---|
bcrypt.genSalt(rounds) |
Generates a random salt; higher rounds = more secure but slower | Signup route, before hashing |
bcrypt.hash(password, salt) |
Combines the password and salt into a one-way hash | Signup route, before saving the user |
bcrypt.compare(password, hash) |
Hashes the entered password and checks it against the stored hash | Login route, to verify credentials |
Features and Learnings:-
bcryptjs in the Express backend.bcrypt.genSalt and bcrypt.hash.bcrypt.compare.