Search code examples
spring-bootspring-webfluxproject-reactor

Read request body in HandlerFilterFunction and pass to next handler if valid


Use case is to validate data in JWT with request body. JWT sub contains

{
  "sub": "{\"data\": {\"street\": \"Street 1\", \"landmark\": \"TallestTower\"}}"
}

Request must contain exact data. Forbidden if not match, otherwise pass on for further processing.

Using:

  • Springboot 3,
  • WebFlux reactive with router functions
  • Jose4j for JWT.

During debug, when it reaches to ResourceRoutesHandler.create() function, and tries to execute request.bodyToMono().flatMap(), it just exits with HTTP 200. There are several further processes in .flatMap() but they are not executed. Interestingly, there is no ServerResponse.ok() in the workspace to produce such result with HTTP 200.

My understanding this ServerRequest.from(request).build() is causing it.

Is there a way to validate request body and send the same request or within the same subscription? Or is there an alternate way to validate request data before reaches to route handler?

Routes


@Configuration
@RequiredArgsConstructor
public class ResourceRoutes {

  final JwtHandlerFilterFunction jwtHandlerFilterFunction;

  public RouterFunction<ServerResponse> createResource(ResourceRoutesHandler recourceRoutesHandler) {
    return RouterFunction.route(
            POST("/create").and(accept(MediaType.APPLICATION_JSON)),
            recourceRoutesHandler::create
        )
        .filter(jwtHandlerFilterFunction);
  }
}

HandlerFilterFunction


@Component()
@RequiredArgsConstructor
public class JwtHandlerFilterFunction
    implements HandlerFilterFunction<ServerResponse, ServerResponse> {

  final ObjectMapper objectMapper;

  @Override
  public Mono<ServerResponse> filter(final ServerRequest request,
                                     final HandlerFunction<ServerResponse> next) {

    Headers headers = request.headers();
  
    Optional<String> jwtToken = Optional.ofNullable(headers.firstHeader("Auth-Token"));

    return jwtToken.map(_jwt -> jwtValidateService
                       .validateReactive(_jwt)
                       .flatMap(verifyPayload(request))
                       .flatMap(validMessage -> next.handle(
                        // clone here and pass on to next
                        // this is causing fail to execute request in route handler
                           ServerRequest.from(request)
                                        .build()
                       ))
                       .onErrorResume(throwable -> ServerResponse.status(HttpStatus.FORBIDDEN)
                           .contentType(MediaType.APPLICATION_JSON)
                           .bodyValue("Authorization failed")
                       ) // jwt validation failed
                   )
                   .orElse(next.handle(request)); // to next if header not present
  }

  private Function<String, Mono<String>> verifyPayload(final ServerRequest clientRequest) {
    return subject -> clientRequest
        .bodyToMono(String.class) // as the body already read how to pass on to next handler?
        .flatMap(requestData -> Mono.fromCallable(() -> {
          // for simplicity
          if (!subject.equals(requestData)) {
            throw new RuntimeException("Invalid subject data");
          }
          return "Valid";
        }));
  }

}

Thank you for your time


Solution

  • I don't think it is a good idea to rely on the request's body in an HandlerFilterFunction. Note that the ServerRequest.from(...) method doesn't copy the body, for a similar reason: body publisher can generally only be subscribed and consumed once.

    not debating the whole arrangement further, I think you might have a way out in your case: since you read the whole body as a String in verifyPayload, you could perhaps merge your two jwtToken flatMaps into a single one by modifying the verifyPayload method like so:

    private Function<String, Mono<String>> verifyPayload(final ServerRequest clientRequest, 
            //added the HandlerFunction as a parameter
            final HandlerFunction<ServerResponse> next) {
        return subject -> clientRequest
            .bodyToMono(String.class)
            .flatMap(body -> {
              // for simplicity
              if (!subject.equals(body)) {
                throw new RuntimeException("Invalid subject data");
              }
              //we have already read the full body, so we can "forward" it
              return next.handle(ServerRequest.from(clientRequest).body(body).build());
            }));
      }