Implementing JWT Authentication in Node.js: From Zero to Secure

Published nodejsjwtauthenticationexpress

JWT auth shows up in almost every Node.js tutorial, but most examples stop at “here’s how to sign a token.” Real production auth needs: secure secret management, proper claim structure, protected route middleware, and a refresh token flow. This guide builds the full pattern from a login endpoint to a protected route — with the security settings that actually matter.

Setup

npm install jsonwebtoken bcryptjs
// .env
JWT_SECRET=your-256-bit-random-secret   # openssl rand -hex 32
JWT_EXPIRES_IN=15m
REFRESH_SECRET=another-256-bit-secret
REFRESH_EXPIRES_IN=7d

Token Generation — Login Endpoint

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

async function login(req, res) {
  const { email, password } = req.body;

  // 1. Find user and verify password
  const user = await User.findByEmail(email);
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 2. Sign access token — minimal claims, short lifetime
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },          // payload: only what you need
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN, issuer: 'your-app' }
  );

  // 3. Sign refresh token — longer lifetime, stored server-side
  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' },
    process.env.REFRESH_SECRET,
    { expiresIn: process.env.REFRESH_EXPIRES_IN }
  );

  // Store refresh token hash in DB for revocation
  await storeRefreshToken(user.id, refreshToken);

  // Return access token in body, refresh token in httpOnly cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true, secure: true, sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
  });
  res.json({ accessToken });
}

Protected Route Middleware

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  const token = authHeader?.replace(/^Bearer\s+/i, '');

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

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],          // always specify — prevents alg:none
      issuer: 'your-app',             // reject tokens from other issuers
    });
    req.user = payload;               // { sub, role, iat, exp, iss }
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    res.status(401).json({ error: 'Invalid token' });
  }
}

// Usage
app.get('/api/profile', authenticate, (req, res) => {
  res.json({ userId: req.user.sub, role: req.user.role });
});

Refresh Token Endpoint

async function refreshAccessToken(req, res) {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  try {
    const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET, {
      algorithms: ['HS256'],
    });

    // Verify it's a refresh token and still valid in DB
    const stored = await getRefreshToken(payload.sub, refreshToken);
    if (!stored) return res.status(401).json({ error: 'Refresh token revoked' });

    // Issue new access token
    const accessToken = jwt.sign(
      { sub: payload.sub, role: stored.user.role },
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_EXPIRES_IN, issuer: 'your-app' }
    );

    res.json({ accessToken });
  } catch {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
}

Security Checklist

Quick reference before you ship:

  • ✅ Secret is at least 256 bits, stored in env vars
  • algorithms option specified in jwt.verify()
  • ✅ Access token lifetime ≤ 1 hour
  • ✅ Refresh tokens stored server-side (enables revocation)
  • ✅ Refresh tokens in httpOnly; Secure; SameSite=Strict cookies
  • issuer claim set and verified
  • ❌ No secrets or PII in the payload
  • ❌ No alg: none possibility

Verify your token structure during development with our free JWT decoder — paste the access token to confirm the claims, expiry, and algorithm are exactly what you expect.

Further Reading

Try it in your browser

No setup needed — use our free JWT Decoder directly online.

Open JWT Decoder →

Sven Schuchardt

Management Consulting · Enterprise Architecture

Bridging the gap between business need and IT & Architecture enablers. With a background in management consulting and enterprise architecture, translating complex technology decisions into clear, actionable insights — written for every stakeholder, from the boardroom to the engineering team.

Connect on LinkedIn