3

TLDR: Security context is not propagated to the chained future produced by Mono.toFuture() when making a WebClient request.

I have a web application that is using spring security OAuth2 to enforce authorization on controllers. The application uses spring Reactive WebClient to orchestrate async https request to a few other downstream APIs, one of which "repositoryA" requires the token provided by the caller to be propagated to the downstream call.

To explain this simply the service that orchestrates these calls looks something like this:

public CompletableFuture<Model> doSomething(String id) {
        return repositoryA.get(id)
            .thenCompose(repositoryB::action)
            .thenCompose(repositoryA::fulfill);
}

Each of repositoryA and repositoryB use their own WebClient, repositoryB handles its own downstream authentication via a set of client credentials.

Repositories methods look something like this:

public CompletableFuture<Model> get(Stringid) {
        return webClient.get()
            .uri(b -> b.pathSegment(PATH, id.toString()).build())
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(Model.class)
            .toFuture();
}

repositoryA's WebClient uses a filter to propagate the token as a header from the security context

public class DelegatingSecurityExchangeFilter implements ExchangeFilterFunction {

    static final String SECURITY_CONTEXT_ATTRIBUTES = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES";

    @Override
    public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction next) {
        return Mono.deferContextual(view -> next.exchange(RequestWithBearer(clientRequest, getToken(view))));
    }

    private OAuth2AccessToken getToken(ContextView view) {
        Map<Class<?>, Object> springCtx = view.get(SECURITY_CONTEXT_ATTRIBUTES);
        Authentication auth = (Authentication) springCtx.get(Authentication.class);
        return (OAuth2AccessToken) auth.getCredentials();
    }

    private ClientRequest RequestWithBearer(ClientRequest request, AbstractOAuth2Token token) {
        return ClientRequest.from(request)
            .headers(headers -> headers.setBearerAuth(token.getTokenValue()))
            .build();
    }

}

This works fine for the first repositoryA.get(id) call, but when debugging I can see that after the the first .toFuture() in a repository methods the security context is not propagated to the next thread.

I have investigated many of the concepts here without success so far, I assume reactive WebClient does not use the same mechanism to spawn threads for its event loop. SecurityReactorContextConfiguration seems like it should be doing the job, but it only works if the thread local context is already available on subscribe.

More context:

Matthew.Lothian
  • 2,072
  • 17
  • 23
  • 3
    Mapping Mono/Flux to futures kills the reactor pipeline - it breaks things like back pressure as well as, as you noted, loses subscriber context. I would recommend you just _don’t do that_. Use reactive constructs throughout. – Boris the Spider May 28 '21 at 17:49
  • @BoristheSpider you may have a point, I am trying to abstract the repository implementation detail from the rest of the application as It needs to be swappable in my case. Having the repositories return Mono/Flux couples the application to the repository implementation detail. – Matthew.Lothian May 28 '21 at 17:55
  • 2
    Then return `org.reactivestreams.Publisher` or even `java.util.concurrent.Flow.Publisher`. Your abstraction is broken. `CompletableFuture` is a fairly simple eager async result. A reactive publisher is not only lazy - i.e at the moment you’re forcing eager execution of the request - but also supports flow control and back pressure. It’s very different from simply wrapping a blocking call in a thread - which is basically what future represents. – Boris the Spider May 28 '21 at 18:01
  • I believe you nailed it, that's likely the answer I'm looking form. – Matthew.Lothian May 28 '21 at 18:07

0 Answers0