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

Intermediate 45 min Apr 14, 2026 29 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 sudo ufw default allow outgoing sudo ufw allow ssh sudo ufw allow 3000/tcp comment 'Node.js app'

Block Redis port from external access

sudo ufw deny 6379/tcp

Enable firewall

sudo ufw --force enable sudo ufw status verbose
# Configure firewalld rules
sudo firewall-cmd --permanent --add-port=3000/tcp
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Configure log rotation

Set up log rotation for application logs to prevent disk space issues.

/var/log/nodejs-auth-app/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 0644 nodejs nodejs
    postrotate
        systemctl reload nodejs-auth-app
    endscript
}

Common issues

Symptom Cause Fix
Redis connection failed Authentication or binding issues Check Redis password in /etc/redis/redis.conf and .env file
JWT verification fails Clock skew or wrong secret Sync system time with sudo ntpdate -s time.nist.gov
Session not persisting Redis store configuration issue Verify Redis connection and session config in application
Rate limiting not working Missing client IP identification Configure proper reverse proxy headers or trust proxy setting
CORS errors in browser Origin not allowed Add frontend domain to ALLOWED_ORIGINS in .env file
Service won't start Port already in use Check with sudo netstat -tlnp | grep :3000 and kill conflicting process

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle infrastructure security hardening for businesses that depend on uptime. From initial setup to ongoing operations.