-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Description
Summary
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.