TrueSpec

JWT Authentication in Express.js

Access/refresh token pattern with middleware guards and secure cookie storage

What You’ll Build

After following this guide, you will have a working implementation of jwt authentication in express.js in your project. Build a robust JWT authentication system for Express.js APIs. Uses short-lived access tokens (15 min) and long-lived refresh tokens (7 days) stored in HTTP-only cookies. Includes middleware for protecting routes and automatic token refresh.

Use Cases & Problems Solved

  • Protect routes so only authenticated users can access sensitive pages
  • Allow users to sign up, log in, and manage their accounts securely
  • Avoid storing raw passwords or building session management from scratch

Prerequisites

  • Node.js 18+
  • Express.js project
  • A user database (any)

Step-by-Step Implementation

Install dependencies

The following snippet shows how to install dependencies. Copy this into your project and adjust the values for your environment.

npm install jsonwebtoken bcryptjs cookie-parser

Token generation utilities

The following snippet shows how to token generation utilities. Copy this into your project and adjust the values for your environment.

// utils/tokens.js
const jwt = require('jsonwebtoken');

function generateAccessToken(user) {
  return jwt.sign(
    { id: user.id, email: user.email },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
}

function generateRefreshToken(user) {
  return jwt.sign(
    { id: user.id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
}

module.exports = { generateAccessToken, generateRefreshToken };

Auth middleware

The following snippet shows how to auth middleware. Copy this into your project and adjust the values for your environment.

// middleware/auth.js
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const token = req.cookies.accessToken ||
    req.headers.authorization?.split(' ')[1];

  if (!token) return res.status(401).json({ error: 'No token provided' });

  try {
    req.user = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid or expired token' });
  }
}

module.exports = { authenticate };

Login and refresh endpoints

The following snippet shows how to login and refresh endpoints. Copy this into your project and adjust the values for your environment.

// routes/auth.js
const bcrypt = require('bcryptjs');
const { generateAccessToken, generateRefreshToken } = require('../utils/tokens');

router.post('/login', async (req, res) => {
  const user = await User.findByEmail(req.body.email);
  if (!user || !await bcrypt.compare(req.body.password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);

  res.cookie('refreshToken', refreshToken, {
    httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000
  });
  res.json({ accessToken });
});

⚠️ Don’t Do This

❌ Storing JWTs in localStorage (XSS vulnerable)

// XSS attack can steal this!
localStorage.setItem('token', response.accessToken);

// Anyone with XSS can read it
fetch('/api/data', {
  headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});

✅ Store tokens in HTTP-only cookies (immune to XSS)

// Server sets HTTP-only cookie — JS cannot access it
res.cookie('accessToken', token, {
  httpOnly: true,  // Cannot be read by JavaScript
  secure: true,    // HTTPS only
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000,
});

Testing

Add these tests to verify your jwt authentication in express.js implementation works correctly:

// __tests__/express.test.ts
import { describe, it, expect } from 'vitest';

describe('JWT Authentication in Express.js', () => {
  it('should handle successful authentication', async () => {
    // Test the happy path
    const result = await authenticate({ email: 'test@example.com', password: 'valid' });
    expect(result.user).toBeDefined();
    expect(result.error).toBeNull();
  });

  it('should reject invalid credentials', async () => {
    const result = await authenticate({ email: 'test@example.com', password: 'wrong' });
    expect(result.user).toBeNull();
    expect(result.error).toBeDefined();
  });

  it('should handle missing fields', async () => {
    const result = await authenticate({ email: '', password: '' });
    expect(result.error).toBeDefined();
  });
});

Verification

# Test login
curl -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"test@test.com","password":"pass123"}' -c cookies.txt

# Test protected route
curl http://localhost:3000/api/profile -b cookies.txt

Related Specs

Intermediate

Secure Session Management with Redis

Express sessions with Redis store, expiration, and fingerprinting

Auth & Identity
Advanced

Role-Based Access Control (RBAC)

Role/permission system with middleware, database schema, and route guards

Auth & Identity
Advanced

Add 2FA/TOTP to Any App

TOTP with otplib, QR code generation, and backup codes

Auth & Identity