API KeysJWTOAuth 2.0OIDCmTLSHMACScopesRBAC

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.

45 min read9 sections
01

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 MethodBest ForStatefulness
API KeysServer-to-server, third-party integrationsStateful (key lookup)
JWT (Bearer Token)User sessions, mobile/SPA clientsStateless (signature verification)
OAuth 2.0 + OIDCDelegated access, SSO, third-party appsStateful (token introspection) or stateless (JWT)
mTLSService-to-service, high-security B2BStateless (certificate validation)
HMAC SigningWebhook verification, tamper-proof requestsStateless (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.

02

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).

AspectDetails
FormatRandom string (32-64 chars), often prefixed: sk_live_abc123
TransmissionHeader (X-API-Key or Authorization) — never in URL query params
StorageHash the key (SHA-256), store hash + metadata. Never store plaintext.
MetadataOwner, created_at, rate_limit_tier, scopes, last_used_at
RotationSupport multiple active keys per client for zero-downtime rotation
kong-api-key-plugin.yamlyaml
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
03

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.

AlgorithmTypeKey ManagementUse Case
RS256Asymmetric (RSA)Auth server has private key, gateway has public keyMost common — gateway only needs public key
ES256Asymmetric (ECDSA)Smaller keys, faster verificationModern alternative to RS256
HS256Symmetric (HMAC)Same secret on auth server and gatewaySimple but secret must be shared — avoid in distributed systems
kong-jwt-plugin.yamlyaml
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 headerget 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 claimsforward 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
04

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.

FlowClient TypeGateway Role
Authorization CodeWeb apps with backendValidate access token, check scopes
Authorization Code + PKCESPAs, mobile appsSame — PKCE prevents code interception
Client CredentialsMachine-to-machineValidate token, identify calling service
Token IntrospectionOpaque tokensCall auth server to validate (cached)
token-introspection-response.jsonjson
{
  "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.

05

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.

AspectStandard TLSMutual TLS
Server identity✅ Server presents cert✅ Server presents cert
Client identity❌ Not verified at TLS layer✅ Client presents cert
Use casePublic APIs, browser clientsB2B integrations, service-to-service
Certificate managementServer certs onlyServer + client certs (more complex)
RevocationCRL or OCSP for serverCRL or OCSP for both sides
nginx-mtls.confnginx
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.

hmac-signature-example.shbash
# 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.

06

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.

LevelWhereExample
Coarse-grained (gateway)API GatewayAdmin role required for /admin/* endpoints
Scope-based (gateway)API GatewayToken must have write:orders scope for POST /orders
Resource-level (service)Backend serviceUser can only view their own orders
Field-level (service)Backend serviceOnly managers see salary field
opa-gateway-policy.yamlyaml
# 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
07

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.

ResponsibilityGatewayService
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

identity-propagation.yamlyaml
# 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.

08

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.

09

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.