Files
gh-bbrowning-bbrowning-clau…/skills/auth-security/reference/jwt-security.md
2025-11-29 18:00:42 +08:00

383 lines
12 KiB
Markdown

# JWT Security Best Practices
This reference provides comprehensive guidance for reviewing JWT (JSON Web Token) implementation and usage in pull requests.
## Critical Security Principles
### 1. Never Forward JWTs to Unintended Services
**The Problem:**
A JWT contains an `aud` (audience) claim that specifies which service(s) the token is intended for. Forwarding a JWT to a service not listed in the audience claim creates serious security vulnerabilities.
**Security Impact:**
- **Confused Deputy Attack**: The receiving service cannot verify that the forwarding service is authorized to act on behalf of the user
- **Privilege Escalation**: If the receiving service accepts tokens not intended for it, attackers can misuse tokens from one service at another
- **ALBEAST-Class Vulnerability**: Tokens intended for one tenant/service can be used at another
**What to Look For in PRs:**
```python
# ❌ CRITICAL SECURITY ISSUE
def call_external_api(user_token):
# Forwarding user's JWT directly to third-party service
response = requests.get(
"https://third-party-api.com/resource",
headers={"Authorization": f"Bearer {user_token}"}
)
```
**Severity:** CRITICAL - Must be blocked before merge
### 2. Validate Audience Claims
Every service that accepts JWTs **MUST** validate the `aud` claim matches its own identifier.
**RFC Requirements:**
- Per RFC 7519: "Each principal intended to process the JWT MUST identify itself with a value in the audience claim"
- Per RFC 9068: "The resource server MUST validate that the aud claim contains a resource indicator value corresponding to an identifier the resource server expects for itself"
- **The JWT MUST be rejected if the audience does not match**
**What to Look For in PRs:**
```python
# ❌ MISSING VALIDATION
def verify_token(token):
decoded = jwt.decode(token, public_key, algorithms=['RS256'])
# Missing audience validation!
return decoded
# ✅ CORRECT VALIDATION
def verify_token(token):
decoded = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='https://api.myservice.com' # Validates aud claim
)
return decoded
```
**Severity:** CRITICAL if missing, HIGH if incomplete
### 3. Validate All Critical Claims
Beyond audience, validate:
**Required Validations:**
- `iss` (issuer): Verify the token came from a trusted authorization server
- `aud` (audience): Verify the token is intended for this service
- `exp` (expiration): Reject expired tokens
- `nbf` (not before): Reject tokens used before their valid time
- `alg` (algorithm): Prevent algorithm confusion attacks
**What to Look For in PRs:**
```python
# ❌ VULNERABLE TO ALGORITHM CONFUSION
def verify_token(token):
# Accepts ANY algorithm from the token header
decoded = jwt.decode(token, secret, algorithms=None)
# ✅ CORRECT - EXPLICIT ALGORITHM
def verify_token(token):
# Only accepts expected algorithm
decoded = jwt.decode(
token,
public_key,
algorithms=['RS256'], # Explicit, not from token
issuer='https://auth.example.com',
audience='https://api.example.com'
)
```
**Severity:** CRITICAL for algorithm validation, HIGH for other claims
### 4. Use Token Exchange for Service-to-Service Communication
When a service needs to call another service on behalf of a user, **use OAuth Token Exchange (RFC 8693)** rather than forwarding the original token.
**The Correct Pattern:**
```python
# ✅ SECURE TOKEN EXCHANGE PATTERN
def call_downstream_service(user_token):
# Exchange user token for service-specific token
exchange_response = requests.post(
'https://auth.example.com/token',
data={
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
'subject_token': user_token,
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
'resource': 'https://downstream-service.example.com',
'scope': 'read:data' # Downscoped to minimum needed
}
)
downstream_token = exchange_response.json()['access_token']
# Use the service-specific token
response = requests.get(
'https://downstream-service.example.com/api/resource',
headers={'Authorization': f'Bearer {downstream_token}'}
)
```
**Benefits of Token Exchange:**
1. **Correct Audience**: New token has proper `aud` claim for downstream service
2. **Least Privilege**: Token is downscoped to minimum permissions needed
3. **Audit Trail**: Clear chain of delegation (user → service A → service B)
4. **Prevents Confused Deputy**: Downstream service can validate the token was properly issued
**What to Look For in PRs:**
- Services forwarding user JWTs to other services without exchange
- Missing token exchange implementation where multi-service calls exist
- Tokens with overly broad scopes being passed between services
**Severity:** CRITICAL in security-sensitive contexts, HIGH otherwise
### 5. Apply Principle of Least Privilege
**Scope Downscoping:**
When exchanging tokens, request only the minimum scopes needed for the specific operation.
```python
# ❌ OVER-PRIVILEGED
exchange_data = {
'scope': 'read write admin delete' # Too many permissions!
}
# ✅ LEAST PRIVILEGE
exchange_data = {
'scope': 'read:invoices' # Only what's needed
}
```
**Severity:** MEDIUM to HIGH depending on the over-privileging
### 6. Secure Token Storage and Transmission
**Transport Security:**
- JWTs MUST be transmitted over HTTPS only
- Never log full JWTs (mask them if needed for debugging)
- Never include JWTs in URLs (query parameters, path segments)
**Storage Security:**
- Don't store JWTs in localStorage if they contain sensitive data (XSS risk)
- Consider httpOnly cookies for web applications
- Use secure, encrypted storage on mobile platforms
- Implement token refresh to minimize long-lived token exposure
**What to Look For in PRs:**
```javascript
// ❌ SECURITY ISSUES
localStorage.setItem('token', jwt); // XSS vulnerability
console.log('Token:', jwt); // Logging sensitive data
const url = `/api/resource?token=${jwt}`; // Token in URL
// ✅ BETTER PRACTICES
// Use httpOnly cookie or secure storage
document.cookie = `token=${jwt}; Secure; HttpOnly; SameSite=Strict`;
console.log('Token:', jwt.substring(0, 10) + '...'); // Masked logging
```
**Severity:** HIGH for URL exposure, MEDIUM for storage issues
## Algorithm Confusion Attacks
### The Vulnerability
JWT headers include an `alg` parameter that specifies the signing algorithm. If the verification code doesn't enforce the expected algorithm, attackers can:
1. Change RS256 (RSA) to HS256 (HMAC)
2. Use the RSA public key as the HMAC secret
3. Create validly-signed tokens
**What to Look For in PRs:**
```python
# ❌ VULNERABLE
def verify_token(token):
# Algorithm taken from token header - DANGEROUS!
header = jwt.get_unverified_header(token)
alg = header['alg']
decoded = jwt.decode(token, key, algorithms=[alg])
# ✅ SECURE
def verify_token(token):
# Algorithm explicitly specified and validated
decoded = jwt.decode(
token,
public_key,
algorithms=['RS256'] # Only RS256 accepted
)
```
**Severity:** CRITICAL
### None Algorithm Attack
Some JWT libraries accept `"alg": "none"` which bypasses signature verification entirely.
**What to Look For in PRs:**
```python
# ❌ VULNERABLE
jwt.decode(token, verify=False) # Dangerous!
jwt.decode(token, algorithms=['RS256', 'none']) # Allows none!
# ✅ SECURE
jwt.decode(token, public_key, algorithms=['RS256'])
```
**Severity:** CRITICAL
## Common JWT Anti-Patterns
### 1. Treating ID Tokens as Access Tokens
**Problem:** ID tokens (from OpenID Connect) are meant for the client that requested authentication, NOT for API authorization.
```python
# ❌ WRONG
# Forwarding ID token to API
api_call(headers={'Authorization': f'Bearer {id_token}'})
# ✅ CORRECT
# Use access token for API calls
api_call(headers={'Authorization': f'Bearer {access_token}'})
```
**Severity:** HIGH
### 2. Missing Signature Validation
**Problem:** Accepting unsigned JWTs or skipping validation.
```python
# ❌ VULNERABLE
payload = jwt.decode(token, options={"verify_signature": False})
# ✅ SECURE
payload = jwt.decode(token, public_key, algorithms=['RS256'])
```
**Severity:** CRITICAL
### 3. Trusting Token Content Without Validation
**Problem:** Extracting claims before validating signature and claims.
```python
# ❌ DANGEROUS
def get_user_id(token):
# Decodes without validation!
payload = jwt.decode(token, options={"verify_signature": False})
return payload['user_id']
# ✅ SAFE
def get_user_id(token):
# Validates first, then extracts
payload = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='https://api.example.com',
issuer='https://auth.example.com'
)
return payload['user_id']
```
**Severity:** CRITICAL
### 4. Using Weak Secrets for HS256
**Problem:** Using predictable or weak secrets for HMAC signing.
```python
# ❌ WEAK SECRET
secret = "secret123"
jwt.encode(payload, secret, algorithm='HS256')
# ✅ STRONG SECRET
# Use cryptographically random, high-entropy secret
# At least 256 bits (32 bytes) for HS256
import secrets
secret = secrets.token_bytes(32)
```
**Severity:** CRITICAL
### 5. Overly Broad Audiences
**Problem:** Using wildcard or overly generic audience values.
```python
# ❌ TOO BROAD
token_data = {
'aud': '*', # Accepts anywhere!
'aud': 'https://example.com' # Too generic
}
# ✅ SPECIFIC
token_data = {
'aud': 'https://api.example.com/v1/orders' # Specific service
}
```
**Severity:** MEDIUM to HIGH
## Review Checklist for JWT Code
When reviewing PRs involving JWT authentication/authorization:
### Critical Checks
- [ ] **Audience validation**: Every service validates `aud` claim matches its identifier
- [ ] **Algorithm enforcement**: Explicit algorithm list, no `none`, no user-controlled algorithm
- [ ] **Signature validation**: All tokens are cryptographically validated
- [ ] **Issuer validation**: `iss` claim validated against trusted issuers
- [ ] **No token forwarding**: Tokens are not forwarded to services not in their `aud` claim
### High Priority Checks
- [ ] **Token exchange**: Service-to-service calls use token exchange, not forwarding
- [ ] **Expiration validation**: `exp` claim checked and enforced
- [ ] **Scope downscoping**: Exchanged tokens request minimum necessary scopes
- [ ] **ID token misuse**: ID tokens not used for API authorization
- [ ] **Transport security**: Tokens only transmitted over HTTPS
### Medium Priority Checks
- [ ] **Least privilege**: Scopes are specific and minimal
- [ ] **Secure storage**: Tokens stored securely (not localStorage for sensitive data)
- [ ] **Token refresh**: Short-lived tokens with refresh mechanism
- [ ] **Logging safety**: Tokens not logged in full
- [ ] **Error messages**: Don't leak token information in errors
### Code Pattern Recognition
**Token Forwarding Pattern (Usually Wrong):**
```python
# Server receives user_token from client
# Server forwards user_token to another service
downstream_service.call(authorization=user_token) # ❌ Check this!
```
**Token Exchange Pattern (Usually Correct):**
```python
# Server receives user_token from client
# Server exchanges for service-specific token
new_token = auth_server.exchange_token(user_token, target_service)
# Server uses new token
downstream_service.call(authorization=new_token) # ✅ Good!
```
## Key RFCs and Standards
- **RFC 7519**: JSON Web Token (JWT) - Core standard
- **RFC 8693**: OAuth 2.0 Token Exchange - Service-to-service delegation
- **RFC 8707**: Resource Indicators for OAuth 2.0 - Explicit audience specification
- **RFC 9068**: JWT Profile for OAuth 2.0 Access Tokens - Access token best practices
- **RFC 9700**: OAuth 2.0 Security Best Current Practice (January 2025) - Latest security guidance
## When to Escalate
Escalate to security team or mark as CRITICAL if you find:
- Tokens forwarded to services not in their audience
- Missing signature validation
- Algorithm confusion vulnerabilities (accepting `none` or user-controlled algorithm)
- Tokens containing sensitive data logged or exposed in URLs
- No audience or issuer validation
- Token exchange not used in multi-service architectures