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

12 KiB

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:

# ❌ 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:

# ❌ 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:

# ❌ 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:

# ✅ 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.

# ❌ 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:

// ❌ 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:

# ❌ 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:

# ❌ 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.

# ❌ 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.

# ❌ 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.

# ❌ 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.

# ❌ 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.

# ❌ 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):

# 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):

# 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