In Test @AuthenticationPrincipal is null because ServerWebExchange is not wrapped
See original GitHub issueSummary
Using @WithMockUser or org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers#mockAuthentication directly in combination with @AuthenticationPrincipal in a WebFlux controller leaves the principal empty while things like @PreAuthorize('isAuthenticated()') work as expected.
Actual Behavior
Mocking the principal in a test does not work with @AuthenticaitonPrincipal.
Because my SecurityFilterChain does not match the request of the web test client (see below, only matches Requests that contain an Authorization: Basic header), the org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter gets never called (which is part of the SecurityFilterChain), meaning the request is never wrapped with a org.springframework.security.web.server.context.SecurityContextServerWebExchange.
org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver#resolveArgument uses exchange.getPrincipal() to resolve the principal, which returns an empty Mono because the exchange is still the DefaultServerWebExchange (not security!).
Now the principal set in org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.MutatorFilter is not considered.
The org.springframework.security.access.prepost.PrePostAdviceReactiveMethodInterceptor uses org.springframework.security.core.context.ReactiveSecurityContextHolder#getContext which would work for the resolver as well.
Expected Behavior
The mocked principal should be resolved correctly for the @AuthenticationPrincipal argument.
The exchange should be wrapped correctly.
Configuration
WebTestClient (minimal)
this.webTestClient = WebTestClient.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.mutateWith(mockUser(someUser))
Security (minimal):
@Configuration
public static class BasicAuthSecurity {
@Bean
public SecurityWebFilterChain basicAuthSecurityWebFilterChain(ServerHttpSecurity http) {
return http
.securityMatcher(new BasicAuthWebExchangeMatcher())
.authorizeExchange().anyExchange().permitAll().and()
.headers().frameOptions().disable().and()
.csrf().disable()
.logout().disable()
.httpBasic().securityContextRepository(NoOpServerSecurityContextRepository.getInstance()).and()
.exceptionHandling()
.accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.UNAUTHORIZED))
.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)).and()
.build();
}
}
public class BasicAuthWebExchangeMatcher implements ServerWebExchangeMatcher {
@Override
public Mono<MatchResult> matches(ServerWebExchange exchange) {
var header = exchange.getRequest().getHeaders().getFirst("Authorization");
return StringUtils.startsWithIgnoreCase(header, "Basic")
? MatchResult.match() : MatchResult.notMatch();
}
}
The important part here is the security matcher, which makes it so my Security chain does not match, nor does any other!
Version
Spring Security 5.1.4
Sample
Company code.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:1
- Comments:14 (13 by maintainers)
Top Related StackOverflow Question
What I wanted to explain here is, if the security chain is meant to set the principal, then way does
mockUser()even exist? It completely bypasses the security and sets the principal/authentication (mockAuthentication()exists) on the context. And there is no case where you should already have an authetnication before entering the security chain (at least to my knowledge).Maybe the right thing here is to not use
mockUser()at all.But that sounds like an issue with
mockUser()to me,PreAuthorizedoes what it should, read the security context from the Mono/Flux subscriber context and prevent entry.mockUser()sets that subscriber context.Imo the thing where you really have to be careful here is to not break
PreAuthorizeif you’re not within a web context. It still has to work without an exchange or a security chain, simply by using the supplied security context.I came here after googling why a WebClient mutated with mockUser() still resulted in request.principal() to be null.
I’m unit testing a reactive router function, so security testing would be outside of the scope. The test is about the route returning a response depending on what kind of principal is there, but not how that would be inserted into the request (that is tested in a more integrating test).
I will try a custom mutator for my case.