Authentication Errors
Authentication Errors
Section titled “Authentication Errors”Observable Symptoms
Section titled “Observable Symptoms”- HTTP 401 Unauthorized responses
- Error: “Authentication required” or “Invalid token”
- JWT validation failures
- Development headers not working
- Token signature verification errors
Quick Fix
Section titled “Quick Fix”# Check authentication modedocker logs api-gateway 2>&1 | grep "JWTAuthenticationEngine\|DEVELOPMENT_AUTH"
# Test with development headerscurl -H "X-Dev-User-Id: test-user" \ -H "X-Dev-Permissions: *" \ http://localhost:3000/api/endpoint
# Decode JWT to inspect claimsecho "YOUR_JWT_TOKEN" | cut -d. -f2 | base64 -d | jq
# Check JWT signature algorithmecho "YOUR_JWT_TOKEN" | cut -d. -f1 | base64 -d | jq '.alg'Authentication Modes
Section titled “Authentication Modes”The API Gateway supports three authentication modes:
- Development Mode - Bypass JWT with dev headers (local only)
- HS256 (Symmetric) - Shared secret JWT validation
- RS256 (Asymmetric) - JWKS public key validation
Common Causes (Ordered by Frequency)
Section titled “Common Causes (Ordered by Frequency)”1. Development Mode Not Enabled
Section titled “1. Development Mode Not Enabled”Frequency: Very Common (40% of cases)
Symptoms:
- Development headers ignored
- 401 error even with X-Dev-User-Id header
- Local development requires JWT
Diagnostic Steps:
# Check if development mode enableddocker logs api-gateway 2>&1 | grep "DEVELOPMENT_AUTH_ENABLED"
# Check environment variablesdocker compose exec api-gateway env | grep DEVELOPMENTSolution:
Enable development mode in docker-compose.yml:
services: api-gateway: environment: - DEVELOPMENT_AUTH_ENABLED=trueRestart API Gateway:
docker compose restart api-gatewayTest with development headers:
curl -H "X-Dev-User-Id: test-user-123" \ -H "X-Dev-Permissions: *" \ http://localhost:3000/api/endpointSecurity Warning: NEVER enable DEVELOPMENT_AUTH_ENABLED in production!
Prevention:
- Use environment-specific docker-compose files
- Document development setup requirements
- Add warning comments in docker-compose.yml
2. JWT Missing Required ‘sub’ Claim
Section titled “2. JWT Missing Required ‘sub’ Claim”Frequency: Very Common (25% of cases)
Symptoms:
- Error: “JWT missing required ‘sub’ claim”
- Valid JWT but authentication fails
- Token from OIDC provider but no user ID
Diagnostic Steps:
# Decode JWT payloadecho "YOUR_JWT_TOKEN" | cut -d. -f2 | base64 -d | jq
# Check for 'sub' claimecho "YOUR_JWT_TOKEN" | cut -d. -f2 | base64 -d | jq '.sub'Example Token:
// ❌ WRONG: No 'sub' claim{ "email": "user@example.com", "name": "John Doe", "permissions": ["users:read"] // Missing: "sub" field}
// ✓ CORRECT: Has 'sub' claim{ "sub": "auth0|123456789", // Required user identifier "email": "user@example.com", "name": "John Doe", "permissions": ["users:read"]}Solution:
Ensure your identity provider includes sub claim in JWT:
Auth0:
// Auth0 includes 'sub' by default// Typically: "sub": "auth0|user-id"Keycloak:
// Keycloak includes 'sub' by default// Typically: "sub": "uuid-format-user-id"Custom JWT issuer:
// Include 'sub' when generating tokenconst token = jwt.sign({ sub: userId, // REQUIRED email: user.email, permissions: user.permissions}, secret);Prevention:
- Validate JWT structure in auth service
- Use standard OIDC providers (Auth0, Keycloak)
- Test JWT structure before deployment
3. Algorithm Mismatch (HS256 vs RS256)
Section titled “3. Algorithm Mismatch (HS256 vs RS256)”Frequency: Common (20% of cases)
Symptoms:
- Error: “invalid algorithm”
- JWT validation fails
- Token from Auth0/Keycloak rejected
Diagnostic Steps:
# Check JWT header for algorithmecho "YOUR_JWT_TOKEN" | cut -d. -f1 | base64 -d | jq '.alg'
# Check API Gateway configurationdocker logs api-gateway 2>&1 | grep "Configured for.*mode"
# Logs will show:# "Configured for HS256 mode" (if JWT_SECRET set)# OR# "Configured for RS256 mode" (if JWKS_URI set)Algorithm Detection:
# JWT header shows algorithmecho "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." | cut -d. -f1 | base64 -d
# Output:# {"alg":"RS256","typ":"JWT"} ← Use JWKS_URI# OR# {"alg":"HS256","typ":"JWT"} ← Use JWT_SECRETSolution:
For RS256 (Auth0, Keycloak, Okta):
api-gateway: environment: - JWKS_URI=https://your-tenant.auth0.com/.well-known/jwks.json # Remove JWT_SECRET if presentFor HS256 (Custom JWT):
api-gateway: environment: - JWT_SECRET=your-secret-key-min-32-chars # Remove JWKS_URI if presentNever set both JWT_SECRET and JWKS_URI - gateway will prefer JWKS_URI.
Prevention:
- Use RS256 for production (more secure)
- Document authentication mode in README
- Match algorithm to provider
4. Invalid Token Signature
Section titled “4. Invalid Token Signature”Frequency: Common (10% of cases)
Symptoms:
- Error: “invalid signature”
- Token structure valid but signature fails
- Different secret between auth service and gateway
Diagnostic Steps:
# Check if JWT_SECRET matches between servicesdocker compose exec auth-service env | grep JWT_SECRETdocker compose exec api-gateway env | grep JWT_SECRET
# Should be identicalCommon Causes:
A. Secret Mismatch (HS256):
# ❌ WRONG: Different secretsauth-service: environment: - JWT_SECRET=secret-one
api-gateway: environment: - JWT_SECRET=secret-two # Different!
# ✓ CORRECT: Same secretauth-service: environment: - JWT_SECRET=shared-secret-key
api-gateway: environment: - JWT_SECRET=shared-secret-key # Must matchB. Wrong JWKS URI (RS256):
# ❌ WRONG: Typo in JWKS URIapi-gateway: environment: - JWKS_URI=https://tenant.auth0.com/.well-known/jwks.json # Missing 'us'
# ✓ CORRECT: Exact URI from providerapi-gateway: environment: - JWKS_URI=https://tenant.us.auth0.com/.well-known/jwks.jsonC. Token from Different Provider:
# Token issued by different Auth0 tenant# Gateway configured for tenant-a# Token from tenant-b
# Solution: Ensure gateway configured for correct providerSolution:
- For HS256: Ensure JWT_SECRET matches exactly between auth service and gateway
- For RS256: Verify JWKS_URI is correct
- Test JWKS endpoint:
# Verify JWKS accessiblecurl https://your-tenant.us.auth0.com/.well-known/jwks.json | jq
# Should return public keysPrevention:
- Use environment variables from shared .env file
- Rotate secrets securely
- Test with tokens from correct provider
5. Expired Token
Section titled “5. Expired Token”Frequency: Occasional (3% of cases)
Symptoms:
- Error: “jwt expired”
- Token worked before but now fails
- Intermittent authentication failures
Diagnostic Steps:
# Decode token and check expirationecho "YOUR_JWT_TOKEN" | cut -d. -f2 | base64 -d | jq '.exp'
# Compare to current Unix timestampdate +%s
# If exp < current timestamp, token is expiredExample:
// JWT payload{ "sub": "user-123", "iat": 1704067200, // Issued at: 2024-01-01 00:00:00 "exp": 1704070800 // Expires at: 2024-01-01 01:00:00}
// Current time: 1704074400 (2024-01-01 02:00:00)// Token expired 1 hour agoSolution:
- Refresh Token:
// Frontend: Refresh token before expirationconst token = localStorage.getItem('jwt');const payload = JSON.parse(atob(token.split('.')[1]));const expiresIn = payload.exp * 1000 - Date.now();
if (expiresIn < 5 * 60 * 1000) { // Less than 5 minutes await refreshToken();}- Increase Token Lifetime (Development Only):
// Auth service (development only)const token = jwt.sign({ sub: userId }, secret, { expiresIn: '24h' // Longer for development});- Production: Use refresh token pattern to get new access tokens
Prevention:
- Implement automatic token refresh
- Show expiration warning to user
- Use refresh tokens in production
6. Missing Permissions in JWT
Section titled “6. Missing Permissions in JWT”Frequency: Occasional (2% of cases)
Symptoms:
- Authentication succeeds (200 OK from auth endpoint)
- But 403 Forbidden on API calls
- User authenticated but not authorized
Note: This is actually an authorization issue, not authentication. Covered here because it often looks like auth failure.
Diagnostic Steps:
# Check permissions in JWTecho "YOUR_JWT_TOKEN" | cut -d. -f2 | base64 -d | jq '.permissions'
# Check required permissions for operationcurl http://localhost:3001/api/services/my-service/contracts | jq \ '.contracts[] | select(.name=="CreateUser") | .requiredPermissions'
# Compare: Does user have all required permissions?Permission Formats in JWT:
The gateway automatically detects permissions in multiple formats:
// Format 1: Direct permissions array (recommended){ "sub": "user-123", "permissions": ["users:create", "users:read"]}
// Format 2: Namespaced (Auth0 custom claims){ "sub": "auth0|user-123", "https://api.yourapp.com/permissions": ["users:create"]}
// Format 3: OAuth2 scope string{ "sub": "user-123", "scope": "users:create users:read" // Space-separated}Solution:
Ensure identity provider includes permissions in JWT:
Auth0:
// Add permissions to token in Auth0 Action/Ruleexports.onExecutePostLogin = async (event, api) => { const permissions = event.authorization?.permissions || []; api.accessToken.setCustomClaim('permissions', permissions);};Keycloak:
// Map roles to permissions in Keycloak client scope// Add "permissions" mapper to access tokenCustom Auth:
const token = jwt.sign({ sub: userId, permissions: user.permissions // Include user permissions}, secret);Prevention:
- Verify permissions included in token claims
- Test with actual tokens from auth provider
- Document required permissions per operation
Authentication Flow Debugging
Section titled “Authentication Flow Debugging”Development Mode Flow
Section titled “Development Mode Flow”1. Client → Gateway: X-Dev-User-Id + X-Dev-Permissions headers2. Gateway: Validates headers, creates auth context3. Gateway → Service: Message with authContext {userId, permissions}4. Service: Receives authContext from messageDebug:
# Check dev headers accepteddocker logs api-gateway 2>&1 | grep "Development auth"
# Should see: "Using development auth" in logsJWT Mode Flow (HS256)
Section titled “JWT Mode Flow (HS256)”1. Client → Gateway: Authorization: Bearer <jwt>2. Gateway: Decodes JWT header to get algorithm (HS256)3. Gateway: Validates signature with JWT_SECRET4. Gateway: Validates expiration, issuer (if configured)5. Gateway: Extracts userId (sub) and permissions6. Gateway → Service: Message with authContextDebug:
# Check HS256 mode activedocker logs api-gateway 2>&1 | grep "Configured for HS256"
# Check JWT validationdocker logs api-gateway 2>&1 | grep "JWT validation\|token\|signature"JWKS Mode Flow (RS256)
Section titled “JWKS Mode Flow (RS256)”1. Client → Gateway: Authorization: Bearer <jwt>2. Gateway: Decodes JWT header to get algorithm (RS256) and kid3. Gateway: Fetches public keys from JWKS_URI (cached 10 min)4. Gateway: Finds matching public key by kid5. Gateway: Validates signature with public key6. Gateway: Validates expiration, issuer, audience (if configured)7. Gateway: Extracts userId (sub) and permissions8. Gateway → Service: Message with authContextDebug:
# Check RS256 mode activedocker logs api-gateway 2>&1 | grep "Configured for RS256\|JWKS"
# Check JWKS fetchdocker logs api-gateway 2>&1 | grep "JWKS\|public key"
# Verify JWKS accessiblecurl https://your-tenant.us.auth0.com/.well-known/jwks.jsonConfiguration Examples
Section titled “Configuration Examples”Example 1: Development Mode Only (Local)
Section titled “Example 1: Development Mode Only (Local)”api-gateway: environment: - DEVELOPMENT_AUTH_ENABLED=trueUsage:
curl -H "X-Dev-User-Id: test-user" \ -H "X-Dev-Permissions: users:create,users:read" \ http://localhost:3000/api/create-userExample 2: HS256 with Shared Secret
Section titled “Example 2: HS256 with Shared Secret”api-gateway: environment: - JWT_SECRET=your-secret-key-min-32-characters-longToken Generation:
import jwt from 'jsonwebtoken';
const token = jwt.sign({ sub: 'user-123', permissions: ['users:create', 'users:read']}, 'your-secret-key-min-32-characters-long', { expiresIn: '1h'});Example 3: Auth0 with RS256
Section titled “Example 3: Auth0 with RS256”api-gateway: environment: - JWKS_URI=https://your-tenant.us.auth0.com/.well-known/jwks.json - JWT_ISSUER=https://your-tenant.us.auth0.com/ # Optional but recommended - JWT_AUDIENCE=https://api.yourapp.com # Optional but recommendedAuth0 Configuration:
- Create API in Auth0 dashboard
- Set API identifier as JWT_AUDIENCE
- Enable RBAC and include permissions in token
- Configure rules/actions to add permissions claim
Example 4: Keycloak with RS256
Section titled “Example 4: Keycloak with RS256”api-gateway: environment: - JWKS_URI=https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs - JWT_ISSUER=https://keycloak.example.com/realms/myrealm - JWT_AUDIENCE=my-client-idExample 5: Development + Production (Flexible)
Section titled “Example 5: Development + Production (Flexible)”api-gateway: environment: - DEVELOPMENT_AUTH_ENABLED=true # For quick testing with headers - JWKS_URI=https://your-tenant.us.auth0.com/.well-known/jwks.json # For real tokensWith this config, both development headers AND real JWTs work.
Testing Authentication
Section titled “Testing Authentication”Test Development Headers
Section titled “Test Development Headers”# Minimal testcurl -H "X-Dev-User-Id: test" \ -H "X-Dev-Permissions: *" \ http://localhost:3000/api/endpoint
# With specific permissionscurl -H "X-Dev-User-Id: user-123" \ -H "X-Dev-Permissions: users:create,users:read,users:update" \ http://localhost:3000/api/create-userTest HS256 JWT
Section titled “Test HS256 JWT”# Generate token (using jwt.io or library)TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Test with tokencurl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/endpointTest RS256 JWT (Auth0)
Section titled “Test RS256 JWT (Auth0)”# Get token from Auth0# Using Auth0 test token or authentication flow
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..."
# Test with tokencurl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/endpointDecode and Inspect JWT
Section titled “Decode and Inspect JWT”# Headerecho "$TOKEN" | cut -d. -f1 | base64 -d | jq
# Payloadecho "$TOKEN" | cut -d. -f2 | base64 -d | jq
# Check expirationecho "$TOKEN" | cut -d. -f2 | base64 -d | jq '.exp'date +%s # Compare to current timestampCommon Error Messages
Section titled “Common Error Messages””JWT missing required ‘sub’ claim”
Section titled “”JWT missing required ‘sub’ claim””Solution: Ensure token includes sub field with user ID
”invalid algorithm”
Section titled “”invalid algorithm””Solution: Match JWT algorithm (HS256/RS256) with gateway configuration
”invalid signature”
Section titled “”invalid signature””Solution: Verify JWT_SECRET matches or JWKS_URI is correct
”jwt expired”
Section titled “”jwt expired””Solution: Refresh token or increase expiration time (dev only)
“JWKS client not configured”
Section titled ““JWKS client not configured””Solution: Set either JWT_SECRET or JWKS_URI in gateway environment
Verification Steps
Section titled “Verification Steps”After fixing authentication:
1. Authentication Succeeds
Section titled “1. Authentication Succeeds”curl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/endpoint
# Should return 200 OK (not 401)2. User ID Extracted
Section titled “2. User ID Extracted”# Check logs show user IDdocker logs api-gateway 2>&1 | tail -20
# Should see: "Authenticated user: user-123"3. Permissions Available
Section titled “3. Permissions Available”# Request that requires permissions should workcurl -H "Authorization: Bearer $TOKEN" \ http://localhost:3000/api/create-user
# Should return 200 or 403 (if permissions wrong)# NOT 401 (authentication works)Related Documentation
Section titled “Related Documentation”- Authentication Concepts - Authentication architecture
- API Gateway Issues - Gateway-specific troubleshooting
- API Calls Failing - General API troubleshooting
- Error Catalog - Error reference
Summary
Section titled “Summary”Most authentication errors are caused by:
- Development mode not enabled - Set
DEVELOPMENT_AUTH_ENABLED=truelocally - Missing
subclaim - Ensure JWT includes user ID insubfield - Algorithm mismatch - Match HS256/RS256 with gateway config
- Invalid signature - Verify JWT_SECRET or JWKS_URI correct
- Expired token - Refresh token or increase expiration
Always decode JWT to inspect structure, claims, and expiration before troubleshooting.