maximdim
maximdim

Reputation: 8169

Spring WebFlux, Security and request body

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.

Upvotes: 5

Views: 2825

Answers (2)

ahll
ahll

Reputation: 2417

just some improve of fitler and cache, hope we hace simple solution in the future.

class WebhookBodyCacheRequest extends ServerHttpRequestDecorator {
  private final Mono<String> body;

  WebhookBodyCacheRequest(final ServerHttpRequest delegate, final WebhookRequestValueExtractor extractor) {
    super(delegate);
    body = extractor.extractBody(delegate).cache(); //cache body for next usage
  }

  @Override
  public Flux<DataBuffer> getBody() {
    return body
        .flux()
        .map(bodyString -> DefaultDataBufferFactory.sharedInstance.wrap(bodyString.getBytes(StandardCharsets.UTF_8)));
  }
}

private AuthenticationWebFilter authenticationWebFilter(final ReactiveAuthenticationManager authenticationManager) {
    final var authFilter = new AuthenticationWebFilter(authenticationManager) {
      @Override
      public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
        return super.filter(
            exchange.mutate().request(new WebhookBodyCacheRequest(exchange.getRequest(), extractor)).build(),
            chain
        );
      }
    };
    authFilter.setServerAuthenticationConverter(new WebhookAuthenticationConverter(verifier, extractor));
    return authFilter;
  }

@Component
class WebhookAuthenticationConverter implements ServerAuthenticationConverter {
  private final WebhookSignatureVerifier verifier;

  private final WebhookRequestValueExtractor extractor;

  WebhookAuthenticationConverter(final WebhookRequestValueExtractor extractor) {
    this.extractor = extractor;
  }

  @Override
  public Mono<Authentication> convert(final ServerWebExchange exchange) {
    return extractor.extractBody(exchange.getRequest())
        .filter(body -> isValid(exchange, body))
        .map(body -> new UsernamePasswordAuthenticationToken(null, null, null));
  }

  private Boolean isValid(final ServerWebExchange exchange, final String body) {
    //verify method
  }
}
@Component
class WebhookRequestValueExtractor {

  Mono<String> extractBody(final ServerHttpRequest request) {
    return DataBufferUtils.join(request.getBody())
        .map(this::readValue);
  }

  private String readValue(final DataBuffer dataBuffer) {
    final var bytes = new byte[dataBuffer.readableByteCount()];
    dataBuffer.read(bytes);
    DataBufferUtils.release(dataBuffer);
    return new String(bytes, StandardCharsets.UTF_8);
  }
}

Upvotes: 0

maximdim
maximdim

Reputation: 8169

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;
}

   
}

Upvotes: 5

Related Questions