10 min read

Guide · Developer security

JWT Security Checklist

Essential security checklist for JWT implementation. Follow these proven practices to protect your applications from common JWT vulnerabilities and attacks.

Critical Security Priorities

1. Always Use HTTPSNever transmit JWTs over unencrypted connections
2. Set Short ExpirationLimit token lifetime to reduce attack window
3. Validate EverythingVerify signature, expiration, and all claims

Security Implementation Progress

Track your progress0 of 24

Token Generation Security

Enforce HTTPS Onlycritical

Never generate, transmit, or accept JWT tokens over unencrypted HTTP connections.

HTTPS
# VULNERABLE: Never do this
http://api.example.com/login

# SECURE: Always use HTTPS
https://api.example.com/login

Use Cryptographically Strong Secretscritical

Generate secrets with at least 256 bits of entropy. Never use predictable or weak secrets.

Node.js
// SECURE: Generate strong secret
const secret = crypto.randomBytes(32).toString('hex');
// Result: 64-character hex string (256 bits)

Set Short Token Expirationhigh

Use 15-60 minutes for access tokens. Implement refresh tokens for longer sessions.

Access Tokens15-60 minutes
API Tokens1-24 hours
Refresh Tokens7-30 days

Validate and Specify Algorithmcritical

Always specify and validate the expected signing algorithm. Prevent algorithm confusion attacks.

Node.js
// VULNERABLE: Accepts any algorithm
jwt.verify(token, secret)

// SECURE: Specify expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] })

Never Store Sensitive Data in JWTcritical

JWTs are encoded, not encrypted. Never include passwords, SSNs, or other sensitive information.

Never Include

  • Passwords or password hashes
  • Social Security Numbers
  • Credit card information
  • API secrets or keys
  • Personal identification numbers

Safe to Include

  • User ID (non-sequential)
  • Username or email
  • User roles and permissions
  • Non-sensitive metadata
  • Application preferences

Token Validation Security

Always Verify Token Signaturecritical

Never skip signature verification. This is your primary defense against token tampering.

Node.js
// SECURE: Proper verification
try {
  const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
  // Token is valid and verified
} catch (error) {
  // Invalid token - reject request
  return res.status(401).json({ error: 'Invalid token' });
}

Validate Token Expirationcritical

Always check the 'exp' claim. Expired tokens should be immediately rejected.

Node.js
// Expiration is automatically checked by most libraries
// But you can also manually verify:
if (decoded.exp && Date.now() >= decoded.exp * 1000) {
  throw new Error('Token has expired');
}

Validate Token Issuer (iss)high

Check that tokens come from expected issuers to prevent token confusion attacks.

Node.js
const decoded = jwt.verify(token, secret, {
  algorithms: ['HS256'],
  issuer: 'your-app-name',
  audience: 'your-api'
});

Validate Token Audience (aud)high

Ensure tokens are intended for your application by validating the audience claim.

Node.js
// Token should specify intended audience
const payload = {
  sub: 'user123',
  aud: 'api.yourapp.com', // Your API endpoint
  iss: 'yourapp.com'
};

Validate 'Not Before' Claim (nbf)medium

If using nbf claim, ensure tokens are not used before their valid time.

Node.js
if (decoded.nbf && Date.now() < decoded.nbf * 1000) {
  throw new Error('Token not yet valid');
}

Storage and Transmission Security

Use Secure Storage Methodshigh

Store tokens in httpOnly cookies or secure storage. Avoid localStorage for sensitive tokens.

Secure Options

  • httpOnly cookies
  • Secure cookies
  • SameSite cookies
  • Memory (temporary)

Use with Caution

  • localStorage (XSS risk)
  • sessionStorage
  • Regular cookies

Never Use

  • URL parameters
  • Referer headers
  • Unencrypted storage
  • Plain text files

Use Authorization Headerhigh

Send tokens in Authorization header with Bearer scheme, not in URL or body.

HTTP
# SECURE: Correct way
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

# VULNERABLE: Never in URL
https://api.example.com/data?token=eyJhbGc...

Implement CSRF Protectionhigh

Use CSRF tokens or double-submit cookie pattern when storing JWTs in cookies.

Node.js
// Set secure cookie with CSRF protection
res.cookie('jwt', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

Configure CORS Properlymedium

Set specific origins in CORS, never use wildcards (*) for credentials.

HTTP headers
# SECURE: Specific origins
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: true

# VULNERABLE: Never with credentials
Access-Control-Allow-Origin: *

Application Security

Implement Refresh Token Strategyhigh

Use refresh tokens for long-lived sessions to minimize exposure of access tokens.

Node.js
// Access token: short-lived (15-60 minutes)
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });

// Refresh token: longer-lived (7-30 days)
const refreshToken = jwt.sign({ type: 'refresh' }, secret, { expiresIn: '7d' });

Implement Token Blacklistinghigh

Maintain a blacklist for revoked tokens, especially for logout and security incidents.

Node.js
// Store revoked token IDs in Redis/database
await redis.setex('blacklist:' + jti, expTime, 'true');

// Check blacklist on each request
const isBlacklisted = await redis.get('blacklist:' + decoded.jti);

Implement Rate Limitingmedium

Add rate limiting to token endpoints to prevent brute force and abuse.

Login Endpoint

  • 5 attempts per minute per IP
  • 10 attempts per hour per user
  • Progressive delays

Token Refresh

  • 10 requests per minute
  • Monitor for unusual patterns
  • Implement circuit breakers

Implement Security Loggingmedium

Log security events for monitoring and incident response.

Node.js
// Log security events
logger.warn('Invalid JWT signature', { ip, userAgent, token: token.slice(0,20) });
logger.info('Token refreshed', { userId, ip });
logger.error('Expired token used', { userId, expired: decoded.exp });

Set Security Headersmedium

Configure security headers to protect against common attacks.

HTTP headers
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'

Development and Testing

Secure Environment Configurationcritical

Use environment variables for secrets, never hardcode in source code.

Node.js
// VULNERABLE: Never do this
const secret = 'mysecretkey123';
const secret = 'hardcoded-in-repo';

// SECURE: Always do this
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('Missing JWT_SECRET');

Use Different Secrets Per Environmenthigh

Development, staging, and production should use completely different secrets.

.env
# Development .env
JWT_SECRET=dev_secret_32_char_minimum_12345

# Production .env
JWT_SECRET=prod_different_secret_67890_xyz

# Never reuse secrets across environments

Test Security Scenariosmedium

Write tests for token validation, expiration, and attack scenarios.

Test cases
// Test cases to include:
// - Expired token rejection
// - Invalid signature detection
// - Malformed token handling
// - Algorithm confusion prevention
// - Missing claims validation

Regular Security Auditsmedium

Conduct regular security reviews and dependency updates.

Monthly Tasks

  • Review token lifetimes
  • Check for library updates
  • Analyze security logs
  • Review access patterns

Quarterly Tasks

  • Rotate signing secrets
  • Security penetration testing
  • Update security policies
  • Team security training

Document Security Decisionsmedium

Document your JWT configuration, security policies, and incident procedures.

  • Document algorithm choices and rationale
  • Record token lifetime decisions
  • Maintain incident response procedures
  • Document key rotation schedule
  • Keep security contact information current
Keep improving: Work through the checklist above until every practice is implemented, then keep monitoring and updating.