JSON Web Tokens (JWT) are a popular method for implementing authentication in REST APIs. In this guide, we'll implement a complete JWT authentication system using Express.js, including user registration, login, password hashing with bcrypt, and protected routes.
Authentication is one of those topics that seems simple until you actually have to implement it securely. I've seen too many applications with authentication vulnerabilities—from storing plain text passwords to improperly validating tokens. When I first built a JWT authentication system, I made plenty of mistakes, but those mistakes taught me what actually matters in production.
JSON Web Tokens have become the standard for stateless authentication in REST APIs, and for good reason. They're compact, can be verified without database lookups, and work perfectly with microservices architectures. But implementing them correctly requires understanding not just how to generate tokens, but also how to secure them, handle expiration, and protect your routes properly.
What is JWT Authentication?
JWT (JSON Web Token) is a compact, URL-safe token format used for securely transmitting information between parties. In authentication:
- Stateless - No need to store sessions on the server
- Scalable - Works across microservices
- Self-contained - Token includes user information
- Secure - Signed with a secret key
- Expirable - Tokens can have expiration times
Installation
Before we start coding, let's get our dependencies installed:
npm install jsonwebtoken bcrypt express
npm install --save-dev @types/jsonwebtoken @types/bcrypt
User Registration with Password Hashing
Here's how to implement user registration with bcrypt password hashing:
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { User } = require("../models/index");
class AuthController {
async register(req, res) {
try {
const { name, email, password, role = "staff" } = req.body;
// Validate input
if (!name || !email || !password) {
return res.status(400).json({
success: false,
message: "Name, email, and password are required",
});
}
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(409).json({
success: false,
message: "User with this email already exists",
});
}
// Hash password with bcrypt (10 rounds)
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await User.create({
name,
email,
password: hashedPassword,
role,
status: "active",
});
// Generate JWT token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET || "your-secret-key-change-this",
{ expiresIn: "7d" }
);
return res.status(201).json({
success: true,
message: "User registered successfully",
data: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
token,
},
});
} catch (error) {
console.error("Error registering user:", error);
return res.status(500).json({
success: false,
message: "Error registering user",
error: error.message,
});
}
}
}
User Login
Login implementation with password verification:
async login(req, res) {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
success: false,
message: "Email and password are required",
});
}
// Find user
const user = await User.findOne({
where: {
email: email.toLowerCase().trim()
}
});
// Always return the same error message to prevent user enumeration
if (!user) {
return res.status(401).json({
success: false,
message: "Invalid email or password",
});
}
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: "Invalid email or password",
});
}
// Generate JWT token
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "7d" }
);
return res.json({
success: true,
message: "Login successful",
data: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
token,
},
});
} catch (error) {
console.error("Error logging in:", error);
return res.status(500).json({
success: false,
message: "Error logging in",
error: error.message,
});
}
}
Protected Route Middleware
Create middleware to protect routes:
const jwt = require("jsonwebtoken");
const authenticateToken = (req, res, next) => {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: "Access token required",
});
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({
success: false,
message: "Invalid or expired token",
});
}
req.user = user;
next();
});
};
module.exports = { authenticateToken };
Best Practices
- Always hash passwords with bcrypt
- Use strong JWT secrets
- Set appropriate token expiration
- Validate tokens on protected routes
- Handle errors consistently
- Prevent user enumeration
📖 Read the Complete Guide
This is just a brief overview! The complete guide on my blog includes:
- ✅ Token Refresh - Implementing refresh tokens
- ✅ Role-Based Access - Protecting routes by role
- ✅ Password Reset - Secure password reset flow
- ✅ Security Best Practices - Production security patterns
- ✅ Error Handling - Comprehensive error management
- ✅ Real-world examples from production applications
👉 Read the full article with all code examples here
What's your experience with JWT authentication? Share your tips in the comments! 🚀
For more backend guides, check out my blog covering Express.js, Prisma ORM, Better Auth, and more.
Top comments (0)