Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebTestClient authentication fails with form-data credentials #10841

Open
membersound opened this issue Feb 16, 2022 · 4 comments
Open

WebTestClient authentication fails with form-data credentials #10841

membersound opened this issue Feb 16, 2022 · 4 comments
Labels
for: team-attention This ticket should be discussed as a team before proceeding in: test An issue in spring-security-test type: bug A general bug

Comments

@membersound
Copy link

membersound commented Feb 16, 2022

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 spring-projects/spring-boot#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 membersound added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Feb 16, 2022
@sjohnr sjohnr added in: test An issue in spring-security-test and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 16, 2022
@sjohnr sjohnr self-assigned this Feb 16, 2022
@sjohnr
Copy link
Member

sjohnr commented Feb 16, 2022

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 sjohnr added the for: team-attention This ticket should be discussed as a team before proceeding label Feb 16, 2022
@sjohnr
Copy link
Member

sjohnr commented Feb 22, 2022

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.

@mengelbrecht
Copy link
Contributor

@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?

@sjohnr
Copy link
Member

sjohnr commented Feb 25, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: team-attention This ticket should be discussed as a team before proceeding in: test An issue in spring-security-test type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants