Skip to main content

The Security Perimeter: Centralized Authentication Middleware

How LZStock establishes a zero-trust network boundary at the API Gateway, utilizing gRPC for centralized identity verification and context hydration.

TL;DR
  • Zero-Trust Identity Hydration: Stripped token parsing from internal microservices entirely. The Gateway strictly validates credentials and injects a trusted investor_id directly into the request context.
  • Instant Revocation via gRPC: Traded a negligible 1ms network hop for absolute security. Validating tokens against a live Identity service avoids the fatal flaw of un-revocable local JWTs.
  • Seamless WebSocket Auth: Overcame browser WebSocket constraints by supporting secure query-parameter token extraction, actively mitigated by infrastructure-level log redaction.

The Objective

In a distributed architecture, if every microservice (e.g., Dashboard Service, Market Service) has to independently parse tokens, validate signatures, and check database revocation lists, the codebase becomes highly coupled and difficult to audit.

The objective is to establish a Centralized Security Perimeter. The API Gateway acts as the single enforcer. It intercepts the HTTP request, delegates the cryptographic verification to a dedicated Identity microservice via high-speed gRPC, and "hydrates" the request context with a trusted investor_id before routing it to the internal network.

The Mental Model & Trust Boundary

Downstream microservices operate in a "Zero-Trust" environment regarding the outside world, but they inherently trust the API Gateway. Once a request passes this middleware, the downstream services assume the user is fully authenticated.

Codebase Anatomy

mods/bc15-api-gateway
└── server
└── middle
└── auth.go

Core Implementation

Below is the centralized middleware. Notice how it seamlessly translates gRPC status codes back to HTTP REST responses and supports both standard REST APIs and WebSocket handshakes.

// internal/middleware/auth.go

func Authenticate(c *routing.Context) (err error) {
// 1. Bypass Preflight: CORS OPTIONS requests do not carry credentials
if c.IsOptions() {
return nil
}

ctx := LZContext.New(c).WithTimeout(time.Second * 3)

// 2. Token Extraction Strategy (WebSocket & REST support)
// We check QueryArgs first to support Browser WebSocket API constraints,
// then fallback to standard HTTP Authorization Headers.
token := string(c.RequestCtx.QueryArgs().Peek("token"))
if token == "" {
token = string(c.Request.Header.Peek("Authorization"))
}

if token == "" {
restful.Unauthorized(c, "Authorization token is strictly required")
c.Abort() // Halt the middleware chain immediately
return nil
}

// 3. Delegation: Offload verification to the specialized Identity Microservice
req := &pb.AuthReq{Token: token}
res, err := gRPC.Services().InvestorServices.Auth(ctx, req)
if err != nil {
// 4. Protocol Translation: gRPC Error to HTTP 401/500
if e, ok := status.FromError(err); ok && e.Code() == codes.PermissionDenied {
restful.Unauthorized(c, "Invalid or expired token")
} else {
restful.Fail(c, "Internal authentication service error")
}
c.Abort()
return nil
}

// 5. Context Hydration: Inject the trusted Identity into the memory context
// Downstream handlers will extract this instead of parsing tokens themselves.
c.Set("investor_id", res.Investor.Id)

// Proceed to the protected business logic
c.Next()
return nil
}

Edge Cases & Trade-offs

  • Query Parameters vs. Security Logs: Extracting the token from QueryArgs is an architectural necessity to support standard browser WebSocket connections (which cannot append custom HTTP headers during the initial handshake). However, the trade-off is that query parameters are often recorded in plain text by edge load balancers (like AWS ALB or NGINX). To mitigate this, infrastructure-level log redaction must be configured to mask the ?token= parameter in all access logs.
  • The Network Hop Penalty: By making a gRPC call to the InvestorService for every single incoming request, we introduce a micro-network hop (typically 1-2ms within the same Kubernetes cluster).
    • Alternative: The Gateway could validate the JWT locally without a network call.
    • The Decision: We chose the gRPC network hop because it enables Immediate Revocation. If a malicious user is banned or logs out, the database reflects it instantly. Local JWT validation would allow the attacker to keep accessing the system until the token's expiration time runs out. The 1ms latency is an acceptable trade-off for absolute security control.

The Outcome

By centralizing identity resolution into a single FastHTTP middleware, LZStock ensures that internal domain microservices remain completely oblivious to cryptographic token parsing, guaranteeing a unified security perimeter with immediate token revocation capabilities.