User Authentication in Node.js
# User Authentication in Node.js
Welcome to Chapter 23! You've built CRUD operations, which means you can save users to a database. But saving a user's password in plain text (like "password123") is a severe security violation.
If a hacker accesses your database, they will steal those passwords and use them to log into the users' bank accounts, emails, and social media. In this chapter, we will learn how to build a secure registration and login system by hashing passwords using a package called bcrypt.
---
1. Introduction
Authentication is the process of verifying who a user is. A standard auth flow has two parts:
- 1. Registration (Signup): The user provides an email and password. The backend scrambles (hashes) the password so it is unreadable, then saves it to the database.
- 2. Login: The user provides their email and plain-text password. The backend finds the user by email, scrambles the provided password, and checks if it matches the scrambled password in the database.
Hashing is a one-way street. You can turn "hello" into "xj82$9", but you can *never* turn "xj82$9" back into "hello". Even you (the database owner) will not know your users' passwords!
---
2. Learning Objectives
By the end of this chapter, you will be able to:
- Understand the difference between hashing and encryption.
-
Install and use the
bcryptjsNPM package.
- Hash a password before saving a user to MongoDB.
- Write a secure Registration POST route.
- Write a secure Login POST route.
-
Use
bcrypt.compare()to verify user credentials.
---
3. Beginner-Friendly Explanations
Encryption vs Hashing
- Encryption: Like locking a box with a key. You can unlock it later to get the original item back. (Two-way).
- Hashing: Like putting an apple in a blender. You get apple sauce. You can never turn the apple sauce back into an apple. (One-way). We use hashing for passwords!
What is a "Salt"?
If two users have the password"12345", a basic hash function would give them the exact same hash. Hackers use "Rainbow Tables" to reverse-engineer common hashes.
Bcrypt automatically generates a random string called a Salt and adds it to the password before hashing it. This ensures that even if two users have the same password, their database hashes will look completely different!
---
4. Syntax Explanation
Let's look at how to use the bcrypt package to hash a string.
```javascript id="ch23-syntax-1" const bcrypt = require('bcryptjs');
async function hashMyPassword() { const plainTextPassword = "mySuperSecretPassword"; // 1. Generate a salt (10 is the number of "rounds", meaning how complex it is) const salt = await bcrypt.genSalt(10); // 2. Hash the password using the salt const hashedPassword = await bcrypt.hash(plainTextPassword, salt); console.log(hashedPassword); // Output: $2a$10$C8wHl.T6rZ/... (A 60-character scrambled string) }
hashMyPassword();
javascript id="ch23-code-1" const bcrypt = require('bcryptjs'); // ... express and mongoose setup
app.post('/api/register', async (req, res) => { try { const { email, password } = req.body;
// 1. Check if user already exists const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(400).json({ error: "Email already registered" }); }
// 2. Hash the password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt);
// 3. Create the user with the HASHED password const newUser = await User.create({ email: email, password: hashedPassword });
res.status(201).json({ message: "User registered successfully!" }); } catch (err) { res.status(500).json({ error: err.message }); } });
javascript id="ch23-code-2" app.post('/api/login', async (req, res) => { try { const { email, password } = req.body;
// 1. Find user by email const user = await User.findOne({ email }); if (!user) { return res.status(400).json({ error: "Invalid email or password" }); }
// 2. Compare passwords using bcrypt // Returns a boolean (true if match, false if not) const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) { return res.status(400).json({ error: "Invalid email or password" }); }
// 3. Success! res.status(200).json({ message: "Login successful!" });
} catch (err) { res.status(500).json({ error: err.message }); } });
javascript id="ch23-mini-project" const express = require('express'); const mongoose = require('mongoose'); const bcrypt = require('bcryptjs');
const app = express(); app.use(express.json());
// 1. Connect to DB (Replace URI!) mongoose.connect('mongodb+srv://admin:pass@cluster.mongodb.net/AuthDB') .then(() => console.log("DB Connected")) .catch(err => console.error(err));
// 2. User Schema const userSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true }, password: { type: String, required: true } }); const User = mongoose.model('User', userSchema);
// 3. Register Route app.post('/register', async (req, res) => { try { const { email, password } = req.body; // Hash password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); // Save user await User.create({ email, password: hashedPassword }); res.status(201).json({ message: "Registered successfully" }); } catch (err) { res.status(400).json({ error: "Registration failed. Email might exist." }); } });
// 4. Login Route app.post('/login', async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user) return res.status(400).json({ error: "Invalid credentials" }); const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) return res.status(400).json({ error: "Invalid credentials" }); res.status(200).json({ message: "Welcome back!" }); } catch (err) { res.status(500).json({ error: "Server error" }); } });
app.listen(3000, () => console.log("Auth Server Running"));
``
---
12. Coding Challenges
Challenge 1: Inside the Register route, write validation logic that prevents registration if the password does not contain at least one number. (Hint: Use JavaScript's Regex or .includes()).
Challenge 2: Currently, MongoDB handles duplicate emails by throwing an ugly database error. Update the Register route to use await User.findOne({ email }) *before* attempting to create the user. If they exist, return a custom 400 JSON error.
---
13. MCQs with Answers
Q1: What is the purpose of hashing a password? A) To make it load faster from the database. B) To compress the string to save space. C) To convert it into an unreadable string so even database administrators cannot see the actual password. D) To allow the user to easily reset it. Answer: C
Q2: Which NPM package is recommended for hashing passwords in Node.js?
A) crypto
B) bcryptjs
C) hash-maker
D) password-js
Answer: B
Q3: Why does bcrypt use a "salt"? A) To ensure that two users with the exact same password end up with completely different database hashes. B) To make the password easier to decrypt. C) To increase the length of the string. D) To satisfy MongoDB requirements. Answer: A
Q4: Which function must you use to check if a user typed the correct password during login?
A) bcrypt.verify()
B) bcrypt.check()
C) bcrypt.compare()
D) user.password === req.body.password
Answer: C
---
14. Interview Questions
- 1. Why is encryption not suitable for storing passwords?
-
2.
What happens during bcrypt.compare(plainText, hash)
?
password, hashes it using that exact same salt, and checks if the new resulting hash perfectly matches the stored hash.
---
15. FAQs
Q: Can I manually un-hash a password if a user forgets it?
A: No! Because hashing is one-way, "forgot password" features do not send the user their old password. Instead, you email them a special link that allows them to overwrite their old hash with a completely new one.
Q: Is
bcryptjs slow?
A: Yes, intentionally! It is designed to be mathematically slow. If a hacker steals your database and tries to guess passwords by hashing millions of words per second, bcrypt slows down their computers, making a brute-force attack take decades.
---
16. Summary
-
Never store plain text passwords.
-
Use
bcryptjs to securely hash passwords.
-
Hashing is a one-way mathematical function.
-
A Salt randomizes the hash to protect against rainbow table attacks.
-
In registration, use
bcrypt.hash().
-
In login, use
bcrypt.compare().
---
17. Next Chapter Recommendation
Our users can now log in safely! But here's the problem: HTTP is "stateless." Once the login request finishes, the server instantly forgets who the user is. If they try to visit
/profile`, the server will block them. In Chapter 24: JWT Authentication, we will learn how to generate "Tokens" (virtual ID cards) that keep users logged in across multiple requests!