Bablu Kumar Singh
Back to Blog
Backend Development
7 min read
May 28, 2026

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security

Authentication is the front door of every application. Get it wrong and nothing else matters — your data, your users, your reputation are all at risk. In this article, written from the trenches of building auth systems for production SaaS platforms, Bablu Kumar Singh walks through a robust JWT-based authentication architecture for Node.js applications.

Password Hashing with bcrypt

Never store passwords in plaintext. Use bcrypt, which adds a per-user salt and is intentionally slow to resist brute-force attacks:

typescript Code Block
import bcrypt from 'bcryptjs';

const SALT_ROUNDS = 12;

async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}

async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

Use at least 12 salt rounds in production. Each additional round doubles the computation time, making offline attacks exponentially harder.

JWT Access Tokens

After verifying credentials, issue a short-lived JSON Web Token:

typescript Code Block
import jwt from 'jsonwebtoken';

const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m';

function generateAccessToken(user: { id: string; email: string; role: string }) {
  return jwt.sign(
    { sub: user.id, email: user.email, role: user.role },
    ACCESS_TOKEN_SECRET,
    { expiresIn: ACCESS_TOKEN_EXPIRY }
  );
}

Why 15 minutes? A short-lived access token limits the damage window if a token is stolen. The user re-authenticates transparently using a refresh token.

Refresh Token Rotation

Refresh tokens are long-lived credentials stored in the database. When a client exchanges a refresh token for a new access token, you rotate the refresh token to detect token theft:

typescript Code Block
import crypto from 'crypto';

async function generateRefreshToken(userId: string): Promise<string> {
  const token = crypto.randomBytes(40).toString('hex');
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days

  await RefreshToken.create({ token, userId, expiresAt });
  return token;
}

async function rotateRefreshToken(oldToken: string) {
  const existing = await RefreshToken.findOne({ token: oldToken });

  if (!existing || existing.expiresAt < new Date()) {
    // Token not found or expired — possible theft, revoke all tokens for this user
    if (existing) await RefreshToken.deleteMany({ userId: existing.userId });
    throw new Error('Invalid refresh token');
  }

  // Delete old token and issue new one
  await RefreshToken.deleteOne({ _id: existing._id });
  const newToken = await generateRefreshToken(existing.userId);
  const accessToken = generateAccessToken(
    await User.findById(existing.userId)
  );

  return { accessToken, refreshToken: newToken };
}

If someone replays an old refresh token, you know the family has been compromised and revoke everything.

Authentication Middleware

Protect routes by verifying the access token on every request:

typescript Code Block
import { Request, Response, NextFunction } from 'express';

export function authenticate(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Access token required' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const payload = jwt.verify(token, ACCESS_TOKEN_SECRET) as jwt.JwtPayload;
    req.user = { id: payload.sub!, email: payload.email, role: payload.role };
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

Login Endpoint

Bringing it all together:

typescript Code Block
router.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await User.findOne({ email });
  if (!user || !(await verifyPassword(password, user.passwordHash))) {
    return res.status(401).json({ error: 'Invalid email or password' });
  }

  const accessToken = generateAccessToken({
    id: user._id.toString(),
    email: user.email,
    role: user.role,
  });
  const refreshToken = await generateRefreshToken(user._id.toString());

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

  res.json({ accessToken });
});

Token Blacklisting for Logout

When a user logs out, invalidate their refresh token and blacklist the access token until it expires:

typescript Code Block
router.post('/auth/logout', authenticate, async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (refreshToken) await RefreshToken.deleteOne({ token: refreshToken });

  // Blacklist the access token in Redis until its expiry
  const token = req.headers.authorization!.split(' ')[1];
  const payload = jwt.decode(token) as jwt.JwtPayload;
  const ttl = payload.exp! - Math.floor(Date.now() / 1000);
  if (ttl > 0) await redis.set(`blacklist:${token}`, '1', 'EX', ttl);

  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out successfully' });
});

Security Checklist

  • Hash passwords with bcrypt (12+ salt rounds).
  • Short-lived access tokens (15 minutes max).
  • Rotate refresh tokens on every use.
  • Store refresh tokens in httpOnly, secure, sameSite cookies — never localStorage.
  • Blacklist access tokens on logout using Redis.
  • Use HTTPS everywhere — tokens in plaintext over HTTP are useless.
  • Implement rate limiting on login endpoints to prevent brute-force attacks.

Key Takeaways

  • Authentication is a layered system — password hashing, token generation, token verification, and token revocation all play a role.
  • Refresh token rotation is the gold standard for detecting stolen credentials.
  • Never store sensitive tokens in localStorage — use httpOnly cookies.
  • Treat authentication as an evolving concern — revisit it quarterly.

Get authentication right, and you build trust with every user who logs in.

#Authentication#JWT#Security#Node.js
Bablu Kumar Singh
Written by

Bablu Kumar Singh

Backend-Focused Full Stack Developer

Backend-Focused Full Stack Developer specializing in Node.js, MongoDB, PostgreSQL, Redis, RabbitMQ, AWS, Docker, System Design, and React Native.

You May Also Like

Designing a Role-Based Access Control (RBAC) System from Scratch
Backend Development
5 min read

Designing a Role-Based Access Control (RBAC) System from Scratch

A blueprint on implementing hierarchical permission matrices in Express.js middleware using bitwise operations and MongoDB caching.

May 24, 2026Read
Building Robust Role-Based Access Control (RBAC) Systems
Backend Development
6 min read

Building Robust Role-Based Access Control (RBAC) Systems

A practical guide to designing and implementing RBAC in Node.js applications — covering permission models, middleware design, hierarchical roles, and database schema patterns.

May 24, 2026Read
Node.js API Design Best Practices
Backend Development
7 min read

Node.js API Design Best Practices

A comprehensive guide to designing clean, scalable, and maintainable REST APIs with Node.js and Express.js — covering project structure, validation, error handling, versioning, and security.

May 2, 2026Read