Spring WebFlux, Security and request body

Viewed 1251

I need to secure REST API implemented with Spring Boot, WebFlux and spring security using HMAC of the request body. Simplifying a bit, on a high level - request comes with the header that has hashed value of the request body, so I have to read the header, read the body, calculate hash of the body and compare with the header value.

I think I should implement ServerAuthenticationConverter but all examples I was able to find so far only looking at the request headers, not the body and I'm not sure if I could just read the body, or should I wrap/mutate the request with cached body so it could be consumed by the underlying component second time?

Is it ok to use something along the lines of:

public class HttpHmacAuthenticationConverter implements ServerAuthenticationConverter {

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        exchange.getRequest().getBody()
                .next()
                .flatMap(dataBuffer -> {
                    try {
                        return Mono.just(StreamUtils.copyToString(dataBuffer.asInputStream(), StandardCharsets.UTF_8));
                    } catch (IOException e) {
                        return Mono.error(e); 
                    }
                })
                ...


I'm getting a warning from the IDE on the copyToString line: Inappropriate blocking method call

Any guidelines or examples?

Thanks!

I have also tried:

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        return Mono.justOrEmpty(exchange.getRequest().getHeaders().toSingleValueMap())
                .zipWith(exchange.getRequest().getBody().next()
                        .flatMap(dataBuffer -> Mono.just(dataBuffer.asByteBuffer().array()))
                )
                .flatMap(tuple -> create(tuple.getT1(), tuple.getT2()));

But that doesn't work - code in the create() method on the last line is never executed.

1 Answers

I make it work. Posting my code for the reference.

Two components are required to make it work - WebFilter that would read and cache request body so it could be consumed multiple times and the ServerAuthenticationConverter that would calculate hash on a body and validate signature.

public class HttpRequestBodyCachingFilter implements WebFilter {
private static final byte[] EMPTY_BODY = new byte[0];

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    // GET and DELETE don't have a body
    HttpMethod method = exchange.getRequest().getMethod();
    if (method == null || method.matches(HttpMethod.GET.name()) || method.matches(HttpMethod.DELETE.name())) {
        return chain.filter(exchange);
    }

    return DataBufferUtils.join(exchange.getRequest().getBody())
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            })
            .defaultIfEmpty(EMPTY_BODY)
            .flatMap(bytes -> {
                ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    @Nonnull
                    @Override
                    public Flux<DataBuffer> getBody() {
                        if (bytes.length > 0) {
                            DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
                            return Flux.just(dataBufferFactory.wrap(bytes));
                        }
                        return Flux.empty();
                    }
                };
                return chain.filter(exchange.mutate().request(decorator).build());
            });
}

}

public class HttpJwsAuthenticationConverter implements ServerAuthenticationConverter {
private static final byte[] EMPTY_BODY = new byte[0];

@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
    return DataBufferUtils.join(exchange.getRequest().getBody())
            .map(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                return bytes;
            })
            .defaultIfEmpty(EMPTY_BODY)
            .flatMap(body -> create(
                    exchange.getRequest().getMethod(),
                    getFullRequestPath(exchange.getRequest()),
                    exchange.getRequest().getHeaders(),
                    body)
            );
}

...

The create method in the Converter implements the logic to validate signature based on the request method, path, headers and the body. It returns an instance of the Authentication if successful or Mono.empty() if not.

The wiring up is done like this:

public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange().pathMatchers(PATH_API).authenticated()
      ...
      .and()
      .addFilterBefore(new HttpRequestBodyCachingFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
      .addFilterAt(jwtAuthenticationFilter(...), SecurityWebFiltersOrder.AUTHENTICATION);
}

private AuthenticationWebFilter jwtAuthenticationFilter(ReactiveAuthenticationManager authManager) {
    AuthenticationWebFilter authFilter = new AuthenticationWebFilter(authManager);
    authFilter.setServerAuthenticationConverter(new HttpJwsAuthenticationConverter());
    authFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(PATH_API));
    return authFilter;
}

@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
    return Mono::just;
}

   
}
Related