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: