AboutBlogContact
Backend EngineeringJune 18, 2015 5 min read 129Updated: June 22, 2026

How to Implement JWT Authentication in Node.js + Express (2015)

AunimedaAunimeda
📋 Table of Contents

Short answer: Use jsonwebtoken to sign tokens with a secret, verify them in Express middleware, store userId in payload. Sign access tokens with short expiry (15 min), issue refresh tokens with longer expiry (30 days) stored server-side.

In 2015, mobile APIs needed stateless auth - no sessions, no cookies. JWT filled that gap. Here's the production implementation we shipped, including the refresh token pattern we got wrong the first time.


Install

npm install express jsonwebtoken bcryptjs
# Node.js 4.x LTS (released October 2015) - first LTS with ES6 support

Token Generation

// auth/tokens.js
var jwt = require('jsonwebtoken');

var ACCESS_SECRET  = process.env.JWT_SECRET;       // 32+ random bytes
var REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

// Access token: short-lived, stateless
function generateAccessToken(userId, role) {
  return jwt.sign(
    { userId: userId, role: role },
    ACCESS_SECRET,
    { expiresIn: '15m' }
  );
}

// Refresh token: longer-lived, stored in DB
function generateRefreshToken(userId) {
  return jwt.sign(
    { userId: userId, type: 'refresh' },
    REFRESH_SECRET,
    { expiresIn: '30d' }
  );
}

module.exports = { generateAccessToken, generateRefreshToken };

Login Endpoint

// routes/auth.js
var express  = require('express');
var bcrypt   = require('bcryptjs');
var db       = require('../db');
var tokens   = require('../auth/tokens');
var router   = express.Router();

router.post('/login', function(req, res) {
  var email    = req.body.email;
  var password = req.body.password;

  db.query('SELECT * FROM users WHERE email = ?', [email], function(err, rows) {
    if (err || rows.length === 0) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    var user = rows[0];

    bcrypt.compare(password, user.password_hash, function(err, valid) {
      if (!valid) {
        return res.status(401).json({ error: 'Invalid credentials' });
      }

      var accessToken  = tokens.generateAccessToken(user.id, user.role);
      var refreshToken = tokens.generateRefreshToken(user.id);

      // Store refresh token in DB (not just in client)
      // Critical: allows invalidation on logout/password change
      db.query(
        'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))',
        [user.id, refreshToken],
        function() {
          res.json({
            accessToken:  accessToken,
            refreshToken: refreshToken,
            expiresIn:    900  // 15 minutes in seconds
          });
        }
      );
    });
  });
});

module.exports = router;

Auth Middleware

// middleware/authenticate.js
var jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  var header = req.headers['authorization'];

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

  // Standard: "Authorization: Bearer <token>"
  var parts = header.split(' ');
  if (parts.length !== 2 || parts[0] !== 'Bearer') {
    return res.status(401).json({ error: 'Token format invalid' });
  }

  var token = parts[1];

  jwt.verify(token, process.env.JWT_SECRET, function(err, decoded) {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
      }
      return res.status(401).json({ error: 'Token invalid' });
    }

    req.userId = decoded.userId;
    req.userRole = decoded.role;
    next();
  });
}

// Role-based access: wrap authenticate
function requireRole(role) {
  return [authenticate, function(req, res, next) {
    if (req.userRole !== role && req.userRole !== 'admin') {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  }];
}

module.exports = { authenticate, requireRole };

Refresh Token Endpoint

// routes/auth.js (continued)
router.post('/refresh', function(req, res) {
  var refreshToken = req.body.refreshToken;

  if (!refreshToken) {
    return res.status(400).json({ error: 'Refresh token required' });
  }

  // Verify JWT signature first
  jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, function(err, decoded) {
    if (err) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }

    // Then verify it exists in DB (not revoked)
    db.query(
      'SELECT * FROM refresh_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW()',
      [refreshToken, decoded.userId],
      function(err, rows) {
        if (err || rows.length === 0) {
          return res.status(401).json({ error: 'Refresh token revoked' });
        }

        // Rotate: delete old token, issue new pair
        db.query('DELETE FROM refresh_tokens WHERE token = ?', [refreshToken]);

        var newAccess  = tokens.generateAccessToken(decoded.userId, rows[0].role);
        var newRefresh = tokens.generateRefreshToken(decoded.userId);

        db.query(
          'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))',
          [decoded.userId, newRefresh]
        );

        res.json({ accessToken: newAccess, refreshToken: newRefresh });
      }
    );
  });
});

Logout (Token Revocation)

router.post('/logout', authenticate, function(req, res) {
  var refreshToken = req.body.refreshToken;

  // Delete specific refresh token
  db.query(
    'DELETE FROM refresh_tokens WHERE user_id = ? AND token = ?',
    [req.userId, refreshToken],
    function() {
      res.json({ message: 'Logged out' });
    }
  );
});

// Logout everywhere (e.g., after password change)
router.post('/logout-all', authenticate, function(req, res) {
  db.query('DELETE FROM refresh_tokens WHERE user_id = ?', [req.userId], function() {
    res.json({ message: 'All sessions terminated' });
  });
});

refresh_tokens table

CREATE TABLE refresh_tokens (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  user_id    INT NOT NULL,
  token      TEXT NOT NULL,    -- Full JWT string
  expires_at DATETIME NOT NULL,
  created_at DATETIME DEFAULT NOW(),
  INDEX idx_user (user_id)
  -- No unique index on token: TEXT column, use application-level check
) ENGINE=InnoDB;

The Mistake We Made First

We stored JWTs in localStorage. In 2015, this was common advice. The problem: XSS vulnerability reads localStorage from injected JavaScript.

Fix: Access token in memory (JS variable), refresh token in httpOnly cookie. The access token lives only for 15 minutes - an XSS attack has a narrow window. The refresh token in httpOnly cookie is inaccessible to JS entirely.

// Sending refresh token as httpOnly cookie instead of body
res.cookie('refreshToken', newRefresh, {
  httpOnly: true,
  secure:   true,    // HTTPS only
  sameSite: 'Strict',
  maxAge:   30 * 24 * 60 * 60 * 1000  // 30 days in ms
});
res.json({ accessToken: newAccess });

This pattern held up. In 2025, it's still the recommended approach for web apps.


Usage on Protected Routes

var auth = require('./middleware/authenticate');

// Public route
app.get('/api/products', getProducts);

// Requires any authenticated user
app.get('/api/orders', auth.authenticate, getOrders);

// Requires admin role
app.delete('/api/users/:id', auth.requireRole('admin'), deleteUser);

Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.

Contact us for backend engineering services. See also: Custom Software Development, Web Development

Read Also

Node.js + TypeScript: Building a Production REST API from Scratch in 2026aunimeda
Backend Engineering

Node.js + TypeScript: Building a Production REST API from Scratch in 2026

A complete guide to building a production-ready REST API with Node.js and TypeScript - authentication, validation, error handling, rate limiting, logging, and deployment. No shortcuts.

Clean Architecture in Node.js: A Practical Guide Without the Academic Fluffaunimeda
Backend Engineering

Clean Architecture in Node.js: A Practical Guide Without the Academic Fluff

Clean Architecture sounds great in theory. In practice, most implementations add complexity without benefit. This guide shows the pattern that actually works in Node.js - dependency inversion, use cases, and repository pattern with real, runnable code.

How to Implement Real-Time Features with WebSockets and Socket.io 1.x (2016)aunimeda
Backend Engineering

How to Implement Real-Time Features with WebSockets and Socket.io 1.x (2016)

Socket.io 1.x (2016) made WebSockets practical: automatic fallback to polling, rooms for namespacing, and a simple emit/on API. We used it to build a real-time order tracking dashboard. Here's the server setup, client integration, authentication via JWT, and scaling with Redis adapter.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles