JSON Web Tokens (JWTs) are the dominant authentication mechanism for modern APIs — and the source of more security incidents than any other auth pattern. This guide covers the production-grade practices for issuing, storing, transmitting, and revoking JWTs, with specific guidance for 2026 threat models including GPU-accelerated brute force and supply-chain attacks on JWT libraries.
What Is a JWT?
A JWT is a compact, URL-safe token format for representing claims (statements about a user or system) between two parties. The token has three Base64-encoded parts separated by dots: header.payload.signature.
- Header — specifies the token type (JWT) and signing algorithm (HS256, RS256, ES256, etc.).
- Payload — contains claims like
sub(subject/user ID),exp(expiration),iat(issued at), and custom claims likeroleorpermissions. - Signature — cryptographic signature over the header and payload, using the algorithm specified in the header and a secret or private key.
The signature is what makes JWTs trustworthy: anyone can create a JWT with any payload (they're just Base64-encoded JSON), but only someone with the secret key can create a valid signature. The verifier checks the signature against the secret to confirm the token hasn't been tampered with.
Important: JWT payloads are not encrypted. They're Base64-encoded, which is a reversible transformation. Anyone with the token can read the payload by pasting it into a decoder. For inspecting JWTs locally without sending them anywhere, use our browser-based JWT Decoder.
Signing Algorithms — HS256 vs RS256 vs ES256
The signing algorithm determines how the signature is created and verified. Three algorithms dominate production JWT deployments:
HS256 (HMAC with SHA-256)
Symmetric algorithm — the same secret is used to sign and verify. Simple to implement, supported by every JWT library, fast. The catch: every service that verifies tokens needs the secret, which expands the attack surface. If any one service leaks the secret, an attacker can forge tokens accepted by all services.
Use HS256 for: single-server apps, small projects where one team owns all services, internal tools where the secret distribution problem is manageable.
RS256 (RSA Signature with SHA-256)
Asymmetric algorithm — a private key signs, a public key verifies. Only the auth service holds the private key. Every other service gets the public key, which can verify tokens but cannot forge them. This is the right choice for multi-service architectures.
Use RS256 for: microservices, any architecture where the verifying service is different from the signing service, production deployments where key compromise in one service shouldn't compromise the whole system.
ES256 (ECDSA with P-256 and SHA-256)
Asymmetric algorithm using elliptic curve cryptography. Same security properties as RS256 but with much shorter keys (256-bit ECDSA is equivalent to 3072-bit RSA) and faster signing/verification. Modern choice when you control the entire stack and all libraries support it.
Token Expiration — Access vs Refresh Tokens
The standard pattern is two-token authentication: short-lived access tokens paired with long-lived refresh tokens.
Access tokens are sent with every API request in the Authorization: Bearer header. They should be short-lived: 15 minutes to 1 hour maximum. If stolen, the attacker has access only until the token expires. Never store access tokens in localStorage or sessionStorage — use httpOnly cookies or in-memory storage.
Refresh tokens are sent only to a dedicated /refresh endpoint to obtain new access tokens. They live longer: 7-30 days typical, up to 90 days for low-security contexts. Refresh tokens should be:
- Stored server-side with a mapping to the user, so they can be revoked.
- Rotated on every use — each refresh issues a new refresh token and invalidates the old one.
- Bound to a device or IP range, so a stolen token from a different location fails.
Common JWT Vulnerabilities (and How to Avoid Them)
1. The alg: none Attack
Early JWT library vulnerabilities allowed attackers to set the algorithm in the header to "none", which told the library to skip signature verification. Modern libraries reject this, but the attack persists in outdated libraries. Defense: pin the expected algorithm in your verification code — jwt.verify(token, key, { algorithms: ['RS256'] }) — and reject tokens with any other algorithm.
2. Algorithm Confusion (RS256 to HS256)
If a verifier accepts both HS256 and RS256, an attacker with the public key (which is, by definition, public) can sign a malicious token using HS256 with the public key as the HMAC secret. The verifier accepts it because the signature is valid HMAC over the payload. Defense: explicitly specify the allowed algorithm in verification, never use the algorithm from the token header.
3. Weak Signing Secrets
For HS256, the signing secret must be high-entropy — at least 256 bits (32 bytes) of cryptographic randomness. Common mistakes: using a password, using a short secret like "secret123", reusing secrets across environments. Defense: generate secrets with a CSPRNG, store them in a secrets manager (Vault, AWS Secrets Manager, Doppler), rotate quarterly.
4. Storing Tokens in localStorage
localStorage is accessible to any JavaScript running on the page, including third-party scripts and XSS payloads. A single XSS vulnerability in your app leaks all access tokens. Defense: store access tokens in httpOnly, Secure, SameSite=Strict cookies. Store refresh tokens in httpOnly cookies with a Path restriction to the /refresh endpoint only.
5. Not Validating the iss and aud Claims
A token issued for service A might be validly signed but should not be accepted by service B. Always validate the iss (issuer) and aud (audience) claims to ensure the token was issued for your service specifically.
Token Revocation — The Stateless Problem
JWTs are designed to be stateless — the verifier doesn't need to check a database, just verify the signature and expiration. This is great for performance but creates a revocation problem: if a token is stolen, you can't invalidate it before expiry without keeping state.
Common revocation patterns:
- Short access token lifetimes (15 min). If a token is stolen, the damage window is small. No revocation needed.
- Token blocklist in Redis. Store revoked token IDs (the
jticlaim) in Redis with TTL matching the token's remaining lifetime. Verifier checks Redis before accepting. Adds ~1ms latency. - Refresh token rotation. Each refresh invalidates the previous refresh token. Stolen refresh tokens are useless once the legitimate user refreshes.
- Signing key rotation. Rotate the signing key periodically (monthly). Tokens signed with the old key stop being accepted. Nuclear option — invalidates all sessions.
Inspecting JWTs Safely
When debugging authentication issues, you often need to inspect a JWT's payload to see what claims were issued. The danger: pasting a JWT into an online decoder sends the token to a third-party server. If the token is still valid, that server could use it to authenticate as the user.
Safe alternatives:
- Decode locally with
echo "eyJ..." | cut -d. -f2 | base64 -don the command line. - Use a browser-based decoder that processes the token locally — like our JWT Decoder which never transmits tokens to any server.
- For high-security tokens (production admin tokens), use only air-gapped tools.
Our JWT Decoder is designed for safe debugging: paste the token, see the decoded header and payload, check expiration timing — all in your browser, nothing transmitted. Use it for any token you wouldn't want to share.
Production JWT Checklist
- Use RS256 or ES256, not HS256, for multi-service deployments.
- Access tokens: 15 min to 1 hour max lifetime.
- Refresh tokens: 7-30 days, stored server-side, rotated on every use.
- Store tokens in httpOnly cookies, never localStorage.
- Pin the expected algorithm in verification — reject tokens with any other algorithm.
- Validate
iss,aud,exp, andnbfclaims. - Use 256-bit+ signing secrets generated by a CSPRNG.
- Implement token revocation via Redis blocklist or short lifetimes.
- Never put sensitive data (PII, passwords, payment info) in the JWT payload — it's not encrypted.
- Rotate signing keys quarterly. Build key rotation into your auth service from day one.
For inspecting JWTs safely during development, use our JWT Decoder — local-only, no transmission, with clear security warnings about not pasting production tokens into any online tool.