Authentication at the Gateway

Check the Token Once, at the Edge

Instead of every service parsing JWTs, the gateway validates the token and rejects bad requests with 401 early. JWTs, login and OAuth2 are covered in the Spring Security material; here we only wire it into the gateway. Add spring-boot-starter-oauth2-resource-server (reactive security is used automatically because the gateway runs on WebFlux):

@Configuration
@EnableWebFluxSecurity
class GatewaySecurityConfig {

    @Bean
    SecurityWebFilterChain gatewaySecurity(ServerHttpSecurity http) {
        return http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)        // stateless JSON API
                .authorizeExchange(ex -> ex
                        .pathMatchers("/api/auth/**").permitAll()           // login, register
                        .pathMatchers(HttpMethod.GET, "/api/products/**", "/api/categories/**").permitAll()
                        .pathMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                        .anyExchange().authenticated())
                .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
                .build();
    }
}
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.shopease.in/realms/shopease   # Keycloak realm, or your auth server

Pass the User to the Services

After validation, the gateway can forward the user ID in a header so downstream services do not have to parse the token. It must first remove any X-User-Id the client sent, or anyone could pretend to be another user:

@Component
class UserIdHeaderFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerWebExchange cleaned = exchange.mutate()
                .request(r -> r.headers(h -> h.remove("X-User-Id")))   // never trust the client
                .build();

        return cleaned.getPrincipal()
                .filter(JwtAuthenticationToken.class::isInstance)
                .cast(JwtAuthenticationToken.class)
                .map(auth -> cleaned.mutate()
                        .request(r -> r.header("X-User-Id", auth.getToken().getSubject()))
                        .build())
                .defaultIfEmpty(cleaned)
                .flatMap(chain::filter);
    }

    @Override
    public int getOrder() { return 0; }
}

Example

curl -i -X POST localhost:8080/api/orders -d @order.json -H "Content-Type: application/json"

curl -i -X POST localhost:8080/api/orders -d @order.json -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI0MiIsInNjb3BlIjoidXNlciJ9.kQ..."

Example output

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

HTTP/1.1 201 Created
{"orderId":1002,"status":"CREATED","total":1499.00}

Defence in Depth

Note: The gateway check is not the only check. Services should not be reachable from the internet (private network only), and sensitive services like payment-service should verify the token or a service-to-service credential too. This is covered in the advance level and in the Spring Security material.