Authentication & Authorization
The most critical security function of a gateway. Verify identity once at the boundary — downstream services receive verified identity and trust the gateway.
Table of Contents
Authentication at the Gateway
The gateway is the authentication boundary. Every request entering your system passes through it, making it the natural place to verify identity. Once the gateway authenticates a request, it forwards verified identity information (user ID, roles, scopes) to downstream services via headers. Backend services trust the gateway — they don't re-verify tokens.
Airport Security Checkpoint
The gateway is like airport security. You verify identity and boarding pass once at the checkpoint. After that, you move freely between gates, shops, and lounges without showing ID again. Each gate (service) trusts that security already verified you. They just check your boarding pass (forwarded identity header) matches the flight (authorized resource).
| Auth Method | Best For | Statefulness |
|---|---|---|
| API Keys | Server-to-server, third-party integrations | Stateful (key lookup) |
| JWT (Bearer Token) | User sessions, mobile/SPA clients | Stateless (signature verification) |
| OAuth 2.0 + OIDC | Delegated access, SSO, third-party apps | Stateful (token introspection) or stateless (JWT) |
| mTLS | Service-to-service, high-security B2B | Stateless (certificate validation) |
| HMAC Signing | Webhook verification, tamper-proof requests | Stateless (signature computation) |
The Downstream Trust Model
Once the gateway verifies a token, it strips the original Authorization header and injects internal headers: X-User-ID,X-User-Roles, X-Tenant-ID. Backend services trust these headers implicitly. This means backend services MUST NOT be directly accessible from outside — only through the gateway. If a service is exposed directly, anyone can forge these headers.
API Keys
API keys are opaque tokens that identify the calling application (not the user). They're the simplest auth mechanism — the client sends a key, the gateway looks it up in a store, and retrieves associated metadata (rate limits, permissions, tenant).
| Aspect | Details |
|---|---|
| Format | Random string (32-64 chars), often prefixed: sk_live_abc123 |
| Transmission | Header (X-API-Key or Authorization) — never in URL query params |
| Storage | Hash the key (SHA-256), store hash + metadata. Never store plaintext. |
| Metadata | Owner, created_at, rate_limit_tier, scopes, last_used_at |
| Rotation | Support multiple active keys per client for zero-downtime rotation |
plugins: - name: key-auth config: key_names: - X-API-Key - apikey hide_credentials: true # Strip key before forwarding key_in_header: true key_in_query: false # Never accept keys in URL key_in_body: false # Consumer with key consumers: - username: partner-acme keyauth_credentials: - key: sk_live_a1b2c3d4e5f6 # In practice, auto-generated
API Key Best Practices
- ✅Prefix keys with environment: sk_live_, sk_test_ — prevents accidental cross-env usage
- ✅Hash keys at rest — if your key store is breached, hashed keys are useless to attackers
- ✅Track last_used_at — identify and revoke unused keys
- ✅Support multiple active keys per consumer — enables zero-downtime rotation
- ✅Set expiration dates — keys without expiry accumulate as security debt
JWT Validation
JWT (JSON Web Token) validation is stateless — the gateway verifies the token's signature and claims without calling an external service. This makes it fast but means revocation requires additional mechanisms.
| Algorithm | Type | Key Management | Use Case |
|---|---|---|---|
| RS256 | Asymmetric (RSA) | Auth server has private key, gateway has public key | Most common — gateway only needs public key |
| ES256 | Asymmetric (ECDSA) | Smaller keys, faster verification | Modern alternative to RS256 |
| HS256 | Symmetric (HMAC) | Same secret on auth server and gateway | Simple but secret must be shared — avoid in distributed systems |
plugins: - name: jwt config: # Fetch public keys from JWKS endpoint uri_param_names: [] header_names: - Authorization claims_to_verify: - exp # Token not expired - nbf # Token not used before valid time key_claim_name: iss maximum_expiration: 3600 # Reject tokens valid > 1 hour # Gateway validation steps: # 1. Extract token from Authorization: Bearer <token> # 2. Decode header → get kid (key ID) and alg # 3. Fetch public key from JWKS endpoint (cached) # 4. Verify signature using public key # 5. Check exp, nbf, iss, aud claims # 6. Extract user claims → forward as headers
JWKS Endpoint and Key Rotation
The JWKS (JSON Web Key Set) endpoint publishes the auth server's current public keys. The gateway caches these keys and refreshes periodically. When the auth server rotates keys, it publishes both old and new keys in JWKS temporarily — allowing tokens signed with the old key to still validate during the transition. The kid (key ID) in the JWT header tells the gateway which key to use.
JWT Pitfalls
- ❌Accepting alg: none — always validate the algorithm matches your expected algorithm
- ❌Not checking exp — expired tokens should be rejected immediately
- ❌Using HS256 in distributed systems — the shared secret becomes a liability
- ❌Storing sensitive data in JWT payload — it's base64-encoded, not encrypted
- ❌Long-lived tokens without refresh — if compromised, they're valid until expiry
OAuth 2.0 & OIDC
OAuth 2.0 is a delegation framework — it lets users grant limited access to their resources without sharing credentials. OIDC (OpenID Connect) adds an identity layer on top, providing user authentication. The gateway's role is to validate access tokens and enforce scopes.
| Flow | Client Type | Gateway Role |
|---|---|---|
| Authorization Code | Web apps with backend | Validate access token, check scopes |
| Authorization Code + PKCE | SPAs, mobile apps | Same — PKCE prevents code interception |
| Client Credentials | Machine-to-machine | Validate token, identify calling service |
| Token Introspection | Opaque tokens | Call auth server to validate (cached) |
{ "active": true, "client_id": "partner-app-123", "username": "alice@example.com", "scope": "read:orders write:orders", "sub": "user-uuid-abc-123", "aud": "https://api.example.com", "iss": "https://auth.example.com", "exp": 1709251200, "iat": 1709247600, "token_type": "Bearer" }
Token Introspection Caching
Calling the auth server for every request defeats the purpose of a gateway. Cache introspection results with a short TTL (30–60 seconds). This means revoked tokens remain valid for up to TTL duration — an acceptable trade-off for most systems. For immediate revocation, use short-lived JWTs with refresh tokens.
Scope Validation at the Gateway
The gateway can enforce coarse-grained scope checks: does this token haveread:orders scope for a GET /orders request? This rejects obviously unauthorized requests before they reach the service. Fine-grained checks (can this user read THIS specific order?) stay in the service.
mTLS & HMAC Signing
Mutual TLS (mTLS)
In standard TLS, only the server proves its identity. In mTLS, both sides present certificates. The gateway verifies the client's certificate against a trusted CA, establishing cryptographic identity without tokens or keys.
| Aspect | Standard TLS | Mutual TLS |
|---|---|---|
| Server identity | ✅ Server presents cert | ✅ Server presents cert |
| Client identity | ❌ Not verified at TLS layer | ✅ Client presents cert |
| Use case | Public APIs, browser clients | B2B integrations, service-to-service |
| Certificate management | Server certs only | Server + client certs (more complex) |
| Revocation | CRL or OCSP for server | CRL or OCSP for both sides |
server { listen 443 ssl; # Server certificate (standard) ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; # Client certificate verification (mTLS) ssl_client_certificate /etc/ssl/trusted-client-ca.crt; ssl_verify_client on; ssl_verify_depth 2; # Forward client identity to backend location / { proxy_pass http://backend; proxy_set_header X-Client-CN $ssl_client_s_dn_cn; proxy_set_header X-Client-Serial $ssl_client_serial; proxy_set_header X-Client-Verify $ssl_client_verify; } }
HMAC Request Signing
HMAC signing proves that a request hasn't been tampered with and comes from a known sender. The client computes a signature over the request (method + path + timestamp + body hash) using a shared secret. The gateway recomputes and compares.
# Client computes signature TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") STRING_TO_SIGN="POST\n/api/webhooks\n${TIMESTAMP}\nsha256=<body-hash>" SIGNATURE=$(echo -n "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$SECRET_KEY" -binary | base64) # Client sends request with signature headers curl -X POST https://api.example.com/api/webhooks \ -H "X-Timestamp: ${TIMESTAMP}" \ -H "X-Signature: hmac-sha256=${SIGNATURE}" \ -H "Content-Type: application/json" \ -d '{"event": "payment.completed"}' # Gateway validates: # 1. Timestamp within 5-minute window (replay prevention) # 2. Recompute signature with stored secret # 3. Compare signatures (constant-time comparison)
Replay Prevention
HMAC signatures alone don't prevent replay attacks — an attacker can resend a captured request with its valid signature. Include a timestamp in the signed string and reject requests older than 5 minutes. For stronger protection, add a nonce (unique per request) and track seen nonces within the time window.
Authorization at the Gateway
Authorization at the gateway is coarse-grained — it answers "can this identity access this endpoint?" not "can this user access this specific resource?" Fine-grained authorization (resource-level) stays in the service that owns the data.
| Level | Where | Example |
|---|---|---|
| Coarse-grained (gateway) | API Gateway | Admin role required for /admin/* endpoints |
| Scope-based (gateway) | API Gateway | Token must have write:orders scope for POST /orders |
| Resource-level (service) | Backend service | User can only view their own orders |
| Field-level (service) | Backend service | Only managers see salary field |
# Open Policy Agent (OPA) integration with gateway # Policy: /admin/* requires admin role package gateway.authz default allow = false # Allow if user has required role for the path allow { required_role := role_for_path(input.path) required_role == input.user_roles[_] } # Path-to-role mapping role_for_path(path) = "admin" { startswith(path, "/admin/") } role_for_path(path) = "user" { startswith(path, "/api/") } # Allow health checks without auth allow { input.path == "/health" }
Gateway Authorization Patterns
- ✅Role-based route protection — /admin/* requires admin role
- ✅Scope enforcement — POST endpoints require write:resource scope
- ✅IP-based restrictions — management APIs only from internal IPs
- ✅Time-based access — maintenance endpoints only during business hours
- ✅OPA integration — externalize policy decisions for complex rules
The Auth Boundary
The auth boundary defines what the gateway handles vs what services handle. Getting this wrong leads to either an overloaded gateway or duplicated auth logic across services.
| Responsibility | Gateway | Service |
|---|---|---|
| Token validation | ✅ Verify signature, expiry, issuer | ❌ Trust gateway headers |
| Identity extraction | ✅ Extract user ID, roles, scopes | ❌ Read from X-User-ID header |
| Coarse authorization | ✅ Role/scope check for endpoint | ❌ Already filtered |
| Fine-grained authorization | ❌ No domain knowledge | ✅ User owns this resource? |
| Rate limiting | ✅ Per-key, per-user limits | ⚠️ Service-specific limits if needed |
| Token refresh | ❌ Not gateway's job | ❌ Client responsibility |
Identity Propagation Headers
# Headers the gateway injects after successful auth # Backend services trust these implicitly X-User-ID: "usr_abc123" # Authenticated user identifier X-User-Email: "alice@example.com" # User email (if available) X-User-Roles: "admin,editor" # Comma-separated roles X-Tenant-ID: "tenant_xyz" # Multi-tenant isolation X-Auth-Method: "jwt" # How the user authenticated X-Request-ID: "req_uuid_here" # Correlation ID for tracing X-Client-ID: "app_partner_acme" # Which application made the call # CRITICAL: Gateway MUST strip these headers from incoming requests # to prevent clients from forging identity
Never Trust Client-Supplied Identity Headers
The gateway must strip any incoming X-User-ID, X-Tenant-ID, or similar headers before processing. If a client can send X-User-ID: admin and the gateway forwards it without overwriting, you have a privilege escalation vulnerability. The gateway is the only source of truth for identity headers.
Interview Questions
Q:Why validate authentication at the gateway instead of in each service?
A: Three reasons: (1) Single enforcement point — one place to update auth logic, not N services. (2) Fail fast — reject unauthenticated requests before they consume backend resources. (3) Separation of concerns — services focus on business logic, not token parsing. The trade-off: the gateway becomes a critical security component that must be hardened and never bypassed.
Q:How do you handle JWT revocation if JWTs are stateless?
A: JWTs can't be truly revoked since they're self-contained. Strategies: (1) Short expiry (5-15 min) + refresh tokens — limits damage window. (2) Token blacklist in Redis — gateway checks blacklist on each request (adds latency). (3) Token versioning — increment user's token_version on revocation, reject tokens with old version. (4) For critical actions, use token introspection (call auth server). Most systems combine short-lived JWTs with refresh token rotation.
Q:What's the difference between authentication and authorization at the gateway?
A: Authentication answers 'who are you?' — verifying the token/key is valid and extracting identity. Authorization answers 'are you allowed?' — checking if that identity can access this endpoint. The gateway handles coarse-grained authorization (role-based route access, scope validation). Fine-grained authorization (can this user access THIS specific order?) requires domain knowledge and stays in the service.
Q:When would you choose mTLS over JWT for service-to-service auth?
A: mTLS when: (1) You need transport-layer identity — the identity is the certificate, not a token that could be stolen. (2) Zero-trust network — every connection is authenticated at the TLS layer. (3) B2B integrations where partners have their own PKI. JWT when: (1) You need to propagate user context (claims). (2) Services are behind a trusted gateway. (3) You want stateless verification without certificate management complexity.
Q:How do you prevent identity header forgery in a gateway architecture?
A: The gateway MUST: (1) Strip all identity headers (X-User-ID, X-Tenant-ID) from incoming requests before auth processing. (2) Only inject these headers after successful authentication. (3) Backend services must be network-isolated — only reachable through the gateway. If a service is directly accessible, anyone can send forged headers. Network policies (Kubernetes NetworkPolicy, security groups) enforce this isolation.
Common Mistakes
Not stripping identity headers from incoming requests
The gateway forwards X-User-ID from the client request without overwriting it after auth, allowing clients to impersonate any user.
✅Always strip identity headers (X-User-ID, X-Tenant-ID, X-User-Roles) from incoming requests BEFORE auth processing. Only inject them after successful authentication. This is a critical security requirement.
Accepting alg: none in JWT validation
The JWT library accepts tokens with algorithm 'none', allowing attackers to forge tokens without a signature.
✅Explicitly configure accepted algorithms (RS256, ES256). Never allow 'none'. Most modern JWT libraries reject it by default, but always verify your configuration. Test with a forged token.
Storing API keys in plaintext
Storing raw API keys in the database — a breach exposes all keys immediately.
✅Hash API keys with SHA-256 before storage. Store the hash + metadata. When a request arrives, hash the provided key and compare. Show the full key only once at creation time. This is the same pattern as password storage.
No token introspection caching
Calling the auth server for every single request to validate opaque tokens, adding 50-100ms latency to every API call.
✅Cache introspection results in Redis with a short TTL (30-60 seconds). Accept that revocation has a delay equal to TTL. For immediate revocation needs, use short-lived JWTs (5 min) with refresh tokens instead of opaque tokens.