JWT Authentication in Node.js and Express
In modern backend development, securing REST APIs is a fundamental requirement. Because HTTP is a stateless protocol, servers do not remember client sessions by default. Traditionally, applications relied on server-side session cookies. However, in modern distributed architectures, microservices, and single-page applications (SPAs), **JSON Web Tokens (JWT)** have become the industry standard for stateless authentication.
This comprehensive tutorial walks through building a secure authentication system from scratch using **Node.js**, **Express**, **bcrypt** for password hashing, and **jsonwebtoken** for issuing and verifying JWTs.
1. Project Setup and Dependencies
Initialize a new Node.js project, install the required packages (`express`, `jsonwebtoken`, `bcryptjs`, `dotenv`), and set up your initial application structure.
# Terminal setup commands
mkdir express-jwt-auth
cd express-jwt-auth
npm init -y
npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon
2. Server Initialization and In-Memory User Store
Create an `server.js` file. For simplicity in this tutorial, we will use an in-memory array to simulate user database records, though the same principles apply to MongoDB or PostgreSQL.
// server.js
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const app = express();
app.use(express.json());
// In-memory user database mock
const users = [];
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET || 'super_secret_jwt_key_123';
// Health check endpoint
app.get('/', (req, res) => {
res.json({ message: "JWT Auth Server Running Successfully" });
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
3. Implementing User Registration and Login Routes
When registering a user, hash their password securely using `bcrypt`. When logging in, verify the submitted password against the stored hash and issue a signed JWT valid for a limited expiration window.
// User Registration Endpoint
app.post('/api/register', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: "Username and password are required" });
}
// Check if user already exists
const existingUser = users.find(u => u.username === username);
if (existingUser) {
return res.status(400).json({ error: "Username already taken" });
}
// Hash password securely with salt rounds = 10
const hashedPassword = await bcrypt.hash(password, 10);
users.push({ username, password: hashedPassword });
res.status(201).json({ message: "User registered successfully!" });
} catch (err) {
res.status(500).json({ error: "Internal server error during registration" });
}
});
// User Login Endpoint
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: "Invalid username or password" });
}
// Create JWT payload and sign token (expires in 1 hour)
const payload = { username: user.username };
const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, { expiresIn: '1h' });
res.json({ message: "Login successful", accessToken });
} catch (err) {
res.status(500).json({ error: "Internal server error during login" });
}
});
4. Securing Routes with Authentication Middleware
To protect sensitive routes, create an Express middleware function that inspects the `Authorization` header, extracts the Bearer token, and verifies its cryptographic signature.
// JWT Verification Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <TOKEN>
if (!token) {
return res.status(401).json({ error: "Access token missing or unauthorized" });
}
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: "Token is invalid or expired" });
}
req.user = user;
next();
});
}
// Protected Route Example
app.get('/api/dashboard', authenticateToken, (req, res) => {
res.json({
message: "Welcome to your protected developer dashboard!",
user: req.user
});
});
Conclusion
Implementing JWT authentication in Node.js and Express provides a robust, stateless security architecture for modern APIs. By combining secure password hashing with `bcrypt` and token verification middleware, you can protect sensitive endpoints and manage user sessions efficiently.