Skip to content

JWT Security: Best Practices for JSON Web Token Implementation

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about JWT Security: Best Practices for JSON Web Token Implementation. We cover key concepts, practical examples, and best practices to help you master this topic.

JSON Web Tokens (JWT) are a compact, URL-safe token format used for authentication and information exchange. While JWTs are widely used, many implementations have critical security flaws: weak algorithms, missing validation, secret leakage, and improper token storage.

flowchart TB
    subgraph Secure JWT Flow
        Login[User Login] --> Issue[Server Issues JWT]
        Issue --> Claims[Include: sub, iat, exp, jti]
        Claims --> Sign[Sign with RS256 or ES256]
        Sign --> Client[Client Stores in httpOnly Cookie]
        Client --> Request[API Request with JWT]
        Request --> Validate[Server Validates: sig, exp, iss, aud]
        Validate -->|Valid| Process[Process Request]
        Validate -->|Invalid| Reject[401 Unauthorized]
    end
    
    subgraph Common Attacks
        None[alg: none Attack]
        Weak[alg: HS256 with Public Key]
        XSS[XSS Steals Token]
        Exp[No Expiration Validation]
    end

What You'll Learn

  • Secure JWT signing algorithms (RS256, ES256 vs HS256)
  • Token validation: signature, expiration, issuer, audience
  • Secure token storage: httpOnly cookies vs localStorage
  • Refresh token rotation and revocation

Why It Matters

JWT vulnerabilities are among the most common authentication flaws. The "alg: none" attack, algorithm confusion attacks, and token theft via XSS have compromised countless applications. Proper JWT implementation is essential for secure token-based authentication.

Real-World Use

A payments API uses RS256-signed JWTs with 15-minute expiry. Access tokens are stored in httpOnly, Secure, SameSite cookies. Refresh tokens are stored in the database with a device fingerprint. On logout or password change, all refresh tokens for the user are revoked.

Secure JWT Implementation

Secure Token Generation (RS256)

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Generate RSA key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 4096,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

function generateAccessToken(user) {
  return jwt.sign(
    {
      sub: user.id,
      role: user.role,
      email: user.email,
      type: 'access'
    },
    privateKey,
    {
      algorithm: 'RS256',
      expiresIn: '15m',
      issuer: 'https://api.example.com',
      jwtid: crypto.randomUUID()
    }
  );
}

function generateRefreshToken(user) {
  return jwt.sign(
    {
      sub: user.id,
      type: 'refresh',
      tokenVersion: user.tokenVersion
    },
    privateKey,
    {
      algorithm: 'RS256',
      expiresIn: '7d',
      issuer: 'https://api.example.com',
      jwtid: crypto.randomUUID()
    }
  );
}

Expected output:

Access token: RS256-signed, 15-min expiry, includes sub, role, jti.
Refresh token: RS256-signed, 7-day expiry, includes tokenVersion for revocation.

Token Validation Middleware

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

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

  try {
    const decoded = jwt.verify(token, publicKey, {
      algorithms: ['RS256'],
      issuer: 'https://api.example.com',
      maxAge: '15m'
    });

    if (decoded.type !== 'access') {
      return res.status(401).json({ error: 'Invalid token type' });
    }

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

Expected output:

Valid token: req.user populated, request proceeds.
Expired token: 401 with code TOKEN_EXPIRED.
Invalid signature: 403 Forbidden.

Refresh Token Rotation

const crypto = require('crypto');

// Store refresh tokens with rotation
const refreshTokens = new Map(); // Use Redis in production

async function rotateRefreshToken(oldToken, user) {
  // Verify old refresh token
  let decoded;
  try {
    decoded = jwt.verify(oldToken, publicKey, {
      algorithms: ['RS256'],
      issuer: 'https://api.example.com'
    });
  } catch (err) {
    return { error: 'Invalid refresh token' };
  }

  // Check if token exists and is not revoked
  const stored = refreshTokens.get(decoded.jti);
  if (!stored || stored.revoked) {
    // Possible token theft: revoke all tokens for user
    revokeAllUserTokens(user.id);
    return { error: 'Token reuse detected' };
  }

  // Revoke old token
  refreshTokens.set(decoded.jti, { ...stored, revoked: true });

  // Issue new tokens
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);
  const rtDecoded = jwt.decode(refreshToken);
  refreshTokens.set(rtDecoded.jti, {
    userId: user.id,
    deviceFingerprint: req.headers['user-agent'],
    createdAt: new Date(),
    revoked: false
  });

  return { accessToken, refreshToken };
}

Expected output:

Old refresh token is revoked (consumed). New access + refresh token pair is issued. If old token is reused, all tokens for user are revoked.

Common Mistakes

  • Using HS256 (symmetric) with a weak or leaked secret — use RS256 or ES256 (asymmetric) so the public key can validate without exposing the private key.
  • Not validating the alg header — attackers can change alg to none or switch from RS256 to HS256 if they know the public key.
  • Storing tokens in localStorage, which is accessible to any JavaScript on the same origin (XSS vulnerability).
  • Using excessively long token expiry (days or weeks) without refresh token rotation.
  • Not including a jti (JWT ID) claim, making token revocation impossible without tracking all issued tokens.

Practice Questions

  1. Why is RS256 more secure than HS256 for JWT signing?
  2. What is the "alg: none" attack and how do you prevent it?
  3. Why should access tokens have short expiry (e.g., 15 minutes)?
  4. How does refresh token rotation prevent token theft?
  5. Why is httpOnly cookie storage safer than localStorage for tokens?

Challenge

Build a complete JWT authentication system with: RS256 signing, access tokens (15min) + refresh tokens (7 days), refresh token rotation with reuse detection, token revocation on password change, and httpOnly cookie storage. Write tests for token expiry, invalid signature, and token reuse.

FAQ

What is the best algorithm for JWT?

Use RS256 (RSA with SHA-256) or ES256 (ECDSA with SHA-256). These are asymmetric: the private key signs, the public key validates. Never use 'none' or symmetric algorithms with shared secrets.

Where should I store JWTs?

In httpOnly, Secure, SameSite cookies. This prevents XSS from reading the token and CSRF from using it (with SameSite). If cookies are not possible, store in memory only.

What is the algorithm confusion attack?

If the server uses a public key for validation but accepts tokens signed with HS256, an attacker can sign tokens using the public key (which is public) as the HMAC secret.

How do I revoke a JWT before expiry?

JWTs are stateless and cannot be revoked server-side without a blocklist. Use short expiry (15 min) and implement refresh token rotation. For immediate revocation, maintain a token blocklist in Redis.

What claims should a JWT include?

Required: sub (user ID), iat (issued at), exp (expiration), jti (unique ID). Recommended: iss (issuer), aud (audience), type (access/refresh). Avoid including sensitive data.

Mini Project

Build a secure JWT authentication service. Implement: RS256 key pair generation, short-lived access tokens (15min), refresh tokens (7 days) with rotation, token blocklist in Redis for immediate revocation, httpOnly cookie storage, and comprehensive validation middleware.

What's Next

Continue to SSL/TLS to learn about securing data in transit.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro