# 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