key, reducing the attack surface.
Recommendation:
- Use
ES256 for performance and smaller token size.
- Use
RS256 for broader compatibility requirements.
- Never accept
alg: none. Never accept HS256 if expecting RS256.
2. Key Management with JWKS
Implement a JSON Web Key Set (JWKS) endpoint. This allows clients to fetch public keys dynamically. Support key rotation by maintaining multiple active keys during the transition period.
Architecture:
- Signing Service: Holds private keys. Rotates keys periodically (e.g., every 30 days).
- Verification Services: Fetch JWKS from a shared endpoint. Cache keys with a TTL.
- Rotation: Generate new key pair. Publish new public key in JWKS. Sign new tokens with new key. Verify tokens using all active public keys. Retire old private key.
3. Token Structure and Claims
Enforce strict claim validation. The payload must contain mandatory claims to prevent replay and scope violations.
Mandatory Claims:
exp: Expiration time. Keep access tokens short-lived (5-15 minutes).
nbf: Not before. Prevent premature use.
iss: Issuer. Validate against expected authority.
aud: Audience. Validate against expected service identifier.
jti: JWT ID. Unique identifier for replay protection and blacklisting.
sub: Subject. User or service identifier.
Avoid: Storing sensitive data (passwords, PII) in the payload. JWTs are Base64URL encoded, not encrypted. Use JWE if encryption is required.
4. Secure Transport and Storage
- Transmission: Enforce HTTPS exclusively.
- Storage: Use
HttpOnly, Secure, SameSite=Strict cookies for browser-based apps. This mitigates XSS token theft.
- Mobile/SPA: If cookies are not feasible, store tokens in memory. Avoid
LocalStorage and SessionStorage due to XSS vulnerability. Implement strict Content Security Policy (CSP).
5. Revocation Strategy
Stateless tokens cannot be revoked natively. Implement a hybrid approach:
- Short TTL: Limit the window of exposure.
- Refresh Tokens: Use long-lived refresh tokens stored securely in a database. Revoke refresh tokens to invalidate access.
- Token Blacklist: For immediate revocation, maintain a distributed cache (Redis) of revoked
jti values. Check blacklist during validation.
6. TypeScript Implementation
Use the jose library for robust, standards-compliant JWT handling.
// jwt.config.ts
import { createRemoteJWKSet, jwtVerify, decodeJwt, SignJWT } from 'jose';
import { randomUUID } from 'crypto';
const JWKS_URI = process.env.JWKS_URI || 'http://localhost:8080/.well-known/jwks.json';
const ISSUER = process.env.JWT_ISSUER || 'auth-service';
const AUDIENCE = process.env.JWT_AUDIENCE || 'api-gateway';
// Remote JWKS set for verification
const JWKS = createRemoteJWKSet(new URL(JWKS_URI), {
cooldownDuration: 60000, // Cache keys for 60s
timeoutDuration: 5000,
});
export const validateToken = async (token: string) => {
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['ES256', 'RS256'], // Explicitly allowed algorithms
clockTolerance: 10, // 10s skew tolerance
});
// Check blacklist for immediate revocation
const isRevoked = await checkTokenBlacklist(payload.jti as string);
if (isRevoked) {
throw new Error('Token has been revoked');
}
return payload;
} catch (error) {
if (error instanceof Error && error.message.includes('invalid algorithm')) {
// Log security event: algorithm confusion attempt
console.warn('Security Alert: Invalid algorithm attempt detected');
}
throw error;
}
};
export const createToken = async (
payload: Record<string, unknown>,
privateKey: CryptoKey,
kid: string
) => {
const jwt = await new SignJWT({ ...payload })
.setProtectedHeader({ alg: 'ES256', kid })
.setIssuer(ISSUER)
.setAudience(AUDIENCE)
.setIssuedAt()
.setNotBefore()
.setExpirationTime('15m') // Short-lived access token
.setJti(randomUUID())
.sign(privateKey);
return jwt;
};
// Mock blacklist check
async function checkTokenBlacklist(jti: string): Promise<boolean> {
// Implement Redis/Distributed cache lookup
return false;
}
Pitfall Guide
1. Algorithm Confusion Attacks
Mistake: Accepting any algorithm specified in the header or allowing alg: none. Attackers can forge tokens by switching to none or using the public key with HS256.
Best Practice: Explicitly configure the allowed algorithms in the verification library. Never trust the header algorithm blindly. Reject tokens with unexpected algorithms immediately.
2. Storing Sensitive Data in Payload
Mistake: Assuming JWTs are encrypted. The payload is readable by anyone who intercepts the token.
Best Practice: Treat JWT payload as public data. Store only non-sensitive identifiers and claims. If sensitive data must be transmitted, use JWE (JSON Web Encryption) or encrypt the data before embedding.
3. Long-Lived Access Tokens
Mistake: Setting exp to days or weeks to reduce login frequency.
Best Practice: Access tokens should be short-lived (5-15 minutes). Use refresh tokens for maintaining sessions. This limits the window for token misuse and reduces the impact of theft.
4. LocalStorage Storage
Mistake: Storing JWTs in localStorage for convenience in SPAs.
Best Practice: localStorage is accessible to all JavaScript on the domain. Any XSS vulnerability leads to total account compromise. Use HttpOnly cookies whenever possible. If cookies are impossible, store tokens in memory and implement rigorous XSS protections.
5. Missing aud and iss Validation
Mistake: Not validating audience and issuer claims.
Best Practice: A token issued for Service A should not be accepted by Service B. Validate iss to ensure the token comes from the trusted authority and aud to ensure the token is intended for the receiving service. This prevents cross-service token replay attacks.
6. Weak Key Management
Mistake: Hardcoding secrets or using weak key lengths.
Best Practice: Use strong cryptographic keys (e.g., P-256 for EC, RSA-2048+). Implement automated key rotation. Never commit keys to source control. Use environment variables or secret management services (Vault, AWS Secrets Manager).
7. Ignoring Clock Skew
Mistake: Strict validation of exp and nbf without tolerance.
Best Practice: Distributed systems have clock drift. Configure a reasonable clockTolerance (e.g., 10-30 seconds) to prevent valid tokens from being rejected due to minor time discrepancies.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Microservices Mesh | RS256/ES256 + JWKS | Decouples signing from verification; enables independent service scaling. | Low |
| Single Monolith | HS256 with rotating secret | Simpler implementation; performance overhead of asymmetry is unnecessary. | Lowest |
| High-Security Domain | JWE + Short TTL + Blacklist | Encryption prevents payload inspection; strict lifecycle controls minimize risk. | Medium |
| Mobile Native App | PKCE + OAuth2 + Access/Refresh | Leverages OS security; avoids token storage risks in app sandbox. | High |
| Legacy Integration | HS256 + Strict Validation | Compatibility with legacy systems; must enforce strict algorithm validation. | Low |
Configuration Template
// security-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { validateToken } from './jwt.config';
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
// Extract token from HttpOnly cookie or Authorization header
const token = req.cookies?.access_token || req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Missing authentication token' });
}
// Validate token structure, signature, and claims
const payload = await validateToken(token);
// Attach payload to request for downstream handlers
req.user = payload;
next();
} catch (error) {
// Do not leak internal error details
console.error('Auth middleware error:', error.message);
return res.status(401).json({ error: 'Invalid or expired token' });
}
};
// JWKS Endpoint Configuration (Signing Service)
import { exportJWK, generateKeyPair } from 'jose';
import express from 'express';
const app = express();
let keys: Map<string, CryptoKey> = new Map();
app.get('/.well-known/jwks.json', async (req, res) => {
const publicKeys = [];
for (const [kid, privateKey] of keys) {
const publicKey = await crypto.subtle.exportKey('spki', privateKey);
const jwk = await exportJWK(publicKey);
jwk.kid = kid;
jwk.use = 'sig';
publicKeys.push(jwk);
}
res.json({ keys: publicKeys });
});
// Key Rotation Logic
async function rotateKeys() {
const { publicKey, privateKey } = await generateKeyPair('ES256');
const kid = randomUUID();
keys.set(kid, privateKey);
// Schedule removal of old keys after max token TTL + buffer
setTimeout(() => keys.delete(kid), 24 * 60 * 60 * 1000);
}
setInterval(rotateKeys, 30 * 24 * 60 * 60 * 1000); // Rotate every 30 days
Quick Start Guide
-
Initialize Project:
mkdir jwt-secure-app && cd jwt-secure-app
npm init -y
npm install express jose cookie-parser
npm install -D typescript @types/node @types/express
npx tsc --init
-
Generate Keys:
Create a script gen-keys.ts to generate an ES256 key pair and export the public key as JWK. Store the private key securely.
import { generateKeyPair, exportJWK } from 'jose';
const { publicKey, privateKey } = await generateKeyPair('ES256');
console.log('Private Key:', await exportJWK(privateKey));
console.log('Public Key:', await exportJWK(publicKey));
-
Create Token Factory:
Implement createToken using the private key. Ensure exp is set to 15 minutes and jti is generated.
-
Deploy Verification Service:
Implement validateToken using createRemoteJWKSet. Configure allowed algorithms and claim validation. Wrap this in an Express middleware.
-
Test Security:
Run the server and attempt to:
- Access a protected route without a token.
- Send a token with
alg: none.
- Send a token with an expired
exp.
- Send a token with a mismatched
aud.
Verify that all invalid requests are rejected with appropriate status codes.
By adhering to these practices, teams can leverage the scalability of JWTs while maintaining a robust security posture that mitigates the most prevalent threats in production environments. Security is not a feature of the token format; it is a result of rigorous implementation and lifecycle management.