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.

C
# 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.

C
// 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.

C
// 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.

C
// 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.