Spring Cloud Gateway Security with JWT

Reading Time: 3 minutes

Security,As we all know that everything that is on the Internet need security. Especially when you create software and work with sensitive user data, such as emails, phone numbers, addresses, credit cards, etc.So,here we will go through securing API Gateway with Json Web Tokens(JWT).

Spring recently released an update for microservice applications, and this update is a Spring Cloud Gateway that stands in front of all of your microservices and accepts requests, and then redirects them to the corresponding service.

It is a practice to add a security layer here, so if some unauthorized request comes in,it is not get passed to the resource microservice and we will reject on an API Gateway level.

So how security should works?

A client makes a request to some secured resource with no authorization. API Gateway rejects it and redirects the user to the Authorization Server to authorize himself in the system, get all required grants and then make the request again with these grants to receive information from that secured resource.

Let’s see API Gateway code:

Firstly, we need the filter, which will be checking all the incoming requests to our API for a JWToken.

@RefreshScope
@Component
public class AuthenticationFilter implements GatewayFilter {

    @Autowired
    private RouterValidator routerValidator;//custom route validator
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        if (routerValidator.isSecured.test(request)) {
            if (this.isAuthMissing(request))
                return this.onError(exchange, "Authorization header is missing in request", HttpStatus.UNAUTHORIZED);

            final String token = this.getAuthHeader(request);

            if (jwtUtil.isInvalid(token))
                return this.onError(exchange, "Authorization header is invalid", HttpStatus.UNAUTHORIZED);

            this.populateRequestWithHeaders(exchange, token);
        }
        return chain.filter(exchange);
    }


    /*PRIVATE*/

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

    private String getAuthHeader(ServerHttpRequest request) {
        return request.getHeaders().getOrEmpty("Authorization").get(0);
    }

    private boolean isAuthMissing(ServerHttpRequest request) {
        return !request.getHeaders().containsKey("Authorization");
    }

    private void populateRequestWithHeaders(ServerWebExchange exchange, String token) {
        Claims claims = jwtUtil.getAllClaimsFromToken(token);
        exchange.getRequest().mutate()
                .header("id", String.valueOf(claims.get("id")))
                .header("role", String.valueOf(claims.get("role")))
                .build();
    }
}

In the filter, we defined that we have some secured routes and ones that do not require tokens.

If a request is made to the secured route then we check for its token, see if it is present in the request.If all these conditions are true we mutate our request on the go.

Here, we set the userId and role into request headers by doing this:

@RequestHeader String userId,
@RequestHeader String role

With no need to parse the token on each microservice level to get this data. We just do this once on API Gateway level and that’s it.

Let’s also take a look at the RouterValidator that decides whether a request should contain a token or not:

@Component
public class RouterValidator {

    public static final List<String> openApiEndpoints= List.of(
            "/auth/register",
            "/auth/login"
    );

    public Predicate<ServerHttpRequest> isSecured =
            request -> openApiEndpoints
                    .stream()
                    .noneMatch(uri -> request.getURI().getPath().contains(uri));

}

It contains a list of open routes strings and checks if the current request URI is not in the openApiEndpoints list. If not, then the token definitely must be present in the request. Otherwise, 401 Unauthorized!

We also need to somehow validate the token if it’s present. So we need JWT util that would parse that token for us and see if it is a valid one. For this, we need to create a custom JWT util service.

@Component
public class JwtUtil {

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

    private Key key;

    @PostConstruct
    public void init(){
        this.key = Keys.hmacShaKeyFor(secret.getBytes());
    }

    public Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    private boolean isTokenExpired(String token) {
        return this.getAllClaimsFromToken(token).getExpiration().before(new Date());
    }

    public boolean isInvalid(String token) {
        return this.isTokenExpired(token);
    }

}

Take a JWT, parse it, check its expiration and secure it.

We have  filter and router validatorjwt util, and now we want to configure our API Gateway.

This is to understand what request to route to what microservice. There should be some set of rules for that,Let’s create it:

@Configuration
@EnableHystrix
public class GatewayConfig {

    @Autowired
    AuthenticationFilter filter;

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("user-service", r -> r.path("/users/**")
                        .filters(f -> f.filter(filter))
                        .uri("lb://user-service"))

                .route("auth-service", r -> r.path("/auth/**")
                        .filters(f -> f.filter(filter))
                        .uri("lb://auth-service"))
                .build();
    }

}

So we defined a GatewayConfig with RouteLocator and tell:

  • all requests that start with /users/** should route to user service and our custom JWT filter should apply to each such request
  • all requests that start with /auth/** should route to auth service and our custom JWT filter should be apply to each such request too.

The browser will see the 401 Unauthorized error, will understand that it needs to authorize.

To access this resource, will authorize himself, gets the token, makes another request to that resource and this time system will allow him to do this with no doubts.And we are done here!

Conclusion:

In this blog, we learned about the securing the API Gateway with AWT and how to implement it.Thank You for reading. For more, you can refer to: https://spring.io/blog/2019/08/16/securing-services-with-spring-cloud-gateway

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading