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

mockJwt() WebTestClientConfigurer with MockMvcWebTestClient throws a NullPointerException. #9257

Open
making opened this issue Dec 3, 2020 · 18 comments
Assignees
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug

Comments

@making
Copy link
Member

making commented Dec 3, 2020

Describe the bug
mockJwt() WebTestClientConfigurer does not seem to work with MockMvcWebTestClient that was introduced in Spring 5.3 as documented.

To Reproduce

	@Test
	void getMessagesWebTestClient() {
		final WebTestClient testClient = MockMvcWebTestClient.bindTo(this.mockMvc)
				.build();
		testClient.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read")))
				.get()
				.uri("/messages")
				.exchange()
				.expectStatus().isOk()
				.expectBody()
				.jsonPath("$[0]").isEqualTo("hello")
				.jsonPath("$[1]").isEqualTo("world"); ;
	}

throws the exception below

java.lang.NullPointerException: Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filter(org.springframework.web.server.WebFilter[])" because "httpHandlerBuilder" is null

	at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$JwtMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:540)
	at org.springframework.test.web.reactive.server.DefaultWebTestClientBuilder.apply(DefaultWebTestClientBuilder.java:247)
	at org.springframework.test.web.reactive.server.DefaultWebTestClient.mutateWith(DefaultWebTestClient.java:160)
	at com.example.demojwttest.MessageControllerTest.getMessagesWebTestClient(MessageControllerTest.java:51)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:564)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

while the equivalent following tests work

// MockMvc
	@Test
	void getMessages() throws Exception {
		this.mockMvc.perform(get("/messages")
				.with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read"))))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$[0]").value("hello"))
				.andExpect(jsonPath("$[1]").value("world"));
	}

// @WithMockUser
	@Test
	@WithMockUser(authorities = "SCOPE_message:read")
	void getMessagesWebTestClientWithMockUser() {
		final WebTestClient testClient = MockMvcWebTestClient.bindTo(this.mockMvc)
				.build();
		testClient.get()
				.uri("/messages")
				.exchange()
				.expectStatus().isOk()
				.expectBody()
				.jsonPath("$[0]").isEqualTo("hello")
				.jsonPath("$[1]").isEqualTo("world"); ;
	}

Expected behavior

The first example works

Sample
https://github.com/making/spring-security-gh-9257

@making making added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Dec 3, 2020
@jzheaux
Copy link
Contributor

jzheaux commented Dec 3, 2020

While there may be a wider picture to consider, the basic reason for the NPE is because MockMvcWebTestClient.bindTo ultimately wires WebTestClient with a ClientHttpConnector instead of a WebHttpHandlerAdapter. Spring Security uses this adapter in order to introduce WebFilters into the mock client.

@rwinch
Copy link
Member

rwinch commented Dec 4, 2020

UPDATED: Fixed workaround, demo CSRF support workaround, and provide link to complete example.

@making Thanks for the report. We will look into a proper solution.

In the meantime, you can work around it using TestSecurityContextHolder.setAuthentication(Authentication). I put together a complete example in the gh-9257-webtestclient branch of my sample repository. You can see an excerpt below :

@SpringBootTest
@AutoConfigureMockMvc
public class WebTestClientTest {
	WebTestClient client;

	@MockBean
	// mock the JwtDecoder so that the jwks is not resolved since no AuthZ Server Setup
	JwtDecoder jwtDecoder;

	// Override the CsrfTokenRepository. Must explicitly wire CsrfTokenRepository Bean into DSL for this to work
	@MockBean
	CsrfTokenRepository csrfTokenRepository;

	DefaultCsrfToken csrf = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "123");

	@Autowired
	void setMockMvc(MockMvc mockMvc) {
		this.client = MockMvcWebTestClient.bindTo(mockMvc)
				.build();
	}

	@BeforeEach
	void setupCsrf() {
		given(this.csrfTokenRepository.generateToken(any())).willReturn(csrf);
	}

	private Consumer<HttpHeaders> csrf() {
		return (headers) -> headers.set(csrf.getHeaderName(), csrf.getToken());
	}

	@Test
	void getWhenAuthenticated() {
		TestSecurityContextHolder.setAuthentication(jwtAuthenticationToken());

		client
				.get()
				.uri("/")
				.exchange()
				.expectStatus().isOk();
	}

	@Test
	void getWhenNotAuthenticated() {
		client
				.get()
				.uri("/")
				.exchange()
				.expectStatus().is4xxClientError();
	}

	@Test
	void csrfWhenNoToken() {
		TestSecurityContextHolder.setAuthentication(jwtAuthenticationToken());

		client
				.post()
				.uri("/")
				.exchange()
				.expectStatus().is4xxClientError();
	}

	@Test
	void csrfWhenValidToken() {
		TestSecurityContextHolder.setAuthentication(jwtAuthenticationToken());

		client
				.post()
				.uri("/")
				.headers(csrf())
				.exchange()
				.expectStatus().isOk();
	}

	private static JwtAuthenticationToken jwtAuthenticationToken() {
		return new JwtAuthenticationToken(jwt().build(), AuthorityUtils.createAuthorityList("SCOPE_message:read"));
	}

	public static Jwt.Builder jwt() {
		// @formatter:off
		return Jwt.withTokenValue("token")
				.header("alg", "none")
				.audience(Arrays.asList("https://audience.example.org"))
				.expiresAt(Instant.MAX)
				.issuedAt(Instant.MIN)
				.issuer("https://issuer.example.org")
				.jti("jti")
				.notBefore(Instant.MIN)
				.subject("mock-test-subject");
		// @formatter:on
	}
}

@rwinch
Copy link
Member

rwinch commented Dec 4, 2020

@rstoyanchev Any ideas on how we can get Spring Security to integrate with the MockMvcWebTestClient support? With WebTestClient we typically add a WebFilter which can access the attribute on ServerWebExchange and that sets up the context, but it doesn't appear there is a way to do this when using MockMvcWebTestClient.

We need to be able to access the attribute on the ServerWebExchange that is populated by the mutateWith method and use it to setup the SecurityContext.

@rstoyanchev
Copy link
Contributor

I think there is some misunderstanding. As of 5.3 WebTestClient can be used to exercise not only WebFlux but also WebMvc controllers with MockMvc as the server. In other words it's all MockMvc, and unrelated to WebFlux, and therefore any WebFlux related hooks do not apply.

For the most part what can be done directly with MockMvc can also be done via MockMvcWebTestClient. For example, you can configure and apply extension hooks to MockMvc just the same. However since it it a client, it is not as easy to modify individual requests with some special support via server side Filter. I can work with you more closely if we want to find a way to improve that.

For further reference the sections on WebTestClient and MockMvc have been updated to reflect this. You can also see all the framework samples tests for MockMvc ported to use with WebTestClient.

@gursahibsahni
Copy link

@making Thanks for the report. We will look into a proper solution. In the meantime, you can work around it using:

@Test
	void getMessagesWebTestClient() {
		TestingAuthenticationToken authentication = new TestingAuthenticationToken("a", "b", "SCOPE_message:read");
		TestSecurityContextHolder.setAuthentication(authentication);
		final WebTestClient testClient = MockMvcWebTestClient.bindTo(this.mockMvc)
				.build();
		testClient.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_message:read")))
				.get()
				.uri("/messages")
				.exchange()
				.expectStatus().isOk()
				.expectBody()
				.jsonPath("$[0]").isEqualTo("hello")
				.jsonPath("$[1]").isEqualTo("world"); ;
	}

This still doesn't work for me.

@membersound
Copy link

Similar also happens in a test with @Autowired WebTestClient webTestClient and executing a webTestClient.mutateWith(csrf()).post()...:

java.lang.NullPointerException
	at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$CsrfMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:259)
	at org.springframework.test.web.reactive.server.DefaultWebTestClientBuilder.apply(DefaultWebTestClientBuilder.java:265)
	at org.springframework.test.web.reactive.server.DefaultWebTestClient.mutateWith(DefaultWebTestClient.java:167)

@soasada
Copy link

soasada commented Mar 22, 2022

I'm experiencing the same behaviour than @membersound, do you have any workaround?

@rwinch
Copy link
Member

rwinch commented May 5, 2022

@gursahibsahni Sorry the workaround I posted previously, should have removed the mutateWith method. I've updated the example and provided a link to a repository that demonstrates the workaround in its entirety.

@soasada @membersound I've updated the sample above to demonstrate how to workaround CSRF test support not working too.

@rstoyanchev Thanks for the reply. I'd like to figure out a way that Spring Security users can use WebTestClient using the same APIs for WebMvc and WebFlux backends. It is confusing that mutateWith does not work for MockMvc based tests. What's more is there is currently no way to use many of Spring Security's test features when WebTestClient + MockMvc.

@nasrmohammad4804
Copy link

nasrmohammad4804 commented Oct 28, 2022

hi i have same issue with webTestClient and get null pointer exception i think because don't use jwt for webTestClient
and i solved this with add @AutoConfigureWebTestClient as class level annotation for integration test
198698140-27aa94c0-631d-4c26-b9d6-9f71cfb758c8

@eiswind
Copy link

eiswind commented Mar 24, 2023

The suggested workaround does not work with the current spring security versions (6.0.2), as csrf token handling has changed quite a bit. So far I was not able to create a working solution.

@siaavush
Copy link

siaavush commented Mar 25, 2023

as @eiswind mentioned, I have the same issue with spring boot 3 and security 6 this is my test class:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@AutoConfigureWireMock(stubs = ["classpath:/mappings/authservice"], port = 0)
@TestPropertySource(
    properties = [
        "spring.flyway.enabled=false",
    ],
)
@Testcontainers
@ExtendWith(SpringExtension::class)
@ActiveProfiles("test")
@AutoConfigureWebTestClient
class ServiceIntegrationTest {
@Test
    fun test() {
           webTestClient.mutateWith(mockJwt().jwt(JwtUtils.generateJwt()))
            .mutateWith(csrf())
            .post()
            .uri("/api/planning/stores/$businessUnitId/units")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON)
            .exchange()
            .expectStatus().isForbidden
     }
}

and this is the error I am getting:

Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filter(org.springframework.web.server.WebFilter[])" because "httpHandlerBuilder" is null
java.lang.NullPointerException: Cannot invoke "org.springframework.web.server.adapter.WebHttpHandlerBuilder.filter(org.springframework.web.server.WebFilter[])" because "httpHandlerBuilder" is null
	at org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$JwtMutator.afterConfigurerAdded(SecurityMockServerConfigurers.java:540)

none of the workarounds seem effective on it

@justin-tay
Copy link
Contributor

It's currently not possible to do this cleanly with MockMvc and using the mutateWith api because the functionality is in MockMvcHttpConnector and the WebTestClient.Builder does not expose a method to set a modified MockMvcHttpConnector as the ClientHttpConnector is final in the builder.

The best workaround currently if you still want to use WebTestClient instead of just using the MockMvc API is #9304 (comment).

Some ideas

  • The WebTestClient.Builder needs to expose a method to set the ClientHttpConnector or some ClientHttpConnectorBuilder
  • The MockMvcHttpConnector needs to store the RequestPostProcessors to be applied on the MockHttpServletRequestBuilder and a means to create a new MockMvcHttpConnector with the existing RequestPostProcessors and additional ones
  • The MockMvcHttpConnector needs to apply the RequestPostProcessors in adaptRequest.

@justin-tay
Copy link
Contributor

justin-tay commented Mar 28, 2023

As a workaround I wrote a simple builder which can be used with the org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.

Edit: Have edited the builder as when I was doing testing I realised that what I typically wanted to use was the filters customised by Spring Boot in the MockMvcBuilder and not binding to the WebApplicationContext with springSecurity.

package com.example;

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import org.springframework.http.HttpMethod;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder;
import org.springframework.web.context.WebApplicationContext;

public class DefaultMockMvcWebTestClient implements WebTestClient {
	private final Function<MockHttpServletRequestBuilder, WebTestClient> builder;
	private final List<RequestPostProcessor> requestPostProcessors = new ArrayList<>();

	public DefaultMockMvcWebTestClient(WebApplicationContext context) {
		this.builder = (requestBuilder) -> MockMvcWebTestClient.bindToApplicationContext(context)
				.apply(springSecurity()).defaultRequest(requestBuilder);
	}

	public DefaultMockMvcWebTestClient(AbstractMockMvcBuilder<?> mockMvcBuilder) {
		this.builder = (requestBuilder) -> MockMvcWebTestClient
				.bindTo(mockMvcBuilder.defaultRequest(requestBuilder).build()).build();
	}

	public DefaultMockMvcWebTestClient(Function<MockHttpServletRequestBuilder, WebTestClient> builder) {
		this.builder = builder;
	}

	public DefaultMockMvcWebTestClient(DefaultMockMvcWebTestClient copy) {
		this.builder = copy.builder;
		this.requestPostProcessors.addAll(copy.requestPostProcessors);
	}

	public DefaultMockMvcWebTestClient with(RequestPostProcessor requestPostProcessor) {
		this.requestPostProcessors.add(requestPostProcessor);
		return this;
	}

	public DefaultMockMvcWebTestClient mutateWith(RequestPostProcessor requestPostProcessor) {
		DefaultMockMvcWebTestClient copy = new DefaultMockMvcWebTestClient(this);
		return copy.with(requestPostProcessor);
	}

	public WebTestClient build() {
		MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/");
		requestPostProcessors.stream().forEach(requestBuilder::with);
		return this.builder.apply(requestBuilder);
	}

	@Override
	public RequestHeadersUriSpec<?> get() {
		return build().get();
	}

	@Override
	public RequestHeadersUriSpec<?> head() {
		return build().head();
	}

	@Override
	public RequestBodyUriSpec post() {
		return build().post();
	}

	@Override
	public RequestBodyUriSpec put() {
		return build().put();
	}

	@Override
	public RequestBodyUriSpec patch() {
		return build().patch();
	}

	@Override
	public RequestHeadersUriSpec<?> delete() {
		return build().delete();
	}

	@Override
	public RequestHeadersUriSpec<?> options() {
		return build().options();
	}

	@Override
	public RequestBodyUriSpec method(HttpMethod method) {
		return build().method(method);
	}

	@Override
	public Builder mutate() {
		return build().mutate();
	}

	@Override
	public WebTestClient mutateWith(WebTestClientConfigurer configurer) {
		return build().mutateWith(configurer);
	}
}

It can be used something like

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;

public class MyTest {
	DefaultMockMvcWebTestClient webTestClient;

	@Autowired
	public void setupClient(AbstractMockMvcBuilder<?> mockMvcBuilder) {
		this.webTestClient = new DefaultMockMvcWebTestClient(mockMvcBuilder);
	}

	@Test
	public void test() {
		this.webTestClient.mutateWith(oidcLogin()).get() // ...
	}
	// ...
}

@rstoyanchev
Copy link
Contributor

@rwinch as per my spring-projects/spring-framework#30233 (review), I think we could do something with the WebTestClient#mutate(WebTestClientConfigurer) hook. For WebFlux use, I believe Spring Security is using the WebHttpHandlerBuilder argument to modify the server. For WebMvc use, it would use the ClientHttpConnector argument which would be the MockMvcClientHttpConnector. We'll need to make some changes in Spring Framework to allow MockMvcClientHttpConnector to be mutated and that should make it possible for Spring Security to address this issue.

@IsaacHu
Copy link

IsaacHu commented Jan 3, 2024

Any target date for the final solution of this issue?
We also suffer in this issue when using WebTestClient to test Spring MVC after involved security.

@WebMvcTest(controllers = XxxController.class)
@WithMockUser
class XxxControllerTest {

    @Autowired
    private MockMvc mockMvc;

    private WebTestClient webTestClient;

    @BeforeEach
    void setupWebClient() {
        webTestClient = MockMvcWebTestClient
                .bindTo(mockMvc)
                .build()
// will encounter NPE if -> .mutateWith(csrf())
        ;
    }

@glorfidev
Copy link

hi i have same issue with webTestClient and get null pointer exception i think because don't use jwt for webTestClient and i solved this with add @AutoConfigureWebTestClient as class level annotation for integration test 198698140-27aa94c0-631d-4c26-b9d6-9f71cfb758c8

Could you provide a source code? I think that @AutoConfigureWebTestClient should not have an impact on the configuration of webtestclient if SpringBootTest is used with RANDOM_PORT

@Bourg
Copy link

Bourg commented Apr 11, 2024

We just worked our way through this same rabbit hole at my job and came up with a solution that works in the latest versions.

For us, the key pieces were:

  1. Configure your own WebTestClient in the way described below - do not rely on the WebTestClient that is auto-configured with @SpringBootTest(webEnvironment = RANDOM_PORT) or similar
  2. Use the Spring Security test context annotations to mock the security context - do not try to use .mutateWith on the WebTestClient

Set your WebTestClient up like this in your test class:

    private WebTestClient webTestClient;

    @Autowired
    public void setWebApplicationContext(final WebApplicationContext context) {
        webTestClient = MockMvcWebTestClient
                .bindToApplicationContext(context)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

this portion of the solution was found in #9304 (comment)

Then, you can annotate your test classes / methods with @WithMockUser or any custom annotation created with @MockSecurityContext + a corresponding implementation of WithSecurityContextFactory (see https://docs.spring.io/spring-security/reference/servlet/test/method.html#test-method-withsecuritycontext)

Full working test class calling a controller that echoes the principal's name:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebTestClientMockUserTest {
    private WebTestClient webTestClient;

    @Autowired
    public void setWebApplicationContext(final WebApplicationContext context) {
        webTestClient = MockMvcWebTestClient
                .bindToApplicationContext(context)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @Test
    @WithMockUser("Austin")
    void testWithMockUser() {
        webTestClient
                .get()
                .uri("/whoami")
                .exchange()
                .expectStatus()
                .isOk()
                .expectBody(String.class)
                .isEqualTo("You are Austin");
    }
}

I think the thing that is causing everyone the most trouble is that you need to mock the security in a WebMVC-first way, not the WebFlux-first way exposed on WebTestClient.mutateWith(), which as all the above comments point out is only designed to operate on the reactive security stack.

@javolek
Copy link

javolek commented Aug 30, 2024

Since version 6.1 there seems to be a possibility to use RequestPostProcessor of MockMvc for a MockMvcHttpConnector inside WebTestClientConfigurer - see: spring-projects/spring-framework#31298. So it is possible to solve it the following way (kotlin):

    fun WebTestClient.mutateWith(rpp: RequestPostProcessor) =
        this.mutateWith { builder, _, connector ->
            (connector as? MockMvcHttpConnector)?.let { mockMvcConnector ->
                builder.clientConnector(mockMvcConnector.with(listOf(rpp)))
            }
    }

This is simple kotlin extension function to WebTestClient allowing passing RequestPostProcessor instace (i.e. SecurityMockMvcRequestPostProcessors.jwt) to mutateWith method. In Java one would need to implement WebTestClientConfigurer. Is there a simpler way to do it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug
Projects
None yet
Development

No branches or pull requests