In Test @AuthenticationPrincipal is null because ServerWebExchange is not wrapped

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:1
  • Comments:14 (13 by maintainers)

github_iconTop GitHub Comments

1reaction
Dav1ddecommented, Oct 26, 2019

Yes, exactly, the security chain will set the correct principal. The requests in your tests are not going through any of the spring security filter chain because they do not match any of your custom matchers.

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.

@PreAuthorize is not behaving correctly. If you try out a Servlet application with the same configuration (mockUser(), but no security chain matching the request) then @PreAuthorize will prevent the request.

But that sounds like an issue with mockUser() to me, PreAuthorize does 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 PreAuthorize if 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.

0reactions
mindhaqcommented, Sep 28, 2020

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Spring WebFlux Request Principal is null in test - Stack Overflow
We're relying on the principal in the request for user information for the log, but during the test, it is always null.
Read more >
ServerWebExchange.getSession - Java - Tabnine
Return a mutable map of request attributes for the current exchange. getAttribute · mutate. Return a builder to mutate properties of this exchange...
Read more >
Spring Security changelog - Awesome Java - LibHunt
... In Test @AuthenticationPrincipal is null because ServerWebExchange is not wrapped #6598; Make MethodSecurityEvaluationContext Delegates to ...
Read more >
Spring Security Reference
that is not mapped (including a null id) will result in an IllegalArgumentException. ... Since FilterChainProxy is a Bean, it is typically wrapped...
Read more >
spring-projects/spring-security 5.3.0.M1 on GitHub
... should not use locked dependencies #7798; Add oauth2Login MockMvc Test ... @AuthenticationPrincipal is null because ServerWebExchange is not wrapped # ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found