Describe the bug
Authenticate an unauthorized Authentication using ServerSecurityContextRepository cause the Mono<SecurityContext> produced by its load method subscribed twice.
To Reproduce See the following config snippet:
@EnableWebFluxSecurity
public class TestSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http,
ServerSecurityContextRepository serverSecurityContextRepository) {
return http.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.securityContextRepository(serverSecurityContextRepository)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.build();
}
@Bean
public ServerSecurityContextRepository serverSecurityContextRepository() {
return new ServerSecurityContextRepository() {
@Override
public Mono<Void> save(ServerWebExchange exchange,
SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
// no authorities -> not authenticated
final Authentication authentication = new UsernamePasswordAuthenticationToken("test", "test");
return Mono.defer(() -> {
System.out.println("TestSecurityConfig: load"); // invoked twice
return Mono.just(new SecurityContextImpl(authentication));
});
}
};
}
}
The root cause is:
* ReactorContextWebFilter save the Mono of SecurityContext into the reactor context, rather than the actual value. The Mono is subscribed the first time in regular authentication process.
* The Mono in ExceptionTranslationWebFilter on AccessDeniedException resumes to retrieve the Principal, which leads to getting the Mono of SecurityContext (and subscribe it again) in SecurityContextServerWebExchange.
Expected behavior
I would expect the load method is only called once in this scenario. Not sure if the current behavior is intended, but it's quite confusing to me.
Perhaps saving Mono<SecurityContext> (opposed to saving the SecurityContext itself) is the root of the pain. I think keeping the Mono could lead to a more ambiguous lifecycle and might eventually harm the way to make Authentication immutable as well.
Sample See the snippet above. An empty project with such a security config and any request could trigger the issue.
Comment From: simonpai
Same issue seen on stackoverflow: https://stackoverflow.com/questions/60699614/webflux-authenticate-fail-cause-authenticate-called-twice
Comment From: rwinch
Thanks for the report.
Perhaps saving Mono
(opposed to saving the SecurityContext itself) is the root of the pain. I think keeping the Mono could lead to a more ambiguous lifecycle and might eventually harm the way to make Authentication immutable as well.
This is intentional to avoid calling load if no one needs the SecurityContext. We could potentially add an option to use Mono.cache() to have it only invoked once. Of course looking it up once has the drawback of potentially getting a stale value
Comment From: mafei-dev
after enabling @EnableReactiveMethodSecurity it is called 3 times.
Comment From: simonpai
@mafei-dev FYI, I worked around by passing in a cached Mono myself.
Comment From: rwinch
There is now an option to cache the result of WebSessionServerSecurityContextRepository which ensures that the logic inside the WebSessionServerSecurityContextRepository is only invoked once.