If a SecurityFilterChain is configured with a CookieCsrfTokenRepository, a CsrfTokenRequestAttributeHandler and the setCsrfRequestAttributeName of latter is set to null (for maintaining Spring Security 5 behavior regarding Csrf cookies according the migration documentation), MockMvc tests expecting a cookie with name "XSRF-TOKEN" fail.

The reason for that behavior can be found in the SecurityMockMvcRequestPostProcessors class:

public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
  CsrfTokenRepository repository = WebTestUtils.getCsrfTokenRepository(request);
  CsrfTokenRequestHandler handler = WebTestUtils.getCsrfTokenRequestHandler(request);
  if (!(repository instanceof TestCsrfTokenRepository)) {
    repository = new TestCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
    WebTestUtils.setCsrfTokenRepository(request, repository);
  }

The repository wrapped by the TestCsrfTokenRepository is always a HttpSessionCsrfTokenRepository, regardless that the initial repository type was a CookieCsrfTokenRepository.

Configuration:

@Bean
SecurityFilterChain test(HttpSecurity http) throws Exception {
  CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
  requestHandler.setCsrfRequestAttributeName(null);
  http
    .securityMatcher("/test")
    .csrf(csrfConfigurer -> csrfConfigurer
      .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
      .csrfTokenRequestHandler(requestHandler))
    ...
  return http.build();
}

Test Setup:

mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
....
@Test
void test() throws Exception {
  mockMvc
    .perform(get("/test"))
      .andExpect(status().isUnauthorized())
      .andExpect(header().string("WWW-Authenticate", "Basic realm=\"rm\""))
      .andExpect(cookie().exists("XSRF-TOKEN"))
      .andExpect(cookieHttpOnly("XSRF-TOKEN", false));
}

Comment From: marcusdacoregio

Hi @smotron.

The documentation says that the CSRF token won't be loaded until it is needed:

In Spring Security 6, the default is that the lookup of the CsrfToken will be deferred until it is needed.

The CsrfToken is needed whenever a request is made with an HTTP verb that would change the state of the application. This is covered in detail in Safe Methods Must be Idempotent. Additionally, it is needed by any request that renders the token to the response...

In your test, you are performing a GET request, this is a safe HTTP method and, therefore, the CSRF token won't be loaded. If we change the test to make a POST, it will pass:

mockMvc
    .perform(post("/test"))
    .andExpect(status().isForbidden())
    .andExpect(cookie().exists("XSRF-TOKEN"))
    .andExpect(cookie().httpOnly("XSRF-TOKEN", false));

You might want to create a new endpoint to retrieve the /csrf token before making an unsafe HTTP request, please refer to this section of the docs.

I don't think I follow this:

The reason for that behavior can be found in the SecurityMockMvcRequestPostProcessors class:

The CsrfRequestPostProcessor#postProcessRequest is only invoked if you are calling the SecurityMockMvcRequestPostProcessors#csrf method, I don't see this being invoked in your test.

I'll close this as solved, feel free to continue discussing if you need further clarifications.