← Back to Blog

JSON Web Tokens (JWT): Authentication Guide

Complete guide to JWT authentication. Learn JWT structure, implementation, security best practices, and token refresh strategies.

Michael Rodriguez14 min readadvanced
M

Michael Rodriguez

API & Security Engineer

Michael is an API engineer and security specialist with over 7 years of experience building RESTful services, data conversion pipelines, and authentication systems. He writes practical guides on JSON Web Tokens, API debugging strategies, data science applications of JSON, and modern AI tooling workflows including MCP and JSON-RPC.

REST APIsJWT & SecurityData ScienceJSON PathMCP / AI ToolingAPI Debugging
14 min read

What is JWT?

JSON Web Token (JWT) is a compact, URL-safe token format for securely transmitting information between parties.

JWT Structure

A JWT has three parts separated by dots:

header.payload.signature
{

"alg": "HS256",

"typ": "JWT"

}

Payload (Claims)

{

"sub": "1234567890",

"name": "John",

"iat": 1516239022,

"exp": 1516242622

}

Signature

Created by signing header + payload with secret key.

Standard Claims

  • sub: Subject (user ID)
  • iss: Issuer
  • aud: Audience
  • exp: Expiration time
  • iat: Issued at time
  • nbf: Not before

Implementation

Node.js

import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET;

// Create token

function createToken(user) {

return jwt.sign(

{ sub: user.id, email: user.email },

SECRET,

{ expiresIn: '1h' }

);

}

// Verify token

function verifyToken(token) {

try {

return jwt.verify(token, SECRET);

} catch (error) {

return null;

}

}

Python

import jwt

from datetime import datetime, timedelta

SECRET = os.environ['JWT_SECRET']

def create_token(user):

payload = {

'sub': user['id'],

'exp': datetime.utcnow() + timedelta(hours=1)

}

return jwt.encode(payload, SECRET, algorithm='HS256')

def verify_token(token):

try:

return jwt.decode(token, SECRET, algorithms=['HS256'])

except jwt.InvalidTokenError:

return None

Express Middleware

function authMiddleware(req, res, next) {

const token = req.headers.authorization?.split(' ')[1];

if (!token) {

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

}

try {

req.user = jwt.verify(token, SECRET);

next();

} catch (error) {

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

}

}

app.get('/protected', authMiddleware, (req, res) => {

res.json({ user: req.user });

});

Token Refresh Strategy

// Short-lived access token

function createAccessToken(user) {

return jwt.sign(

{ sub: user.id, type: 'access' },

ACCESS_SECRET,

{ expiresIn: '15m' }

);

}

// Long-lived refresh token

function createRefreshToken(user) {

return jwt.sign(

{ sub: user.id, type: 'refresh' },

REFRESH_SECRET,

{ expiresIn: '7d' }

);

}

Security Best Practices

  • Use strong secrets (256+ bits random, never commit to git)
  • Set short expiration for access tokens (5-15 minutes)
  • Validate all claims (algorithm, issuer, audience, expiration)
  • Store tokens securely (HttpOnly cookies, not localStorage)
  • Implement token revocation (blacklist or database)
  • Use HTTPS only to prevent token interception
  • Rotate secrets regularly (every 90 days)
  • Never store sensitive data in payload (passwords, PII)
  • Validate algorithm explicitly (prevent alg: none attack)
  • Implement rate limiting on auth endpoints
  • Common Security Mistakes

    1. Don't Store Sensitive Data

    // ❌ Bad - payload is base64, not encrypted!
    

    const token = jwt.sign({

    userId: 123,

    password: 'secret123', // NEVER!

    creditCard: '4532...', // NEVER!

    ssn: '123-45-6789' // NEVER!

    }, secret);

    // ✅ Good - only non-sensitive identifiers

    const token = jwt.sign({

    sub: 123,

    email: 'user@example.com',

    role: 'user',

    permissions: ['read', 'write']

    }, secret, { expiresIn: '15m' });

    2. Always Validate Algorithm (Prevent alg:none Attack)

    // ❌ Vulnerable to algorithm confusion attack
    

    jwt.verify(token, secret);

    // ✅ Secure - explicitly allow only specific algorithms

    jwt.verify(token, secret, {

    algorithms: ['HS256'], // Only allow HMAC SHA-256

    issuer: 'myapp.com',

    audience: 'myapp-users'

    });

    3. Use HttpOnly Cookies (Prevent XSS)

    // ❌ Vulnerable to XSS attacks
    

    res.json({ token }); // Client stores in localStorage

    // ✅ Secure - HttpOnly prevents JavaScript access

    res.cookie('accessToken', token, {

    httpOnly: true, // Not accessible via JavaScript

    secure: true, // HTTPS only

    sameSite: 'strict', // Prevent CSRF

    maxAge: 15 60 1000 // 15 minutes

    });

    Token Refresh Strategy

    Dual Token Pattern (Access + Refresh)

    const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
    

    const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;

    // Short-lived access token (15 minutes)

    function createAccessToken(user) {

    return jwt.sign(

    {

    sub: user.id,

    email: user.email,

    role: user.role,

    type: 'access'

    },

    ACCESS_TOKEN_SECRET,

    { expiresIn: '15m', issuer: 'myapp.com' }

    );

    }

    // Long-lived refresh token (7 days)

    function createRefreshToken(user) {

    return jwt.sign(

    {

    sub: user.id,

    type: 'refresh',

    tokenVersion: user.tokenVersion // For revocation

    },

    REFRESH_TOKEN_SECRET,

    { expiresIn: '7d', issuer: 'myapp.com' }

    );

    }

    // Login endpoint

    app.post('/api/login', async (req, res) => {

    const user = await authenticateUser(req.body.email, req.body.password);

    if (!user) {

    return res.status(401).json({ error: 'Invalid credentials' });

    }

    const accessToken = createAccessToken(user);

    const refreshToken = createRefreshToken(user);

    // Store refresh token in database for revocation capability

    await db.refreshTokens.create({

    userId: user.id,

    token: refreshToken,

    expiresAt: new Date(Date.now() + 7 24 60 60 1000)

    });

    // Send tokens via HttpOnly cookies

    res.cookie('accessToken', accessToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 60 1000 });

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

    res.json({ user: { id: user.id, email: user.email, role: user.role } });

    });

    // Refresh endpoint

    app.post('/api/refresh', async (req, res) => {

    const { refreshToken } = req.cookies;

    if (!refreshToken) {

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

    }

    try {

    const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, { algorithms: ['HS256'] });

    // Check if token is in database (not revoked)

    const tokenExists = await db.refreshTokens.findOne({

    userId: payload.sub,

    token: refreshToken

    });

    if (!tokenExists) {

    return res.status(401).json({ error: 'Token revoked' });

    }

    // Get user and check token version

    const user = await db.users.findById(payload.sub);

    if (user.tokenVersion !== payload.tokenVersion) {

    return res.status(401).json({ error: 'Token invalidated' });

    }

    // Issue new access token

    const newAccessToken = createAccessToken(user);

    res.cookie('accessToken', newAccessToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 60 1000 });

    res.json({ success: true });

    } catch (error) {

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

    }

    });

    Token Revocation Strategies

    Database Blacklist

    // Logout: add token to blacklist
    

    app.post('/api/logout', authenticate, async (req, res) => {

    const token = req.cookies.accessToken;

    const decoded = jwt.decode(token);

    // Store in blacklist until expiration

    await db.tokenBlacklist.create({

    token,

    expiresAt: new Date(decoded.exp 1000)

    });

    // Delete refresh token

    await db.refreshTokens.deleteMany({ userId: req.user.sub });

    res.clearCookie('accessToken');

    res.clearCookie('refreshToken');

    res.json({ success: true });

    });

    // Middleware to check blacklist

    function authenticate(req, res, next) {

    const token = req.cookies.accessToken;

    if (!token) {

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

    }

    // Check blacklist first

    const isBlacklisted = await db.tokenBlacklist.findOne({ token });

    if (isBlacklisted) {

    return res.status(401).json({ error: 'Token revoked' });

    }

    try {

    req.user = jwt.verify(token, ACCESS_TOKEN_SECRET, { algorithms: ['HS256'] });

    next();

    } catch (error) {

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

    }

    }

    Token Version Increment

    // Force logout all devices by incrementing user's token version
    

    app.post('/api/logout-all-devices', authenticate, async (req, res) => {

    await db.users.update(

    { id: req.user.sub },

    { $inc: { tokenVersion: 1 } } // Increment version

    );

    await db.refreshTokens.deleteMany({ userId: req.user.sub });

    res.json({ success: true });

    });

    Client-Side Token Handling

    Automatic Token Refresh

    class ApiClient {
    

    constructor() {

    this.isRefreshing = false;

    this.failedQueue = [];

    }

    async fetch(url, options = {}) {

    try {

    const response = await fetch(url, {

    ...options,

    credentials: 'include' // Send cookies

    });

    if (response.status === 401) {

    // Token expired, try refresh

    return await this.handleTokenExpired(url, options);

    }

    return response;

    } catch (error) {

    throw error;

    }

    }

    async handleTokenExpired(url, options) {

    if (this.isRefreshing) {

    // Queue request until refresh completes

    return new Promise((resolve, reject) => {

    this.failedQueue.push({ resolve, reject, url, options });

    });

    }

    this.isRefreshing = true;

    try {

    // Refresh token

    await fetch('/api/refresh', {

    method: 'POST',

    credentials: 'include'

    });

    // Retry original request

    const response = await fetch(url, options);

    // Process queued requests

    this.failedQueue.forEach(({ resolve, url, options }) => {

    resolve(fetch(url, options));

    });

    this.failedQueue = [];

    return response;

    } catch (error) {

    // Refresh failed, logout

    window.location.href = '/login';

    throw error;

    } finally {

    this.isRefreshing = false;

    }

    }

    }

    const api = new ApiClient();

    // Usage

    const response = await api.fetch('/api/protected-data');

    const data = await response.json();

    Production Best Practices

    Environment-Based Secrets

    # .env (NEVER commit to git)
    

    JWT_ACCESS_SECRET=a1b2c3d4e5f6... # 64+ random chars

    JWT_REFRESH_SECRET=x9y8z7w6v5u4... # Different secret

    JWT_ISSUER=myapp.com

    JWT_AUDIENCE=myapp-users

    import crypto from 'crypto';
    
    

    // Generate secure secret

    const secret = crypto.randomBytes(64).toString('hex');

    console.log(secret); // Use this in production

    Rate Limiting Auth Endpoints

    import rateLimit from 'express-rate-limit';
    
    

    const authLimiter = rateLimit({

    windowMs: 15 60 * 1000, // 15 minutes

    max: 5, // 5 attempts

    message: { error: 'Too many login attempts, try again later' },

    standardHeaders: true

    });

    app.post('/api/login', authLimiter, loginHandler);

    app.post('/api/refresh', authLimiter, refreshHandler);

    Audit Logging

    app.post('/api/login', async (req, res) => {
    

    const user = await authenticateUser(req.body.email, req.body.password);

    // Log authentication attempt

    await db.auditLog.create({

    userId: user?.id,

    email: req.body.email,

    action: user ? 'LOGIN_SUCCESS' : 'LOGIN_FAILED',

    ip: req.ip,

    userAgent: req.get('user-agent'),

    timestamp: new Date()

    });

    // ...

    });

    Testing JWT Implementation

    import request from 'supertest';
    

    import app from './app';

    describe('JWT Authentication', () => {

    it('should issue tokens on login', async () => {

    const response = await request(app)

    .post('/api/login')

    .send({ email: 'test@example.com', password: 'password123' })

    .expect(200);

    const cookies = response.headers['set-cookie'];

    expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true);

    expect(cookies.some(c => c.startsWith('refreshToken='))).toBe(true);

    });

    it('should reject expired tokens', async () => {

    const expiredToken = jwt.sign(

    { sub: 123 },

    ACCESS_TOKEN_SECRET,

    { expiresIn: '-1h' } // Already expired

    );

    await request(app)

    .get('/api/protected')

    .set('Cookie', accessToken=${expiredToken})

    .expect(401);

    });

    it('should refresh access token', async () => {

    // Login first

    const loginRes = await request(app)

    .post('/api/login')

    .send({ email: 'test@example.com', password: 'password123' });

    const refreshToken = loginRes.headers['set-cookie']

    .find(c => c.startsWith('refreshToken='))

    .split(';')[0]

    .split('=')[1];

    // Refresh

    const refreshRes = await request(app)

    .post('/api/refresh')

    .set('Cookie', refreshToken=${refreshToken})

    .expect(200);

    expect(refreshRes.headers['set-cookie']).toBeDefined();

    });

    });

    Conclusion

    JWT is powerful for stateless authentication but requires careful implementation. Use short-lived access tokens (15m), long-lived refresh tokens (7d), HttpOnly cookies (not localStorage), and explicit algorithm validation. Always validate all claims (exp, iss, aud), implement token revocation via blacklist or token versioning, and never store sensitive data in the payload. For production, use strong random secrets (64+ chars), rate limit auth endpoints, audit all auth events, and rotate secrets regularly.

    Share:

    Related Articles