banterCZ
banterCZ

Reputation: 1871

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?

Upvotes: 0

Views: 51

Answers (1)

Roar S.
Roar S.

Reputation: 11319

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
}

Upvotes: 1

Related Questions