Spring Cloud Gateway — Securing Services with API Key

In this post, I will explore how to implement API security with an API key using Spring Cloud Gateway.

Overview

In microservices architectures, the security of data passing between the different API services becomes decisive. it is therefore important to setup several levels in order to improve the security between the different exchanges between these services.

That’s why the API Security Model, based on Richardson Maturity Model, describes API security at increasing levels of security.

The API Security Maturity Model

In this model, security and trust are increasingly improved at each level. It has four levels:

  • Level 0: API Keys and Basic Authentication
  • Level 1: Token-Based Authentication
  • Level 2: Token-Based Authorization
  • Level 3: Centralized Trust Using Claims

In this story, we will focus on level 0 (API Keys) with implementation through the Spring Cloud Gateway.

Spring Cloud Gateway provides a library for building API gateways on top of Spring and Java. It provides a flexible way of routing requests based on a number of criteria, as well as focuses on cross-cutting concerns such as security, resiliency, and monitoring.

According to the architecture above, access to services (A or B) will be conditioned by the validation of the key which will be sent in the header of the request.

Project Setup

Creating a simple Spring Boot project from start.spring.io with the following dependencies: Lombok, spring cloud gateway, and spring data redis.

Project Structure

Our architecture contains three main packages: configurationfilterdomain

Global filters are executed for every route defined in the API Gateway. In our case, we are going to create a custom pre-filter. This filter code is executed before Spring Cloud Gateway routes the request to a destination web service endpoint.

@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    RedisHashComponent redisHashComponent;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        List<String> apiKeyHeader = exchange.getRequest().getHeaders().get("gatewayKey");
        log.info("Api key: {}", apiKeyHeader);

        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        String routeId = route != null ? route.getId() : null;
        if (routeId == null || CollectionUtils.isEmpty(apiKeyHeader) || !isAuthorized(routeId, apiKeyHeader.get(0))) {
            log.error("Api Key not valid");
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "You cannot consume this service. Please check your api key.");
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    /**
     * @param routeId id of route
     * @param apikey client header api key
     * @return true if is authorized, false otherwise
     */
    private boolean isAuthorized(String routeId, String apikey) {

        Object apiKeyObject = redisHashComponent.hGet(AppConstant.RECORD_KEY, apikey);
        if(apiKeyObject != null){
            ApiKey key = ObjectMapperUtils.objectMapper(apiKeyObject, ApiKey.class);
          return  key.getServices().contains(routeId);
        }else {
            return false;
        }
    }


}

For each call made by the client, a check is made in redis to confirm that the client has the right to consume the requested service.

Now , We have a custom routing configuration.

 @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(AppConstant.SERVICE_A_KEY,
                        r -> r.path("/api/service-a/**")
                                .filters(f -> f.stripPrefix(2)).uri("http://localhost:8081"))
                .route(AppConstant.SERVICE_B_KEY,
                        r -> r.path("/api/service-b/**")
                                .filters(f -> f.stripPrefix(2)).uri("http://localhost:8082"))
                .build();
    }

Testing

We can run our gateway and our services.

  • When the client is not authorized to consume the service with its api key

When the client is authorized to consume the service

Summary

When used in large solutions, it is necessary to use existing API Gateway solutions that offer already implemented security services like Kong API GatewayTyk, etc.

All source code is available on GitHub.

References

👉 Link to Medium blog

** Cover image by Nicolas Picard on Unsplash

Related Posts