spring-security icon indicating copy to clipboard operation
spring-security copied to clipboard

WebTestClient authentication fails with form-data credentials

Open membersound opened this issue 3 years ago • 4 comments

spring-boot-2.6.3

I'm migrating my MockMvc tests to WebTestClient, for having all my tests using the same underlying API.

The following example project shows that authenticating on the /login page works with MockMvc, but does not with WebTestClient. In real world, I'm testing a ldap security configuration, but the issue is reproducible even with in-memory authentication.

This is a result result of https://github.com/spring-projects/spring-boot/issues/29825 (see the issue also for a full sample project attached)

I assume this is a bug, as authentication with MockMvc works flawless, and WebTestClient does not.

Tests:

@SpringBootTest
@AutoConfigureMockMvc
public class PersonControllerTest {
	@Autowired
	private MockMvc mockMvc;

	@Autowired
	private WebTestClient webTestClient;

	//works
	@Test
	public void testMockMvc() throws Exception {
		SecurityMockMvcRequestBuilders.FormLoginRequestBuilder login = formLogin()
				.user("junituser")
				.password("junitpw");

		 mockMvc.perform(login)
				.andExpect(authenticated().withUsername("junituser"));
	}

	//works
	@Test
	public void testMockMvcUnauthenticated() throws Exception {
		SecurityMockMvcRequestBuilders.FormLoginRequestBuilder login = formLogin()
				.user("junituser")
				.password("invalid");

		mockMvc.perform(login)
				.andExpect(unauthenticated());
	}

        //works
	@Test
	public void testRedirectToLoginPage() {
		webTestClient.get().uri("/").exchange().expectStatus().is3xxRedirection();
	}

        //works
	@Test
	public void testLoginPageAnonymous() {
		webTestClient.get().uri("/login").exchange().expectStatus().isOk();
	}

	//fails with 403 forbidden
	@Test
	public void testWebClient() {
		MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
		formData.add("username", "junituser");
		formData.add("password", "junitpw");

		webTestClient.post()
				.uri("/login") //the test would fail the same executed against '/example' @RestController
				.body(BodyInserters.fromFormData(formData))
				.exchange()
				.expectStatus()
				.isOk();
	}

	//throws NPE
	@Test
	public void testWebClientCsrf() {
		MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
		formData.add("username", "junituser");
		formData.add("password", "junitpw");

		//there is no FormLoginRequestBuilder for WebTestClient?
		webTestClient.mutateWith(csrf())
				.post()
				.uri("/login")
				.body(BodyInserters.fromFormData(formData))
				.exchange()
				.expectStatus()
				.isOk();
	}
}

Source:

@RestController
public class PersonController {
	@GetMapping("/example")
	public String example() {
		return "Authorized user";
	}

	@PostMapping("/example")
	public String examplePost() {
		return "Authorized user";
	}
}

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication()
				.withUser("junituser")
				.password("{noop}junitpw")
				.roles("USER");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.anyRequest().authenticated()
				.and()
				.formLogin().permitAll();
	}
}

@SpringBootApplication
public class MainApp {
	public static void main(String[] args) {
		SpringApplication.run(MainApp.class, args);
	}
}

membersound avatar Feb 16 '22 07:02 membersound

Hi @membersound. Thanks for the sample over in spring boot issue. I was able to pull in the project and see your errors. I am able to convert the simple project you provided to a Spring WebFlux project by removing the spring-boot-starter-web dependency and changing your SecurityConfig to use webflux support instead. When I do this, your WebTestClient tests work (though they do not pass, as the assertions are expecting 200 instead of 403 and 302 respectively). However, the mockmvc tests cannot work in the same project.

All that to say, I'm uncertain whether your scenario would be expected to work currently as I believe the WebTestClient support in Spring Security is designed to work with webflux, at least in a mock server setup. I'll ask the team if your scenario is supported and get back to you.

sjohnr avatar Feb 16 '22 22:02 sjohnr

Hi @membersound. Just to let you know, I've spoken with the team around this issue. A couple of takeaways:

  • It's not 100% clear yet whether Spring itself supports the scenario you are going for (WebTestClient + MockMvc integration tests in a servlet environment), so more research is required on that. It appears it may be, but I'll leave this issue open to research that and I welcome your input.
  • This is not currently supported in Spring Security, so we should throw an appropriate exception such as IllegalStateException instead of NullPointerException when one of the mutators such as csrf() is applied. I'll open a new issue for this change.
  • We should also document that it is not supported, and/or actually detect that WebHttpHandlerBuilder is missing and proactively throw an IllegalStateException during WebTestClient setup instead of waiting for one of the mutators such as csrf() to be applied. I'll create a new issue for this change as well.

sjohnr avatar Feb 22 '22 18:02 sjohnr

@sjohnr currently it is possible to use e.g. csrf with WebTestClient in a servlet environment via this workaround:

val webTestClient = MockMvcWebTestClient.bindToApplicationContext(webApplicationContext)
    .apply(SecurityMockMvcConfigurers.springSecurity())
    .defaultRequest(MockMvcRequestBuilders.get("/").with(SecurityMockMvcRequestPostProcessors.csrf()))
    .configureClient()
    .build()

If an exception is thrown proactively during WebTestClient setup would that mean that this code will not work anymore and all tests would have to be reverted back to using MockMvc directly?

mengelbrecht avatar Feb 25 '22 08:02 mengelbrecht

Thanks @mengelbrecht, that's definitely worth exploring. I'm less familiar with the test infrastructure, so thanks for pointing that out!

sjohnr avatar Feb 25 '22 17:02 sjohnr