Search code examples
unit-testingspring-securityspring-webfluxcsrf

How to write Spring webflux integration test using CSRF


I am using reactive Spring Boot 3.4.3 with webflux. For easier setup, CSRF has been disabled .csrf(ServerHttpSecurity.CsrfSpec::disable). Then I turn it on in this way (because of expected integration with JavaScript frontend)

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    return http
            .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))

My integration tests start failing, which is expected.

403 FORBIDDEN Forbidden
An expected CSRF token cannot be found

So I added .mutateWith(csrf()) as suggested in the documentation https://docs.spring.io/spring-security/reference/reactive/test/web/csrf.html

It leads to

java.lang.NullPointerException: Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filters(java.util.function.Consumer)" because "httpHandlerBuilder" is null

    at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$CsrfMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:260)

That is a known error for non-reactive code, see csrf() doesn't work with WebTestClient in non-reactive code But as I stated before, my project is reactive.

The simplified test looks something like this

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class UserControllerTest {
    
    @Autowired
    private WebTestClient webTestClient;

    @Test
    @WithUserDetails("john.doe")
    void testDeleteUserAsOrganizationAdmin() {
        webTestClient
                .mutateWith(csrf())
                .post()
        // rest omitted for brevity
    }

So question is:

How to make Spring webflux integration test working with CSRF?


Solution

  • Update

    The issue is that the OP is missing the @AutoConfigureWebTestClient annotation.

    From WebFluxTest docs

    If you are looking to load your full application configuration and use WebTestClient, you should consider @SpringBootTest combined with @AutoConfigureWebTestClient rather than this annotation.

    Example Hello World test:

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @AutoConfigureWebTestClient
    class MyControllerTest {
    
        @Autowired
        WebTestClient webTestClient;
    
        @Test
        void postWithoutCsrf_expectForbidden() {
            webTestClient
                    .post()
                    .uri("/hello")
                    .exchange()
                    .expectStatus().isForbidden();
        }
    
        @Test
        void postWithCsrf_expectOk() {
            webTestClient
                    .mutateWith(csrf())
                    .post()
                    .uri("/hello")
                    .exchange()
                    .expectStatus().isOk()
                    .expectBody(String.class).isEqualTo("Hello World");
        }
    }
    

    Original answer

    A lightweight alternative for testing specific controllers, which is sufficient to address this issue, is to use @WebFluxTest with mocked dependencies. The SecurityWebFilterChain can be configured within a @TestConfiguration or imported using @Import(FluxSecurityConfig.class) (use the name for your config class).

    When commenting out .mutateWith(csrf()), you will get a 403 FORBIDDEN.

    @WebFluxTest(UserController.class)
    class UserControllerTest {
    
        @Autowired
        WebTestClient webTestClient;
    
        // use your actual service(s) here
        @MockitoBean
        MyService myService;
    
        @TestConfiguration
        @EnableWebFluxSecurity
        static class TestConfiguration {
    
            @Bean
            public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
                return http
                        .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
                        .authorizeExchange(exchanges -> exchanges.anyExchange().permitAll())
                        .build();
            }
        }
    
        @Test
        void testDeleteUserAsOrganizationAdmin() {
            // setup mocks
    
            webTestClient
                    .mutateWith(csrf())
                    .post()
                    // remaining code left out
    
            // verify mocks
        }
    
        // other tests here
    }