Why Authenticate at the Gateway?
Every microservice that processes authenticated requests must validate the caller's identity. Without a gateway, each service implements its own JWT parsing, signature verification, and claims extraction. This duplicates code, creates inconsistency (one service checking expiry, another not), and means every service needs access to public keys or a connection to the authentication service.
Centralizing authentication at the gateway offers three advantages:
- Fail-fast: Unauthenticated requests are rejected at the front door with minimal compute — no database queries, no service calls, no logging of PII
- Consistency: Authentication policy is configured in one place, not scattered across dozens of services with varying levels of rigor
- Simplification: Downstream services receive pre-authenticated requests and trust injected identity headers rather than re-validating tokens
The trade-off: the gateway becomes the trust boundary. Services behind the gateway must be protected from direct access (network policy, private subnets) — if a service is reachable without going through the gateway, authentication is bypassed.
JWT Validation
JSON Web Tokens are the dominant authentication mechanism for APIs. A JWT is a signed, base64-encoded JSON payload containing claims (assertions about the caller's identity and permissions).
Token Structure
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 ← header (base64)
.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcwOTAzNTI2MH0 ← payload (base64)
.SflKxwRJSMeKKF2QT4fwpMeJf36P... ← signature
Decoded payload:
{
"sub": "user_123",
"email": "[email protected]",
"roles": ["admin"],
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"iat": 1709031660,
"exp": 1709035260
}
JWKS Endpoint Validation
The gateway fetches the authentication server's public keys from its JWKS (JSON Web Key Set) endpoint and uses them to verify token signatures. The JWKS URL is typically {issuer}/.well-known/jwks.json:
# Kong JWT plugin with JWKS
plugins:
- name: jwt
config:
key_claim_name: kid # header claim identifying the signing key
claims_to_verify: [exp, nbf]
maximum_expiration: 3600 # reject tokens with exp > 1 hour from now
# JWKS fetcher config (Kong OIDC plugin)
- name: openid-connect
config:
issuer: https://auth.example.com
# Kong auto-fetches JWKS from issuer/.well-known/openid-configuration
The gateway caches JWKS keys and rotates them automatically when a new key is published. This allows zero-downtime key rotation: publish the new key, wait for gateways to cache it, then start signing tokens with the new key.
Claims Validation Checklist
| Claim | Validation |
|---|---|
| `sig` | Cryptographic signature verification (always) |
| `exp` | Token not expired (always) |
| `nbf` | Token not used before "not before" time (if present) |
| `iss` | Issuer matches expected authentication server (always) |
| `aud` | Audience includes this API (always for API tokens) |
| `iat` | Issued-at not too far in the past (optional, defense against replayed old tokens) |
Token Refresh Handling
The gateway does not handle token refresh — that is the client's responsibility. When the gateway returns 401 due to an expired token, the client should call the authentication server's /token endpoint with a refresh token to obtain a new access token, then retry the original request.
OAuth Token Introspection
For opaque tokens (non-JWT tokens that are random strings), the gateway cannot validate the token locally. It must call the authorization server's introspection endpoint (defined in RFC 7662):
POST /oauth2/introspect HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic {gateway_credentials}
token=2YotnFZFEjr1zCsicMWpAA
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"sub": "user_123",
"scope": "read:orders write:orders",
"exp": 1709035260
}
Introspection adds latency to every authenticated request. Cache introspection results using the token as the cache key, with a TTL shorter than the token's expiry. This reduces introspection calls from N per request to 1 per token lifetime:
def introspect_token(token: str) -> dict | None:
cache_key = f'introspect:{hashlib.sha256(token.encode()).hexdigest()}'
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
result = auth_server.introspect(token)
if result.get('active'):
ttl = result['exp'] - int(time.time()) - 60 # 60s buffer before exp
redis.setex(cache_key, max(ttl, 0), json.dumps(result))
return result
API Key Verification
API keys are long, random strings that identify and authenticate a client. They are simpler than OAuth but lack token expiry and scopes. Common in developer-facing APIs and machine-to-machine integrations:
GET /api/v1/data
X-API-Key: sk_live_4xMp8QzN9rLcKvD2
The gateway looks up the key in a database or cache to find the associated client, check if it is active, and retrieve its rate limit tier:
# Kong key-auth plugin
plugins:
- name: key-auth
config:
key_names: ["X-API-Key", "apikey"] # header or query param
key_in_body: false
hide_credentials: true # strip key before forwarding to upstream
Always strip the API key before forwarding to upstream services — services should not have access to the raw credential.
Identity Propagation to Downstream Services
After authentication, the gateway injects identity information into the request as standard headers that downstream services can read without re-validating:
X-User-ID: user_123
X-User-Email: [email protected]
X-User-Roles: admin,editor
X-Auth-Token-Exp: 1709035260
X-Request-ID: 04af84e7-1307-4f68-b5b3-6b58f6ab11f9
Trust Boundary Considerations
These headers are only trustworthy if upstream services cannot be reached without going through the gateway. Two defenses:
- Network policy: Place backend services in a private subnet or VPC with no public ingress — only the gateway can reach them
- Header signing: The gateway signs the injected headers with an HMAC key shared only with backend services; services verify the signature
Never trust X-User-ID headers that arrive from external clients — the gateway should strip them from inbound requests before injecting its own validated values:
# Kong: strip any incoming X-User-* headers before auth
plugins:
- name: request-transformer
config:
remove:
headers: ["X-User-ID", "X-User-Email", "X-User-Roles"]
Summary
JWT validation at the gateway is the preferred approach for modern APIs — verify the signature locally using cached JWKS keys, validate all standard claims, and inject clean identity headers downstream. Use OAuth introspection for opaque tokens but cache aggressively to avoid latency. API keys are appropriate for machine-to-machine integrations — always strip them before forwarding. Protect backend services at the network level so identity headers cannot be spoofed.