Authentication
JWT
Authentication
Token Security
Best Practices
+1 more

JWT Security: Vulnerabilities, Best Practices & Implementation Guide

SCR Team
February 15, 2026
15 min read
Share

JWT Security in 2025: Complete Guide to Vulnerabilities & Best Practices

JSON Web Tokens (JWTs) power authentication in 94% of modern APIs (Source: Postman State of API Report, 2024). Yet JWT misconfigurations remain among the top API security risks (OWASP API Top 10). This guide provides research-backed best practices, real breach case studies, and production-ready code.


What is a JWT? Token Anatomy Explained

A JWT consists of three Base64-encoded parts separated by dots:

┌────────────────────────────────────────────────────────────────┐
│  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9                          │ ◄── Header
│  .                                                              │
│  eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIn │ ◄── Payload
│  .                                                              │
│  SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c                   │ ◄── Signature
└────────────────────────────────────────────────────────────────┘
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload (Claims)

{
  "sub": "user123",
  "email": "user@example.com",
  "iat": 1708012800,
  "exp": 1708013700,
  "aud": "api.example.com",
  "iss": "auth.example.com"
}

Signature

RSASHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), privateKey)

JWT Security Statistics (2025)

MetricValueSource
APIs using JWT94%Postman 2024
JWT-related breaches (2024)23% of auth breachesVerizon DBIR
Avg. breach cost (auth failures)$4.76MIBM Cost of Data Breach 2024
Time to exploit weak JWT< 2 hoursAuth0 Security Research

Top 5 JWT Vulnerabilities & How to Fix Them

1. Algorithm Confusion Attack (CVE-2015-9235)

The Problem: Attacker changes alg header from RS256 to HS256, using the public key as the HMAC secret.

// ❌ VULNERABLE: Accepts any algorithm
const decoded = jwt.verify(token, publicKey);
// ✅ SECURE: Whitelist algorithms
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256']  // ONLY RS256 allowed
});

Real Breach: Auth0 libraries were vulnerable until 2015. Attackers forged admin tokens.


2. Missing Token Expiration

The Problem: Tokens without exp claim never expire—compromised tokens are valid forever.

// ❌ DANGEROUS: No expiration
jwt.sign({ userId: 123 }, secret);

// ✅ SECURE: Short-lived tokens
jwt.sign({ userId: 123 }, secret, { expiresIn: '15m' });

Best Practice:

  • Access tokens: 15-30 minutes
  • Refresh tokens: 7 days (stored securely)

3. Token Storage in localStorage (XSS Vulnerability)

The Problem: JavaScript can access localStorage—XSS attacks steal tokens.

┌─────────────────────────────────────────────────────────────────┐
│  XSS Attack Flow                                                │
│                                                                 │
│  1. Attacker injects malicious script                           │
│  2. Script reads localStorage.getItem('token')                  │
│  3. Token exfiltrated to attacker server                        │
│  4. Attacker impersonates victim                                │
└─────────────────────────────────────────────────────────────────┘
// ❌ VULNERABLE: XSS can steal this
localStorage.setItem('token', jwt);

// ✅ SECURE: httpOnly cookies (JS cannot access)
res.cookie('token', jwt, {
  httpOnly: true,    // Cannot be read by JavaScript
  secure: true,      // HTTPS only
  sameSite: 'strict' // CSRF protection
});

4. HS256 with Weak/Leaked Secrets

The Problem: HS256 uses symmetric keys. If secret leaks, attacker forges any token.

// ❌ WEAK: Short, guessable secret
jwt.sign(payload, 'secret123');

// ❌ LEAKED: Secret in source code
const SECRET = 'production-jwt-secret-2024';

// ✅ SECURE: RS256 (asymmetric)
jwt.sign(payload, privateKey, { algorithm: 'RS256' });

Key Comparison:

AlgorithmKey TypeBest ForRisk
HS256SymmetricSingle serverSecret leak = full compromise
RS256AsymmetricDistributed systemsPublic key is safe to share
ES256Asymmetric (ECDSA)Mobile/IoTSmaller keys, same security

5. Missing Claim Validation

The Problem: Accepting tokens without validating aud, iss, or custom claims.

// ❌ VULNERABLE: No claim validation
const decoded = jwt.verify(token, secret);

// ✅ SECURE: Full validation
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],
  audience: 'api.example.com',
  issuer: 'auth.example.com',
  clockTolerance: 30  // 30 seconds clock skew tolerance
});

// Additional validation
if (!decoded.userId || !decoded.email) {
  throw new Error('Invalid token claims');
}

Refresh Token Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                     Refresh Token Flow                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Client                    Server                    Database        │
│    │                         │                           │           │
│    │──── Login ─────────────►│                           │           │
│    │                         │──── Store Refresh ───────►│           │
│    │◄─── Access + Refresh ───│                           │           │
│    │                         │                           │           │
│    │─── API Request ────────►│                           │           │
│    │     (Access Token)      │                           │           │
│    │◄───── Response ─────────│                           │           │
│    │                         │                           │           │
│    │  [Access Token Expires] │                           │           │
│    │                         │                           │           │
│    │─── Refresh Request ────►│                           │           │
│    │    (Refresh Token)      │──── Validate ────────────►│           │
│    │                         │◄─── Valid ────────────────│           │
│    │◄─── New Access Token ───│──── Rotate Refresh ──────►│           │
│    │                         │                           │           │
└─────────────────────────────────────────────────────────────────────┘

Implementation

// Login: Issue both tokens
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);
  
  const accessToken = jwt.sign(
    { userId: user.id, email: user.email },
    privateKey,
    { algorithm: 'RS256', expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion },
    privateKey,
    { algorithm: 'RS256', expiresIn: '7d' }
  );
  
  // Store refresh token hash in DB for revocation
  await storeRefreshToken(user.id, refreshToken);
  
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/api/refresh'  // Only sent to refresh endpoint
  });
  
  res.json({ accessToken });
});

// Refresh: Issue new access token
app.post('/api/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  
  const decoded = jwt.verify(refreshToken, publicKey, {
    algorithms: ['RS256']
  });
  
  // Check if token is revoked
  if (await isTokenRevoked(decoded.userId, refreshToken)) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  
  const newAccessToken = jwt.sign(
    { userId: decoded.userId },
    privateKey,
    { algorithm: 'RS256', expiresIn: '15m' }
  );
  
  res.json({ accessToken: newAccessToken });
});

JWT Security Checklist

  • Use RS256 or ES256 (asymmetric) over HS256
  • Set short expiration (15-30 min for access tokens)
  • Store tokens in httpOnly cookies (not localStorage)
  • Implement refresh token rotation
  • Whitelist algorithms in verify options
  • Validate all claims (exp, iat, aud, iss)
  • Use HTTPS everywhere
  • Implement token revocation for logout
  • Rotate signing keys quarterly
  • Rate limit token endpoints
  • Monitor for anomalous token patterns
  • Never log or expose tokens in errors

Real-World JWT Breaches

1. Auth0 Algorithm Confusion (2015)

  • Vulnerability: Library accepted HS256 when expecting RS256
  • Impact: Token forgery possible with public key
  • Fix: Strict algorithm whitelisting

2. Zoom JWT Vulnerabilities (2020)

  • Vulnerability: No audience validation, long-lived tokens
  • Impact: Meeting hijacking
  • Fix: Short expiration + audience validation

3. Palo Alto PAN-OS (2024, CVE-2024-0012)

  • Vulnerability: JWT authentication bypass
  • Impact: Admin access without credentials
  • Fix: Proper signature verification

JWT vs Session Comparison

FeatureJWTSessions
Server storageNone (stateless)Required
ScalabilityExcellentRequires shared store
RevocationComplexImmediate
Mobile/APIIdealRequires cookies
Token sizeLarger (payload)Small (session ID)
Best forAPIs, microservicesTraditional web apps

Tools & Resources

ToolPurposeURL
jwt.ioToken debuggerhttps://jwt.io
jwt_toolPenetration testingGitHub
joseJS JWT librarynpmjs.com/jose
PyJWTPython librarypypi.org/project/PyJWT

SEO Summary & Key Takeaways

  1. Use RS256/ES256 — Asymmetric algorithms prevent secret leakage
  2. 15-minute expiration — Limit blast radius of stolen tokens
  3. httpOnly cookies — Eliminate XSS token theft
  4. Whitelist algorithms — Prevent algorithm confusion attacks
  5. Validate all claims — aud, iss, exp are mandatory

Next steps: Audit your JWT implementation against the checklist above. Use jwt.io to decode and inspect your tokens (never in production!).


Last updated: February 2025 | Sources: OWASP, Auth0, RFC 7519, Postman API Report 2024

Advertisement