Skip to content

API Gateway in Microservices — Part 2 | Authentication in Gateway | Global and Route Specific Filters

The Embassy Security Checkpoint Analogy

Imagine traveling to a foreign nation that requires a visa. You do not show up unannounced at individual ministry offices in the capital city — the Ministry of Transport, the Ministry of Commerce, the Ministry of Health — presenting your passport and credentials at every single desk. Instead, you present your passport and visa at the international border embassy checkpoint once.

The border officer inspects your passport, validates its holographic seal, extracts your identity and permitted visa privileges, stamps your approval document, and attaches a trusted badge to your jacket. When you subsequently visit the Ministry of Commerce, the internal guards simply check your trusted badge. They do not reverify your passport against the global database or repeat the visa verification process.

This is the exact security model used in modern microservices architecture with an API Gateway. The API Gateway serves as the border checkpoint. It intercepts incoming client requests, validates JWT authentication tokens, rejects unauthorized traffic at the edge, extracts the user identity and roles, and injects trusted identity headers (X-User-Id, X-User-Role) into the request before forwarding it downstream. Internal microservices trust these headers, completely eliminating the need to duplicate complex authentication filters in twenty different backend services.

This lecture covers global versus route specific filters in Spring Cloud Gateway, building custom gateway filter factories, JWT token validation at the edge, and secure downstream identity propagation.


Global Filters vs Route Specific Filters

Spring Cloud Gateway provides two distinct tiers of filters:

CategoryGlobalFilterAbstractGatewayFilterFactory
ScopeApplies to every single route configured in the gatewayApplied only to explicit routes that declare the filter
RegistrationAuto detected as Spring @Component beansDeclared in route configuration properties or Java DSL
Typical Use CaseRequest logging, global metrics, tracing, CORS handlingAuthentication, route specific rate limits, request header enrichment
Execution OrderControlled via @Order or Ordered interfaceControlled by filter chain position in route definition
Client Request -> [ GlobalFilter 1 (Trace ID) ]
                        |
                  [ Route Specific Filter (JWT Auth) ]
                        |
                  [ GlobalFilter 2 (Metrics) ]
                        |
                  Downstream Microservice

The Architecture of Edge Authentication

In a distributed system, where should authentication happen?

[Insecure / Redundant Pattern: Auth in Every Service]
  Client -> Gateway -> Order Service (validates JWT, connects to User DB)
                    -> Product Service (validates JWT, connects to User DB)
                    -> Payment Service (validates JWT, connects to User DB)
  * Massive code duplication across all services
  * High database overhead verifying users on every inter-service call

[Production Pattern: Edge Authentication at Gateway]
  Client ---> [ API Gateway ]
                     |
         1. Extracts Authorization Header
         2. Validates JWT Signature & Expiry
         3. If invalid -> returns 401 Unauthorized immediately
         4. If valid -> extracts userId & role
         5. Injects X-User-Id and X-User-Role into downstream request
                     |
                     +---> Order Service (reads X-User-Id header directly)
                     +---> Product Service (reads X-User-Id header directly)

By validating tokens at the gateway edge:

  1. Malicious or expired requests are stopped at the perimeter with zero processing overhead on backend services.
  2. Backend microservices remain lightweight, stateless, and focused purely on core business domain logic.

Building a Custom JWT Authentication Filter

To create a route specific filter in Spring Cloud Gateway, extend AbstractGatewayFilterFactory<Config>:

java
package com.example.gateway.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.security.Key;

@Component
public class AuthenticationFilter extends AbstractGatewayFilterFactory<AuthenticationFilter.Config> {

    @Value("${jwt.secret:mySuperSecretKeyForJwtSigningMustBeLongEnough123456}")
    private String jwtSecret;

    public AuthenticationFilter() {
        super(Config.class);
    }

    public static class Config {
        // Configuration properties for this filter can be declared here
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // 1. Check for Authorization header presence
            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                return onError(exchange, "Missing Authorization Header", HttpStatus.UNAUTHORIZED);
            }

            String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
                return onError(exchange, "Invalid Authorization Header Format", HttpStatus.UNAUTHORIZED);
            }

            String token = authHeader.substring(7);

            try {
                // 2. Validate JWT signature and extract claims
                Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
                Claims claims = Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

                String userId = claims.getSubject();
                String role = claims.get("role", String.class);

                // 3. Mutate request to add trusted headers for downstream services
                ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                    .header("X-User-Id", userId)
                    .header("X-User-Role", role != null ? role : "USER")
                    .build();

                // 4. Continue filter chain with mutated request
                return chain.filter(exchange.mutate().request(mutatedRequest).build());

            } catch (Exception e) {
                return onError(exchange, "Unauthorized token: " + e.getMessage(), HttpStatus.UNAUTHORIZED);
            }
        };
    }

    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        return response.setComplete();
    }
}

Configuring the Custom Filter in application.properties

Once your filter extends AbstractGatewayFilterFactory and is registered as a @Component, Spring Cloud Gateway recognizes it by its prefix (the class name minus the GatewayFilterFactory or Filter suffix):

properties
# Route for authenticated Order Service
spring.cloud.gateway.routes[0].id=order-service
spring.cloud.gateway.routes[0].uri=lb://ORDER-SERVICE
spring.cloud.gateway.routes[0].predicates[0]=Path=/order/**
spring.cloud.gateway.routes[0].filters[0]=AuthenticationFilter

# Route for public Auth Service (login/register does not require token)
spring.cloud.gateway.routes[1].id=auth-service
spring.cloud.gateway.routes[1].uri=lb://AUTH-SERVICE
spring.cloud.gateway.routes[1].predicates[0]=Path=/auth/**

Notice that requests to /auth/** do not include AuthenticationFilter, allowing login and registration endpoints to remain publicly accessible.


Reading Propagated Headers in Downstream Microservices

Because the gateway validated the token and injected X-User-Id, downstream controllers can read the user identity directly using standard @RequestHeader:

java
package com.example.orderservice.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {

    @PostMapping
    public ResponseEntity<String> placeOrder(
            @RequestHeader("X-User-Id") String userId,
            @RequestHeader(value = "X-User-Role", defaultValue = "USER") String role,
            @RequestBody OrderRequest request) {

        System.out.println("Processing order for authenticated user: " + userId + " with role: " + role);

        return ResponseEntity.ok("Order confirmed for user " + userId);
    }
}

Implementing a Global Logging Filter

To execute logic across every route, implement GlobalFilter and Ordered:

java
package com.example.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

@Component
public class GlobalTracingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String traceId = UUID.randomUUID().toString();

        // Pre-filter: Log and inject trace id into request
        System.out.println("[GATEWAY INCOMING] Request path: " + exchange.getRequest().getPath() + " TraceId: " + traceId);

        ServerWebExchange mutatedExchange = exchange.mutate()
            .request(r -> r.header("X-Trace-Id", traceId))
            .build();

        return chain.filter(mutatedExchange)
            .then(Mono.fromRunnable(() -> {
                // Post-filter: Log response status code
                System.out.println("[GATEWAY OUTGOING] Status: " + exchange.getResponse().getStatusCode() + " TraceId: " + traceId);
            }));
    }

    @Override
    public int getOrder() {
        // Lower number means higher priority in filter chain
        return -1;
    }
}

Interview Questions & Pitfalls

Q1: Why is validating JWT tokens at the API Gateway preferred over validating them in each downstream microservice?

Centralizing JWT validation at the gateway prevents invalid requests from ever entering the internal network, protecting backend resources from denial of service. It completely eliminates duplicated security code across microservices, reduces database lookups, and simplifies token validation logic to a single codebase.

Q2: What is the difference between GlobalFilter and AbstractGatewayFilterFactory?

GlobalFilter applies automatically to all routes in the gateway without needing configuration in route definitions. AbstractGatewayFilterFactory creates route specific filters that must be explicitly declared on individual routes in application.properties or Java DSL, making it ideal for authentication or route specific limits.

Q3: Why must you use reactive classes such as ServerHttpRequest and Mono<Void> in Spring Cloud Gateway filters?

Spring Cloud Gateway is built on non blocking Spring WebFlux and Netty, not the standard Servlet API. Standard HttpServletRequest and HttpServletResponse are blocking constructs and do not exist in Spring Cloud Gateway. All filter logic must operate within Project Reactor's reactive pipeline.

Q4: How do downstream services prevent clients from spoofing the X-User-Id header?

In a production deployment, internal microservices are isolated inside a private virtual network (VPC) with security group rules. Public internet traffic can only reach the API Gateway. The API Gateway strips any client provided X-User-Id headers before injecting its own verified value, ensuring downstream services receive only trusted headers.

Q5: How do post filter actions execute in a reactive gateway filter?

In Spring WebFlux, post filter logic is chained onto the return Mono<Void> using operators like .then(Mono.fromRunnable(...)). The runnable executes asynchronously after the downstream service returns its response to the gateway.