Summary
ReactiveSecurityContextHolder is broken when used with Futures. It does not always provide results and sometimes just fires the onComplete signal.
Actual Behavior
Executes onComplete()
Expected Behavior
Should execute onNext()
Version
5.0.7 Release
Sample
@Test
public void testWorkingContext() {
Authentication authentication = new PreAuthenticatedAuthenticationToken("TEST", "");
Mono<String> working = ReactiveSecurityContextHolder.getContext()
.map(securityContext -> (String)securityContext.getAuthentication().getPrincipal());
Mono<String> stringMono = working.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
StepVerifier.create(stringMono).expectNext("TEST").verifyComplete();
}
@Test
public void testBrokenContext() {
Authentication authentication = new PreAuthenticatedAuthenticationToken("TEST", "");
Mono<String> working = ReactiveSecurityContextHolder.getContext()
.map(securityContext -> (String)securityContext.getAuthentication().getPrincipal());
Mono<String> broken = Mono.fromFuture(working.toFuture());
Mono<String> stringMono = broken.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
StepVerifier.create(stringMono).expectNext("TEST").verifyComplete();
}
Comment From: rwinch
Thanks for the report. If it is just an arbitrary Future I would not expect it to work. However, if the Future was produced from a Mono I would think it could work. If it were to work it would be a Project Reactor change.
What are your thoughts @simonbasle and @smaldini?
Comment From: dave-fl
@rwinch In my test, the CompletableFuture is produced from a Mono.
Comment From: rwinch
@dave-fl Right. This is why I didn't immediately close the issue since this seems somewhat reasonable to support to me.
In any case, this is something we will need Project Reactor to support. which is why I pinged Simon and Stephane. We will wait to see what their thoughts are.
Comment From: dave-fl
Another simple example.
@Test
public void simple() {
Authentication authentication = new PreAuthenticatedAuthenticationToken("SIMPLE SIMON", "");
Mono<String> context = ReactiveSecurityContextHolder.getContext().flatMap(securityContext -> {
return Mono.just((String)securityContext.getAuthentication().getPrincipal());
});
Mono<String> wrappedWithSubscribe = Mono.create(monoSink -> context.subscribe(s -> monoSink.success(s), e -> monoSink.error(e), () -> monoSink.success()));
Mono<String> addedContext = wrappedWithSubscribe
.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
StepVerifier.create(addedContext).expectNext("SIMPLE SIMON").verifyComplete();
}
Comment From: simonbasle
@dave-fl you are switching to a Future and then back to a Mono. The Context is purely a Reactor concept, and is propagated along Reactive Streams signals by Flux and Mono as long as you don't break the chain of operators (your second example with a subscribe breaks that assumption).
I gather you did that first test to simulate using an external Future-based API in the middle?
The MonoCompletionStage can probably be made aware of the Context of a MonoToFuture instance, but that would only help if you're guaranteed that external API doesn't wrap the Mono-produced Future, or change it in any way.
Comment From: dave-fl
@simonbasle Yes using Caffeine Async Cache - which uses futures. I don't believe it changes the future as its just storing what its being provided.
Comment From: dave-fl
Is there anything that can be done with the subscription to preserve the context?
Comment From: simonbasle
Is there anything that can be done with the subscription to preserve the context?
How do you mean? Context is accessed by an operator's internal Subscriber by looking at its immediate downstream Subscriber. If there is a stopgap in the reactive chain and there's no such Subscriber, then the Context cannot be accessed.
Comment From: dave-fl
@simonbasle I guess what I am asking you is that if one wanted to introduce their own Mono as I have done in this example, is there not a means to patch in that context so that chain is not broken e.g. do whatever it is that is normally done in the completion stage.
Comment From: simonbasle
The sink has a currentContext() method so you could do sink -> context.subscriberContext(sink.currentContext()).subscribe(...).
That said, subscribe inside create is an anti-pattern to me, and it achieves nothing. Probably just an attempt at reproducing the issue, but it fails by nature and not as a byproduct of a bug. (bit like saying "this code throws a NPE: throw new NullPointerException();" 😄).
Comment From: sepanniemi
The same problem seems to exist if I try to use SecurityContext in RxJava2 flow.
Single.fromPublisher(ReactiveSecurityContextHolder.getContext())
Is there any way to access the current security context if using RxJava data types in WebFlux RestController?
I tried also with RxJava2Adapter and with Mono.toFuture, as said above the context is visible only when staying purely in reactor flow.
Comment From: simonbasle
Nope, the reactive metadata Context only works as long as the reactive sequence is made of Reactor operators
Comment From: sepanniemi
I was able to transfer the SecurityContext to RxJava flow using reactor Hooks and custom operator which copies the SecurityContext from reactor context to ThreadLocal as Single
Comment From: lee-gilbert
Might this also provide a solution for retrieving the SecurityContext within Spring Data AuditorAware implementations?
Comment From: rwinch
@lee-gilbert I'm not sure. Can you provide more information?
Comment From: lee-gilbert
@rwinch It appears the issue is discussed without a solution here: https://jira.spring.io/browse/DATACMNS-1231
Comment From: mgumerov
Sorry, this might be a bit off topic but not by much because it questions if ReactiveSecurityContextHolder and Future are related at all.
So, in my project I subscribe to ReactiveSecurityContextHolder.getContext in a bean (i.e. the subscription outlives a single chain), like @Bean Mono
Doesn't that violate a Mono<> protocol? And anyway, clearly that is not compatible with Future<> is it?
Comment From: simonbasle
(sounds indeed a bit off-topic and potentially a separate issue / question, but here goes)
emits multiple values to the subscriber - although each chain only observes one value.
these statements seem to contradict each other. are you subscribing to the Mono multiple times?
the Mono contract of at most one onNext signal is like similar Reactive Streams contracts: it is only true in the context of a single Subscription - ie. for a specific Subscriber-Publisher relationship. A Mono/Publisher is like a Subscription factory, multiple subscriptions can happen.
Bear in mind that some Mono will explicitly reject more than one subscription (e.g. Monos representing a transient source that can be only read once, like a unique response in a Network I/O).
Others will conceptually re-generate the source data ("cold" Monos - e.g. higher level "remote call" => perform the request again) and other will cache or just complete empty to late subscribers.
You should definitely not disconnect the getContext Mono from the request chain and make it a Bean IMHO. If you're already doing that, I assume you have implemented another kind of scope yourself, but it better be solid. You might have overlooked the potential cold and deferred nature of the getContext Mono (if someone from the @spring-projects/spring-security team can confirm it is cold).