Configure Node.js JWT authentication with Redis session storage and security hardening

Intermediate 45 min Apr 14, 2026 698 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Set up secure JWT authentication for Node.js applications with Redis session storage, security middleware, and production-ready hardening practices.

Prerequisites

  • Root or sudo access
  • Node.js 18+ installed
  • Redis server access
  • Basic JavaScript knowledge
  • Understanding of HTTP and REST APIs

What this solves

This tutorial helps you implement JWT (JSON Web Token) authentication in Node.js applications with Redis for session storage. You'll learn to create secure authentication middleware, configure Redis for session management, and apply security hardening techniques to protect against common attack vectors like token hijacking, session fixation, and brute force attacks.

Step-by-step installation

Update system packages and install Node.js

Start by updating your package manager and installing Node.js with npm package manager.

sudo apt update && sudo apt upgrade -y
sudo apt install -y nodejs npm curl
sudo dnf update -y
sudo dnf install -y nodejs npm curl

Install and configure Redis

Install Redis server for session storage and configure it with security settings.

sudo apt install -y redis-server
sudo dnf install -y redis

Configure Redis security settings

Modify Redis configuration to enable authentication and bind to localhost only for security.

# Bind to localhost only
bind 127.0.0.1 ::1

# Set authentication password
requirepass your_secure_redis_password_here_2024

# Disable dangerous commands
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command DEBUG ""

# Set memory limit and policy
maxmemory 256mb
maxmemory-policy allkeys-lru

Start Redis service

Enable and start Redis service to run on system boot.

sudo systemctl enable --now redis-server
sudo systemctl status redis-server

Create Node.js project structure

Set up a new Node.js project directory with proper permissions and initialize package.json.

mkdir -p /opt/nodejs-auth-app
cd /opt/nodejs-auth-app
sudo chown $USER:$USER /opt/nodejs-auth-app
npm init -y

Install required Node.js packages

Install Express.js, JWT libraries, Redis client, and security middleware packages.

npm install express jsonwebtoken bcryptjs redis express-rate-limit helmet cors express-validator morgan dotenv connect-redis express-session

Create environment configuration

Set up environment variables for JWT secrets, Redis connection, and security settings.

# JWT Configuration
JWT_SECRET=your_jwt_secret_key_minimum_32_characters_2024
JWT_EXPIRES_IN=15m
JWT_REFRESH_SECRET=your_jwt_refresh_secret_minimum_32_characters_2024
JWT_REFRESH_EXPIRES_IN=7d

# Redis Configuration
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=your_secure_redis_password_here_2024

# Session Configuration
SESSION_SECRET=your_session_secret_minimum_32_characters_2024
SESSION_MAX_AGE=900000

# Application Configuration
PORT=3000
NODE_ENV=production

Create Redis connection module

Set up Redis client with authentication and connection pooling for session storage.

const redis = require('redis');
const { promisify } = require('util');

const client = redis.createClient({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD,
  retry_strategy: (times) => {
    const delay = Math.min(times * 50, 2000);
    return delay;
  }
});

client.on('error', (err) => {
  console.error('Redis Client Error:', err);
});

client.on('connect', () => {
  console.log('Connected to Redis');
});

// Promisify Redis methods
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);
const delAsync = promisify(client.del).bind(client);
const existsAsync = promisify(client.exists).bind(client);

module.exports = {
  client,
  getAsync,
  setAsync,
  delAsync,
  existsAsync
};

Create JWT utility module

Implement JWT token generation, verification, and refresh token functionality with security best practices.

const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { setAsync, getAsync, delAsync } = require('../config/redis');

class JWTManager {
  static generateTokens(payload) {
    const accessToken = jwt.sign(
      payload,
      process.env.JWT_SECRET,
      { 
        expiresIn: process.env.JWT_EXPIRES_IN,
        issuer: 'nodejs-auth-app',
        audience: 'api-users'
      }
    );

    const refreshToken = jwt.sign(
      { userId: payload.userId },
      process.env.JWT_REFRESH_SECRET,
      { 
        expiresIn: process.env.JWT_REFRESH_EXPIRES_IN,
        issuer: 'nodejs-auth-app'
      }
    );

    return { accessToken, refreshToken };
  }

  static verifyAccessToken(token) {
    return jwt.verify(token, process.env.JWT_SECRET, {
      issuer: 'nodejs-auth-app',
      audience: 'api-users'
    });
  }

  static verifyRefreshToken(token) {
    return jwt.verify(token, process.env.JWT_REFRESH_SECRET, {
      issuer: 'nodejs-auth-app'
    });
  }

  static async storeRefreshToken(userId, refreshToken) {
    const key = `refresh_token:${userId}`;
    const hashedToken = crypto.createHash('sha256').update(refreshToken).digest('hex');
    await setAsync(key, hashedToken, 'EX', 7 * 24 * 60 * 60); // 7 days
  }

  static async verifyStoredRefreshToken(userId, refreshToken) {
    const key = `refresh_token:${userId}`;
    const storedHash = await getAsync(key);
    const providedHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
    return storedHash === providedHash;
  }

  static async revokeRefreshToken(userId) {
    const key = `refresh_token:${userId}`;
    await delAsync(key);
  }
}

module.exports = JWTManager;

Create authentication middleware

Implement JWT authentication middleware with rate limiting and security checks.

const JWTManager = require('../utils/jwt');
const rateLimit = require('express-rate-limit');
const { getAsync, setAsync } = require('../config/redis');

// Rate limiting for authentication attempts
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // limit each IP to 5 requests per windowMs
  message: 'Too many authentication attempts, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});

// JWT verification middleware
const authenticateToken = async (req, res, next) => {
  try {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

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

    // Check if token is blacklisted
    const isBlacklisted = await getAsync(`blacklist:${token}`);
    if (isBlacklisted) {
      return res.status(401).json({ error: 'Token has been revoked' });
    }

    const decoded = JWTManager.verifyAccessToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    if (error.name === 'JsonWebTokenError') {
      return res.status(401).json({ error: 'Invalid token' });
    }
    return res.status(500).json({ error: 'Authentication error' });
  }
};

// Blacklist token on logout
const blacklistToken = async (token, expiresIn) => {
  const key = `blacklist:${token}`;
  await setAsync(key, 'true', 'EX', expiresIn);
};

module.exports = {
  authenticateToken,
  authLimiter,
  blacklistToken
};

Create session configuration

Configure Express sessions with Redis store and security settings for session management.

const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const { client } = require('./redis');

const sessionConfig = {
  store: new RedisStore({ client: client }),
  secret: process.env.SESSION_SECRET,
  name: 'sessionId',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only in production
    httpOnly: true, // Prevent XSS attacks
    maxAge: parseInt(process.env.SESSION_MAX_AGE) || 900000, // 15 minutes
    sameSite: 'strict' // CSRF protection
  },
  rolling: true // Reset expiration on activity
};

module.exports = sessionConfig;

Create main application file

Set up Express server with security middleware, authentication routes, and proper error handling.

require('dotenv').config();
const express = require('express');
const bcrypt = require('bcryptjs');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const session = require('express-session');
const { body, validationResult } = require('express-validator');

const JWTManager = require('./utils/jwt');
const { authenticateToken, authLimiter, blacklistToken } = require('./middleware/auth');
const sessionConfig = require('./config/session');
require('./config/redis'); // Initialize Redis connection

const app = express();

// Security middleware
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3001'],
  credentials: true,
  optionsSuccessStatus: 200
}));

app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(session(sessionConfig));

// Mock user data (replace with database in production)
const users = new Map();

// Register endpoint
app.post('/api/register', [
  body('username').isLength({ min: 3, max: 20 }).isAlphanumeric(),
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
], authLimiter, async (req, res) => {
  try {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { username, email, password } = req.body;
    
    if (users.has(email)) {
      return res.status(409).json({ error: 'User already exists' });
    }

    const hashedPassword = await bcrypt.hash(password, 12);
    const userId = Date.now().toString();
    
    users.set(email, {
      userId,
      username,
      email,
      password: hashedPassword,
      createdAt: new Date()
    });

    res.status(201).json({ message: 'User registered successfully', userId });
  } catch (error) {
    res.status(500).json({ error: 'Registration failed' });
  }
});

// Login endpoint
app.post('/api/login', [
  body('email').isEmail().normalizeEmail(),
  body('password').notEmpty()
], authLimiter, async (req, res) => {
  try {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { email, password } = req.body;
    const user = users.get(email);
    
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const payload = {
      userId: user.userId,
      email: user.email,
      username: user.username
    };

    const { accessToken, refreshToken } = JWTManager.generateTokens(payload);
    await JWTManager.storeRefreshToken(user.userId, refreshToken);

    // Store session data
    req.session.userId = user.userId;
    req.session.loginTime = new Date();

    res.json({
      message: 'Login successful',
      accessToken,
      refreshToken,
      user: {
        userId: user.userId,
        username: user.username,
        email: user.email
      }
    });
  } catch (error) {
    res.status(500).json({ error: 'Login failed' });
  }
});

// Token refresh endpoint
app.post('/api/refresh', async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    if (!refreshToken) {
      return res.status(401).json({ error: 'Refresh token required' });
    }

    const decoded = JWTManager.verifyRefreshToken(refreshToken);
    const isValid = await JWTManager.verifyStoredRefreshToken(decoded.userId, refreshToken);
    
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }

    // Find user (replace with database query)
    const user = Array.from(users.values()).find(u => u.userId === decoded.userId);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    const payload = {
      userId: user.userId,
      email: user.email,
      username: user.username
    };

    const { accessToken, refreshToken: newRefreshToken } = JWTManager.generateTokens(payload);
    
    // Revoke old refresh token and store new one
    await JWTManager.revokeRefreshToken(decoded.userId);
    await JWTManager.storeRefreshToken(decoded.userId, newRefreshToken);

    res.json({ accessToken, refreshToken: newRefreshToken });
  } catch (error) {
    res.status(401).json({ error: 'Token refresh failed' });
  }
});

// Protected route example
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({
    message: 'Protected resource accessed',
    user: req.user,
    sessionId: req.session.id
  });
});

// Logout endpoint
app.post('/api/logout', authenticateToken, async (req, res) => {
  try {
    const token = req.headers['authorization'].split(' ')[1];
    const decoded = JWTManager.verifyAccessToken(token);
    
    // Blacklist current token
    await blacklistToken(token, 900); // 15 minutes
    
    // Revoke refresh token
    await JWTManager.revokeRefreshToken(decoded.userId);
    
    // Destroy session
    req.session.destroy((err) => {
      if (err) {
        console.error('Session destroy error:', err);
      }
    });

    res.json({ message: 'Logout successful' });
  } catch (error) {
    res.status(500).json({ error: 'Logout failed' });
  }
});

// Error handling middleware
app.use((error, req, res, next) => {
  console.error('Application error:', error);
  res.status(500).json({ error: 'Internal server error' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, '127.0.0.1', () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = app;

Create systemd service file

Set up systemd service to run the Node.js application with proper user permissions and security settings.

[Unit]
Description=Node.js JWT Authentication App
After=network.target redis.service
Requires=redis.service

[Service]
Type=simple
User=nodejs
Group=nodejs
WorkingDirectory=/opt/nodejs-auth-app
EnvironmentFile=/opt/nodejs-auth-app/.env
ExecStart=/usr/bin/node src/app.js
Restart=always
RestartSec=10

# Security settings
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/nodejs-auth-app

# Resource limits
LimitNOFILE=65536
LimitNPROC=4096

[Install]
WantedBy=multi-user.target

Create application user and set permissions

Create a dedicated user for running the Node.js application with minimal privileges.

sudo useradd --system --shell /bin/false --home-dir /opt/nodejs-auth-app nodejs
sudo chown -R nodejs:nodejs /opt/nodejs-auth-app
sudo chmod 755 /opt/nodejs-auth-app
sudo chmod 600 /opt/nodejs-auth-app/.env
Never use chmod 777. It gives every user on the system full access to your files. Instead, use specific ownership with chown and minimal permissions like 755 for directories and 644/600 for files.

Start and enable the application service

Enable the systemd service to start automatically and verify it's running correctly.

sudo systemctl daemon-reload
sudo systemctl enable --now nodejs-auth-app
sudo systemctl status nodejs-auth-app

Verify your setup

Test the authentication endpoints and verify Redis session storage is working correctly.

# Check service status
sudo systemctl status nodejs-auth-app
sudo systemctl status redis-server

# Test registration
curl -X POST http://localhost:3000/api/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","password":"SecurePass123!"}'

# Test login
curl -X POST http://localhost:3000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"SecurePass123!"}'

# Check Redis sessions
redis-cli -a your_secure_redis_password_here_2024 KEYS "*"

# Check application logs
sudo journalctl -u nodejs-auth-app -f

Security hardening configuration

Configure firewall rules

Set up firewall rules to restrict access to Redis and application ports.

# Install ufw if not already installed
sudo apt install -y ufw

# Configure firewall rules
sudo ufw default deny incoming

Automated install script

Run this to automate the entire setup

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

At their desk in Rotterdam 11:35 · reachable in a message, no ticket form