JWT (JSON Web Token) is the most common authentication scheme in modern web applications. This article explains its principles and practices.

JWT Basics

What is JWT

JWT Structure:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Header.Payload.Signature                          │
│                                                     │
│   eyJhbGciOiJIUzI1NiJ9.                             │
│   eyJzdWIiOiJ1c2VyMTIzIn0.                          │
│   SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c      │
│                                                     │
│   ├── Header: Algorithm and type                    │
│   ├── Payload: User data and claims                 │
│   └── Signature: Verification signature             │
│                                                     │
└─────────────────────────────────────────────────────┘

Three Parts Explained

// Header - Describes token type and algorithm
{
  "alg": "HS256",  // Signing algorithm
  "typ": "JWT"    // Token type
}

// Payload - Contains claims
{
  // Registered Claims
  "iss": "auth.example.com",  // Issuer
  "sub": "user123",           // Subject (user ID)
  "aud": "api.example.com",   // Audience
  "exp": 1706486400,          // Expiration time
  "iat": 1706400000,          // Issued at
  "nbf": 1706400000,          // Not before

  // Private claims
  "userId": "user123",
  "role": "admin",
  "permissions": ["read", "write"]
}

// Signature
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Node.js Implementation

Generating Tokens

import jwt from 'jsonwebtoken';

const SECRET_KEY = process.env.JWT_SECRET;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';

// Generate access token
function generateAccessToken(user) {
  return jwt.sign(
    {
      userId: user.id,
      email: user.email,
      role: user.role
    },
    SECRET_KEY,
    {
      expiresIn: ACCESS_TOKEN_EXPIRY,
      issuer: 'api.example.com',
      audience: 'example.com'
    }
  );
}

// Generate refresh token
function generateRefreshToken(user) {
  return jwt.sign(
    { userId: user.id, tokenType: 'refresh' },
    SECRET_KEY,
    { expiresIn: REFRESH_TOKEN_EXPIRY }
  );
}

// Login example
async function login(email, password) {
  const user = await authenticateUser(email, password);
  if (!user) {
    throw new Error('Invalid credentials');
  }

  return {
    accessToken: generateAccessToken(user),
    refreshToken: generateRefreshToken(user),
    expiresIn: 900  // 15 minutes in seconds
  };
}

Verifying Tokens

// Verification middleware
function verifyToken(req, res, next) {
  const authHeader = req.headers.authorization;

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

  const token = authHeader.slice(7);

  try {
    const decoded = jwt.verify(token, SECRET_KEY, {
      issuer: 'api.example.com',
      audience: 'example.com'
    });

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

// Using middleware
app.get('/api/protected', verifyToken, (req, res) => {
  res.json({ message: `Hello ${req.user.email}` });
});

Refreshing Tokens

async function refreshAccessToken(refreshToken) {
  try {
    const decoded = jwt.verify(refreshToken, SECRET_KEY);

    if (decoded.tokenType !== 'refresh') {
      throw new Error('Invalid token type');
    }

    // Check if blacklisted
    const isBlacklisted = await checkBlacklist(refreshToken);
    if (isBlacklisted) {
      throw new Error('Token revoked');
    }

    // Get user info
    const user = await getUserById(decoded.userId);
    if (!user) {
      throw new Error('User not found');
    }

    // Generate new access token
    return {
      accessToken: generateAccessToken(user),
      expiresIn: 900
    };
  } catch (error) {
    throw new Error('Invalid refresh token');
  }
}

// Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  try {
    const tokens = await refreshAccessToken(refreshToken);
    res.json(tokens);
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
});

Frontend Handling

Token Storage

// Storage options comparison
/*
┌─────────────────────────────────────────────────────┐
│                                                     │
│   localStorage                                      │
│   ├── Persistent, survives refresh                 │
│   ├── Vulnerable to XSS attacks                    │
│   └── Suitable for: non-sensitive scenarios        │
│                                                     │
│   sessionStorage                                    │
│   ├── Lost when tab closes                         │
│   ├── Same XSS risk                                │
│   └── Suitable for: temporary sessions             │
│                                                     │
│   HttpOnly Cookie                                   │
│   ├── JavaScript cannot access                     │
│   ├── Needs CSRF protection                        │
│   └── Suitable for: high security requirements     │
│                                                     │
│   Memory (variable)                                 │
│   ├── Lost on page refresh                         │
│   ├── Most secure                                  │
│   └── Suitable for: combined with refresh token    │
│                                                     │
└─────────────────────────────────────────────────────┘
*/

// Memory storage approach
class TokenManager {
  constructor() {
    this.accessToken = null;
  }

  setToken(token) {
    this.accessToken = token;
  }

  getToken() {
    return this.accessToken;
  }

  clearToken() {
    this.accessToken = null;
  }

  isTokenExpired() {
    if (!this.accessToken) return true;

    try {
      const payload = JSON.parse(
        atob(this.accessToken.split('.')[1])
      );
      return Date.now() >= payload.exp * 1000;
    } catch {
      return true;
    }
  }
}

export const tokenManager = new TokenManager();

Auto Refresh

// Axios interceptors
import axios from 'axios';
import { tokenManager } from './token-manager';

const api = axios.create({
  baseURL: '/api'
});

// Request interceptor
api.interceptors.request.use(async (config) => {
  // Check if token is expiring soon (refresh 1 min before)
  if (tokenManager.isTokenExpiringSoon(60)) {
    await refreshToken();
  }

  const token = tokenManager.getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // 401 and not retried yet
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        await refreshToken();
        originalRequest.headers.Authorization =
          `Bearer ${tokenManager.getToken()}`;
        return api(originalRequest);
      } catch {
        // Refresh failed, redirect to login
        window.location.href = '/login';
        return Promise.reject(error);
      }
    }

    return Promise.reject(error);
  }
);

async function refreshToken() {
  const response = await axios.post('/api/auth/refresh', {
    refreshToken: getCookie('refreshToken')
  });
  tokenManager.setToken(response.data.accessToken);
}

Security Best Practices

Algorithm Selection

// Recommended: Use RS256 asymmetric encryption
import jwt from 'jsonwebtoken';
import fs from 'fs';

const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');

// Sign with private key
function signToken(payload) {
  return jwt.sign(payload, privateKey, {
    algorithm: 'RS256',
    expiresIn: '15m'
  });
}

// Verify with public key
function verifyToken(token) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256']
  });
}

Token Revocation

// Redis blacklist approach
import Redis from 'ioredis';

const redis = new Redis();

// Revoke token
async function revokeToken(token) {
  const decoded = jwt.decode(token);
  const exp = decoded.exp;
  const ttl = exp - Math.floor(Date.now() / 1000);

  if (ttl > 0) {
    await redis.setex(`blacklist:${token}`, ttl, '1');
  }
}

// Check if revoked
async function isTokenRevoked(token) {
  const result = await redis.get(`blacklist:${token}`);
  return result === '1';
}

// Logout
app.post('/api/auth/logout', verifyToken, async (req, res) => {
  const token = req.headers.authorization.slice(7);
  await revokeToken(token);
  res.json({ message: 'Logged out' });
});

Attack Prevention

JWT Security Checklist:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Signature Verification                            │
│   ├── Always verify signature                      │
│   ├── Check alg field, reject none                 │
│   └── Use strong keys (256+ bits)                  │
│                                                     │
│   Claims Verification                               │
│   ├── Verify exp expiration time                   │
│   ├── Verify iss issuer                            │
│   ├── Verify aud audience                          │
│   └── Verify nbf not before time                   │
│                                                     │
│   Transport Security                                │
│   ├── Only transmit over HTTPS                     │
│   ├── Set reasonable expiration times              │
│   └── Require re-auth for sensitive ops            │
│                                                     │
│   Storage Security                                  │
│   ├── Prefer HttpOnly Cookies                      │
│   ├── Set Secure and SameSite attributes           │
│   └── Avoid passing tokens in URLs                 │
│                                                     │
└─────────────────────────────────────────────────────┘

Complete Auth Flow

// Auth service
class AuthService {
  async login(email, password) {
    const user = await this.validateCredentials(email, password);

    const accessToken = this.generateAccessToken(user);
    const refreshToken = this.generateRefreshToken(user);

    // Store refresh token in database
    await this.saveRefreshToken(user.id, refreshToken);

    return { accessToken, refreshToken, user };
  }

  async logout(userId, refreshToken) {
    // Delete refresh token
    await this.deleteRefreshToken(userId, refreshToken);
  }

  async refreshTokens(refreshToken) {
    const decoded = jwt.verify(refreshToken, SECRET_KEY);

    // Verify refresh token exists in database
    const valid = await this.validateRefreshToken(
      decoded.userId,
      refreshToken
    );
    if (!valid) {
      throw new Error('Invalid refresh token');
    }

    const user = await this.getUserById(decoded.userId);
    return {
      accessToken: this.generateAccessToken(user)
    };
  }
}

Best Practices Summary

ItemRecommendation
Access token expiry5-15 minutes
Refresh token expiry7-30 days
Signing algorithmRS256 or HS256
Storage methodHttpOnly Cookie
Transport methodHTTPS + Authorization Header

JWT is simple to use, but secure implementation requires caution.