Introduction
Security is paramount in modern web applications. User authentication ensures that only authorized users can access protected resources. In this lesson, you'll learn how to implement secure user registration and login, hash passwords with bcrypt, generate and verify JWT tokens, protect routes with authentication middleware, and follow security best practices.
Learning Objectives
By the end of this lesson, you will be able to:
- Implement user registration and login
- Hash passwords securely with bcrypt
- Generate and verify JWT tokens
- Protect routes with authentication middleware
- Implement authorization and role-based access
- Follow security best practices
Prerequisites
- Completion of Lessons 01-08
- Understanding of HTTP headers and cookies
- Knowledge of Express middleware
Estimated Time
4 hours (including practice exercises)
Authentication vs Authorization
- Authentication - Verifying who you are (login)
- Authorization - Verifying what you can access (permissions)
Password Hashing with Bcrypt
Never store passwords in plain text! Use bcrypt to hash passwords securely.
Installation
npm install bcrypt
User Model with Password Hashing
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters']
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, { timestamps: true });
// Hash password before saving
userSchema.pre('save', async function(next) {
// Only hash if password is modified
if (!this.isModified('password')) {
return next();
}
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Remove password from JSON output
userSchema.methods.toJSON = function() {
const user = this.toObject();
delete user.password;
return user;
};
module.exports = mongoose.model('User', userSchema);
JWT (JSON Web Tokens)
JWT is a compact, URL-safe means of representing claims between two parties.
Installation
npm install jsonwebtoken
Environment Variables
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRE=7d
JWT Utility Functions
const jwt = require('jsonwebtoken');
// Generate JWT token
const generateToken = (userId) => {
return jwt.sign(
{ id: userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
// Verify JWT token
const verifyToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return null;
}
};
module.exports = { generateToken, verifyToken };
User Registration
const User = require('../models/User');
const { generateToken } = require('../utils/jwt');
// Register new user
exports.register = async (req, res) => {
try {
const { name, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'Email already registered'
});
}
// Create user
const user = await User.create({
name,
email,
password // Will be hashed by pre-save hook
});
// Generate token
const token = generateToken(user._id);
res.status(201).json({
success: true,
message: 'User registered successfully',
token,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
User Login
// Login user
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Please provide email and password'
});
}
// Find user (include password for comparison)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Check password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid credentials'
});
}
// Generate token
const token = generateToken(user._id);
res.json({
success: true,
message: 'Login successful',
token,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
Authentication Middleware
const { verifyToken } = require('../utils/jwt');
const User = require('../models/User');
// Protect routes - require authentication
exports.protect = async (req, res, next) => {
try {
let token;
// Get token from header
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
// Check if token exists
if (!token) {
return res.status(401).json({
success: false,
message: 'Not authorized to access this route'
});
}
// Verify token
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({
success: false,
message: 'Invalid or expired token'
});
}
// Get user from token
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
success: false,
message: 'User not found'
});
}
// Attach user to request
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Not authorized'
});
}
};
// Authorize specific roles
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: `Role '${req.user.role}' is not authorized to access this route`
});
}
next();
};
};
Protected Routes
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { protect, authorize } = require('../middleware/auth');
// Public routes
router.post('/register', authController.register);
router.post('/login', authController.login);
// Protected routes
router.get('/me', protect, authController.getMe);
router.put('/updateprofile', protect, authController.updateProfile);
router.put('/updatepassword', protect, authController.updatePassword);
// Admin only routes
router.get('/users', protect, authorize('admin'), authController.getAllUsers);
router.delete('/users/:id', protect, authorize('admin'), authController.deleteUser);
module.exports = router;
Using Protected Routes
// Login
const login = async (email, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
// Store token
localStorage.setItem('token', data.token);
}
return data;
};
// Access protected route
const getProfile = async () => {
const token = localStorage.getItem('token');
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
};
Password Reset Flow
const crypto = require('crypto');
// Generate password reset token
userSchema.methods.getResetPasswordToken = function() {
// Generate token
const resetToken = crypto.randomBytes(20).toString('hex');
// Hash and set to resetPasswordToken field
this.resetPasswordToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
// Set expire (10 minutes)
this.resetPasswordExpire = Date.now() + 10 * 60 * 1000;
return resetToken;
};
// Forgot password
exports.forgotPassword = async (req, res) => {
try {
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Get reset token
const resetToken = user.getResetPasswordToken();
await user.save({ validateBeforeSave: false });
// Create reset URL
const resetUrl = `${req.protocol}://${req.get('host')}/api/auth/resetpassword/${resetToken}`;
// Send email (implement email service)
// await sendEmail({ ... });
res.json({
success: true,
message: 'Password reset email sent',
resetToken // Remove in production
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
// Reset password
exports.resetPassword = async (req, res) => {
try {
// Get hashed token
const resetPasswordToken = crypto
.createHash('sha256')
.update(req.params.resettoken)
.digest('hex');
const user = await User.findOne({
resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() }
});
if (!user) {
return res.status(400).json({
success: false,
message: 'Invalid or expired token'
});
}
// Set new password
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpire = undefined;
await user.save();
// Generate new token
const token = generateToken(user._id);
res.json({
success: true,
message: 'Password reset successful',
token
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message
});
}
};
Security Best Practices
1. Rate Limiting
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests per window
message: 'Too many login attempts, please try again later'
});
module.exports = { loginLimiter };
2. Helmet for Security Headers
npm install helmet
const helmet = require('helmet');
app.use(helmet());
3. Input Validation
npm install express-validator
const { body, validationResult } = require('express-validator');
exports.registerValidator = [
body('name')
.trim()
.isLength({ min: 2 })
.withMessage('Name must be at least 2 characters'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Please enter a valid email'),
body('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
next();
}
];
4. CORS Configuration
const cors = require('cors');
const corsOptions = {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true
};
app.use(cors(corsOptions));
5. Environment Variables
- Use strong JWT secrets
- Set appropriate token expiration
- Use HTTPS in production
- Implement rate limiting
- Validate all user input
- Keep dependencies updated
Complete Auth System Example
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const connectDB = require('./config/database');
const app = express();
// Connect to database
connectDB();
// Security middleware
app.use(helmet());
app.use(cors());
// Body parser
app.use(express.json());
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: 'Server error'
});
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Key Takeaways
- Never store passwords in plain text - use bcrypt
- JWT tokens enable stateless authentication
- Middleware protects routes from unauthorized access
- Role-based authorization controls resource access
- Rate limiting prevents brute force attacks
- Input validation prevents injection attacks
- Security headers protect against common vulnerabilities
- HTTPS is essential in production
Practice Exercises
Exercise 1: Email Verification
Implement email verification on registration:
// Add to User model:
// - isVerified field
// - verificationToken field
// Send verification email on registration
// Create verify endpoint
Exercise 2: Refresh Tokens
Implement refresh token mechanism:
// Create two tokens:
// - Access token (short-lived, 15 min)
// - Refresh token (long-lived, 7 days)
// Implement token refresh endpoint
Exercise 3: Two-Factor Authentication
Add 2FA with TOTP:
// Install: npm install speakeasy qrcode
// Generate secret for user
// Create QR code
// Verify TOTP code on login
Exercise 4: Session Management
Track active user sessions:
// Store active sessions in database
// Allow users to view active sessions
// Implement logout from all devices
// Auto-expire old sessions
Additional Resources
What's Next?
In Lesson 10, you'll learn professional RESTful API design patterns. You'll discover how to structure APIs properly, implement versioning, document with Swagger, handle errors consistently, and follow industry best practices.