Express.js JWT Authentication

JSON Web Token (JWT) is the industry standard for securely transmitting information between a client and a server. In the world of modern web development, particularly with Express.js, JWTs have replaced traditional session-based authentication for most APIs. Think of a JWT as a digital "ID card" that the server issues to a user; for every subsequent request, the user presents this card to prove who they are without the server needing to look up their session in a database.

Developer Tip: JWTs are excellent for Single Page Applications (SPAs) like React or Vue, and mobile apps, because they allow the backend to remain completely stateless.

 

Key Features of JWT Authentication

  • Token-Based Authentication: Instead of sending cookies back and forth, the client sends a cryptographically signed token in the HTTP headers.
  • Stateless and Scalable: The server doesn't need to store session data in memory or a database. This makes it incredibly easy to scale your application across multiple servers.
  • Compact and Secure: JWTs are URL-safe and can be passed through headers or even query parameters. Because they are digitally signed, the server can trust the data inside them hasn't been tampered with.
  • Granular Control: You can embed specific user permissions (roles) and expiration times directly into the token payload.
Watch Out: By default, JWTs are encoded, not encrypted. This means anyone who intercepts the token can read the data inside. Never put sensitive information like passwords or credit card numbers in a JWT payload.

 

Steps to Implement JWT Authentication

Install Required Packages
To get started, we need two core libraries: jsonwebtoken to handle the creation and verification of tokens, and bcryptjs to handle secure password hashing.

npm install jsonwebtoken bcryptjs
Best Practice: Use bcryptjs instead of the native bcrypt library if you want to avoid issues with C++ build dependencies during deployment, especially on environments like Docker or Heroku.

Create a Token
Once a user provides a valid username and password, you generate a token. This token is then sent back to the client, which usually stores it in localStorage or an HttpOnly cookie.

Example:

const jwt = require('jsonwebtoken');

// The payload contains the user data you want to store
const payload = { id: user.id, role: 'admin' };
const secret = process.env.JWT_SECRET; // Always use an environment variable
const token = jwt.sign(payload, secret, { expiresIn: '2h' });

res.json({ token });
Common Mistake: Setting an infinite or extremely long expiration time. If a token is stolen, the attacker has access forever. Always set a reasonable expiresIn value (e.g., '1h' or '24h').

Verify a Token
To protect a route, you create a middleware function. This function intercepts the request, looks for the token in the Authorization header, and validates it.

Example:

const authenticateToken = (req, res, next) => {
    // Expected format: "Bearer <token>"
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) return res.status(401).send('Access Denied: No Token Provided');

    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.status(403).send('Invalid or Expired Token');
        
        // Attach the user data to the request object
        req.user = user;
        next();
    });
};

// Apply the middleware to a route
app.get('/api/dashboard', authenticateToken, (req, res) => {
    res.send(`Welcome User ID: ${req.user.id}`);
});

Hash Passwords with Bcrypt
Never store plain-text passwords in your database. Use bcryptjs to turn a password into a "hash" that cannot be reversed.

Example:

const bcrypt = require('bcryptjs');

// Higher salt rounds = more secure, but slower processing
const saltRounds = 10;
const hashedPassword = bcrypt.hashSync(userPassword, saltRounds);

Compare Passwords
During login, you compare the plain-text password provided by the user with the hashed version stored in your database.

Example:

const isMatch = bcrypt.compareSync(submittedPassword, storedHashedPassword);
if (!isMatch) return res.status(400).send('Invalid Credentials');

 

Complete Example of JWT Authentication

Here is a consolidated example showing how these pieces fit together in a real Express application.

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
app.use(express.json());

// In a real app, this would be a database like MongoDB or PostgreSQL
const users = []; 
const JWT_SECRET = 'super_secret_key_123'; 

// 1. Register: Hash password and save user
app.post('/register', (req, res) => {
    const { username, password } = req.body;
    const hashedPassword = bcrypt.hashSync(password, 10);
    
    users.push({ id: users.length + 1, username, password: hashedPassword });
    res.status(201).send('User registered successfully');
});

// 2. Login: Verify password and issue JWT
app.post('/login', (req, res) => {
    const { username, password } = req.body;
    const user = users.find(u => u.username === username);

    if (!user || !bcrypt.compareSync(password, user.password)) {
        return res.status(401).send('Invalid username or password');
    }

    const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });
    res.json({ token });
});

// 3. Protected Route: Only accessible with a valid token
app.get('/profile', (req, res) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) return res.status(401).send('Not authorized');

    jwt.verify(token, JWT_SECRET, (err, decoded) => {
        if (err) return res.status(403).send('Token is no longer valid');
        res.json({ message: `Welcome ${decoded.username}`, userId: decoded.id });
    });
});

app.listen(3000, () => {
    console.log('Auth Server running on http://localhost:3000');
});

 

Summary

JWT Authentication is a powerful tool for building secure Express.js applications. By shifting the responsibility of state from the server to the client, you create an architecture that is lightweight and highly scalable. Remember to always use environment variables for your secrets, hash passwords before they touch your database, and keep your token expiration times short to minimize security risks.

Next Steps: Once you've mastered basic JWTs, look into Refresh Tokens. They allow users to stay logged in securely for longer periods without requiring them to re-enter their password every hour.