# Stop Using Full OAuth Handshakes for Internal Microservice Auth

OAuth 2.0 was designed to solve a specific problem: delegating access to third-party applications without sharing credentials over public networks. Somehow, over the last decade, teams started blindly copying this pattern into internal inter-service communication.

If your backend services are making external network round-trips to Keycloak, Auth0, or an internal Identity Provider (IdP) to introspect a token on every single RPC, you've built a distributed bottleneck.

# The Latency Tax of Introspection

Consider a standard internal request flow: a client hits your API Gateway, which fans out to `Order Service`, which calls `Inventory Service` and `Payment Service`.

If every service independently calls an OAuth introspection endpoint to validate the `Bearer` token:

1.  **Network Cascades:** A single user request turns into 3–4 extra HTTP calls back to your IdP. If your IdP sits at 15ms response latency, you've added 45–60ms of pure authorization overhead to your P99 latency.
    
2.  **Single Point of Failure:** If the IdP experiences a minor connection pool exhaustion or redis hiccup, every microservice in your cluster degrades simultaneously.
    

```plaintext
[Client]
   │
   ▼
[API Gateway]
   │
   ▼
[Order Service] ────► [OAuth IdP]
   │                         ▲
   ▼                         │
[Inventory Svc] ─────────┘
  (Introspection Bottleneck)
```

# Asymmetric JWTs Aren't a Free Lunch Either

Teams quickly realize introspection is too slow and switch to self-contained asymmetric JWTs (RS256/ES256). Services validate tokens locally using the IdP's public JSON Web Key Set (JWKS).

While this removes the introspection HTTP call, it introduces new failure modes:

*   **Cryptographic Overhead:** Validating RSA-2048 or ECDSA signatures on high-throughput endpoints (10k+ RPS) burns CPU cycles unnecessarily inside hot execution paths.
    
*   **JWKS Cache Stampedes:** When keys rotate or an expired key cache misses simultaneously under heavy load, dozens of instances rush to fetch `/.well-known/jwks.json`, hitting rate limits or timing out.
    
*   **Instant Revocation is Broken:** You cannot revoke a stateless JWT immediately without maintaining a distributed revocation list—which brings back the central storage dependency you tried to avoid.
    

# What to Do Instead: Gateway Validation + Lightweight Tokens

For internal service-to-service communication within a private VPC or service mesh, authorization should be split into two distinct tiers:

### 1\. Edge Authentication (Public to Private)

Authenticate incoming client requests at the API Gateway using OAuth 2.0, OIDC, or mTLS. The Gateway performs the heavy lifting: validating external JWTs, checking scopes, and terminating public TLS.

### 2\. Internal Context Headers & API Tokens

Once past the Gateway, strip the heavy external token. Pass a lightweight internal context header (e.g., `X-User-ID`, `X-Tenant-ID`, `X-Scopes`) signed with a fast symmetric HMAC key or encrypted via mTLS.

For service-to-service calls (machine-to-machine), use static or periodically rotated API tokens validated against a local memory cache or fast local Redis replica:

```go
// Fast middleware: Hash-based API token lookup (sub-millisecond)
func AuthenticateServiceToken(cache *TokenCache, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := r.Header.Get("X-Service-Token")
		if token == "" {
			http.Error(w, "missing service token", http.StatusUnauthorized)
			return
		}

		// Hash token to prevent timing attacks and lookup in local memory/cache
		tokenHash := sha256.Sum256([]byte(token))
		serviceID, ok := cache.Lookup(tokenHash)
		if !ok {
			http.Error(w, "invalid service token", http.StatusUnauthorized)
			return
		}

		ctx := context.WithValue(r.Context(), serviceIDKey, serviceID)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}
```

# Takeaway

Save full OAuth 2.0 flows for public edges and third-party integrations. Inside your cluster boundary, delegate heavy authentication to the Gateway, rely on mTLS or lightweight signed headers, and keep your hot path authorization checks in memory.

# Sources

*   Code samples: [https://github.com/flashlabs/kiss-samples/tree/main/token-auth](https://github.com/flashlabs/kiss-samples/tree/main/token-auth)
    
*   RFC 6749 - The OAuth 2.0 Authorization Framework: [https://datatracker.ietf.org/doc/html/rfc6749](https://datatracker.ietf.org/doc/html/rfc6749)
    
*   RFC 7662 - OAuth 2.0 Token Introspection: [https://datatracker.ietf.org/doc/html/rfc7662](https://datatracker.ietf.org/doc/html/rfc7662)
    

Cheers!
