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:
What password hashing is and why storing plain text passwords is dangerous
What a "salt" is and how it protects against rainbow table attacks
Installing and configuring bcryptjs in our Express backend
Hashing the password before saving a new user with bcrypt.hash
Comparing a hashed password on login with bcrypt.compare
Best practices for choosing salt rounds
---
What 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:-
Understood why storing plain text passwords is a critical security flaw.
Learned what a salt is and how it defeats rainbow table attacks.
Installed and configured bcryptjs in the Express backend.
Generated a salt and hashed the password using bcrypt.genSalt and bcrypt.hash.
Verified login credentials securely using bcrypt.compare.
Understood the trade-off between salt rounds and performance.
Prepared CloudNoteBook's authentication system for the next step: generating JWT tokens on signup and login.