Authentication Basics
Security is paramount. Today you learn to register users safely, hash passwords with bcrypt, and implement secure login systems that protect user credentials.
Why Password Security Matters
Storing plain text passwords is catastrophic. When databases get breached (and they do), attackers gain immediate access to all user accounts. Password hashing makes stolen data useless.
Never store plain text passwords. Never. Ever. Always hash passwords before storing them. This is non-negotiable in professional development.
Understanding Bcrypt
Bcrypt is a password hashing algorithm designed to be slow and computationally expensive, making brute-force attacks impractical:
How Bcrypt Works
- Salting - Adds random data to each password before hashing, preventing rainbow table attacks
- Cost Factor - Controls how slow the hashing is (higher = more secure but slower)
- One-Way - Cannot reverse a hash back to the original password
- Built-in Salt - Salt is stored with the hash, no separate storage needed
npm install bcrypt
Hashing Passwords
const bcrypt = require('bcrypt');
// Salt rounds (cost factor) - 10-12 is recommended
// Higher = more secure but slower
const SALT_ROUNDS = 10;
// Hash a password (async recommended)
async function hashPassword(plainPassword) {
// bcrypt.hash generates salt and hashes in one step
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);
return hashedPassword;
}
// Example output (will be different each time due to random salt):
// $2b$10$N9qo8uLOickgx2ZMRZoMye.IjqR5rKP5VrVYJE4JkFd6HQG3N9O.i
// ^ ^ ^ ^
// | | | |
// | | 22-char salt 31-char hash
// | cost factor (10)
// bcrypt identifier
// Verify a password against stored hash
async function verifyPassword(plainPassword, hashedPassword) {
// bcrypt.compare extracts salt from hash automatically
const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
return isMatch; // true if password matches, false otherwise
}
User Model with Password Hashing
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
// Email as unique identifier for login
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
// Password with minimum length requirement
password: {
type: String,
required: [true, 'Password is required'],
minlength: [8, 'Password must be at least 8 characters'],
select: false // Never include password in queries by default
},
// Display name
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
// Account creation timestamp
createdAt: {
type: Date,
default: Date.now
}
});
// Pre-save middleware to hash password before saving
userSchema.pre('save', async function(next) {
// Only hash if password was modified (or new)
// This prevents re-hashing on unrelated updates
if (!this.isModified('password')) {
return next();
}
try {
// Hash password with cost factor 10
this.password = await bcrypt.hash(this.password, 10);
next();
} catch (err) {
next(err);
}
});
// Instance method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
// 'this.password' is the hashed password from database
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
Registration Endpoint
const express = require('express');
const router = express.Router();
const User = require('../models/User');
// POST /api/auth/register - Create new user account
router.post('/register', async (req, res) => {
try {
// Extract registration data from request body
const { email, password, name } = req.body;
// Check if user with this email already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
error: 'Email already registered'
});
}
// Create new user - password is hashed automatically by pre-save middleware
const user = await User.create({
email,
password, // Plain password - will be hashed by middleware
name
});
// Return success without exposing password
res.status(201).json({
message: 'User registered successfully',
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (err) {
// Handle validation errors
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ error: messages.join(', ') });
}
res.status(500).json({ error: 'Registration failed' });
}
});
module.exports = router;
Login Endpoint
// POST /api/auth/login - Authenticate user
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validate input exists
if (!email || !password) {
return res.status(400).json({
error: 'Email and password are required'
});
}
// Find user by email, explicitly include password field
// (+password overrides select: false in schema)
const user = await User.findOne({ email }).select('+password');
// Check if user exists
if (!user) {
// Use generic message to prevent email enumeration
return res.status(401).json({
error: 'Invalid email or password'
});
}
// Compare provided password with stored hash
const isValidPassword = await user.comparePassword(password);
if (!isValidPassword) {
// Same generic message - don't reveal which field is wrong
return res.status(401).json({
error: 'Invalid email or password'
});
}
// Login successful - return user info
// In Day 10, we'll add JWT token here
res.json({
message: 'Login successful',
user: {
id: user._id,
email: user.email,
name: user.name
}
});
} catch (err) {
res.status(500).json({ error: 'Login failed' });
}
});
Always return the same error message for invalid email AND invalid password. If you say "Email not found" vs "Wrong password", attackers can enumerate which emails exist in your system.
Complete Authentication Server
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/authapp')
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
// User Schema with password hashing
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, minlength: 8, select: false },
name: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Compare password method
userSchema.methods.comparePassword = function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
const User = mongoose.model('User', userSchema);
// Register endpoint
app.post('/api/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Check existing user
const exists = await User.findOne({ email });
if (exists) {
return res.status(400).json({ error: 'Email already registered' });
}
// Create user
const user = await User.create({ email, password, name });
res.status(201).json({
message: 'Registration successful',
user: { id: user._id, email: user.email, name: user.name }
});
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user with password included
const user = await User.findOne({ email }).select('+password');
// Verify user exists and password matches
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
res.json({
message: 'Login successful',
user: { id: user._id, email: user.email, name: user.name }
});
} catch (err) {
res.status(500).json({ error: 'Login failed' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Video Resources
Knowledge Check
1. Why should you use the same error message for invalid email and invalid password?
2. What does `select: false` do in a Mongoose schema field?
3. What does the salt in bcrypt protect against?
Build a Complete Auth System
Extend the authentication system with additional features:
- Add password confirmation field during registration (password must match confirmPassword)
- Implement password strength validation (must include uppercase, lowercase, number, special char)
- Add a "forgot password" endpoint that generates a reset token
- Create a profile endpoint that returns user data (excluding password)
- Implement rate limiting on login attempts (max 5 per minute)