3

I have the version 5.6.10 in the following dependencies

  • spring-security-test
  • spring-security-core
  • spring-security-web

I have a controller with CSRF

@GetMapping(value = "/data")
public ResponseEntity<DataResponse> data(@RequestParam(required = false) Double param, CsrfToken token){
    ...
}

I have a JUnit test that was working before adding the , CsrfToken token to Repository.

@WebMvcTest(controllers = Controller.class, excludeAutoConfiguration = {SecurityAutoConfiguration.class})
@ContextConfiguration(classes = {Controller.class, TestConfiguration.class})
class ControllerTest {

    @Autowired private MockMvc mockMvc;


    @Test
    void test() throws Exception {
    
        mockMvc.perform(get("/.../data?param=2.0")
                    .contextPath("/CONTEXT").servletPath("/.../data")
                    .contentType(MediaType.APPLICATION_JSON)
            )
            .andExpectAll(
                status().isOk(),
                ...
            )
            .andReturn();
    }
}

@WebAppConfiguration
@EnableWebMvc
public class TestConfiguration {

    @Bean
    ReactjsControllerExceptionHandler reactjsControllerExceptionHandler() {
        return new ControllerExceptionHandler(); // is a @ControllerAdvice that extends ResponseEntityExceptionHandler. I think it does not matter for this case.
    }
}

I am getting

No primary or single unique constructor found for interface org.springframework.security.web.csrf.CsrfToken

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: No primary or single unique constructor found for interface org.springframework.security.web.csrf.CsrfToken

    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:502)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:596)
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:201)
Caused by: java.lang.IllegalStateException: No primary or single unique constructor found for interface org.springframework.security.web.csrf.CsrfToken
    at org.springframework.beans.BeanUtils.getResolvableConstructor(BeanUtils.java:268)
    at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.createAttribute(ModelAttributeMethodProcessor.java:219)
    at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.createAttribute(ServletModelAttributeMethodProcessor.java:85)
    at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:147)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    ... 87 more

I already tried these options:

  • mockMvc.perform(get(...).with(csrf()))
  • Setting csfrToken as attribute:
.with(request -> {
    request.setAttribute(CsrfToken.class.getName(), csrfToken);
    return request;
})
  • Mocking with Mockito
CsrfToken csrfToken = Mockito.mock(CsrfToken.class);
Mockito.when(csrfToken.getToken()).thenReturn("myToken");

I am getting always the same error, what can I do?

Olivier
  • 13,283
  • 1
  • 8
  • 24
rMonteiro
  • 1,371
  • 1
  • 14
  • 37
  • Please add your full test to the question. – M. Deinum Apr 18 '23 at 08:05
  • `spring` tells you: hey! I have no idea to create instance of `CsrfToken` using data coming with http request (read: `spring` does not support mapping `CsrfToken` to controller method arguments, that is actually reasonable since `CsrfToken` does not contain useful information). So, how does your Q relate to unit testing? – Andrey B. Panfilov Apr 18 '23 at 12:49
  • @M.Deinum I added more code. – rMonteiro Apr 18 '23 at 14:12
  • @AndreyB.Panfilov because this work in the real application, the problem is only in JUnit tests – rMonteiro Apr 18 '23 at 14:12
  • So you add a security feeature, but disable security and thus the integration for CSRF token injection into the controller. ou are also using Spring Boot, so ditch the `@EnableWebMvc` as that will disable large parts of the auto configuration. – M. Deinum Apr 18 '23 at 17:44
  • 2
    Why are you requiring a token in a `GET` method? Usually, those methods should be safe and thus not require such protection. Why are you injecting the `CsrfToken` instead of letting Spring Security check it? – Marcus Hert da Coregio Apr 19 '23 at 14:07

1 Answers1

2

Quite sure, that spring-security does not "csrf protect" GET endpoints "by default" (extend the later mentioned test with csrf "expectations" they won't hold/invalid csrf won't have effect)

Nevertheless, some "magic" makes it possible to "populate" CsrfToken param/attribute (also in spring-web(-security!) GET requests) ...

Sorry, can only partially reproduce!

  • starter used: boot:2+web+security(+devtools)

    (For "best version match" use:

    • parent:2.6.14
    • and <spring-security.version>5.6.10</spring-security.version> (property)

    )

  • Sa/imple controller (in child package of main class/root package):

    @RestController
    public class MyController {
       @GetMapping(value = "/data")
       public ResponseEntity<String> data(
         @RequestParam(required = false) Double param
       ) {
         return ResponseEntity.ok("Hello");
       }
    }
    
  • protected by spring-boot-starter-security (defaults)

  • (a "comparable", realistic, security-integrating, suceeding) Test:

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.http.MediaType;
    import org.springframework.security.test.context.support.WithMockUser;
    import org.springframework.test.web.servlet.MockMvc;
    import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
    @WebMvcTest(controllers = MyController.class)
    class MyControllerTest {
    
       @Autowired
       private MockMvc mockMvc;
    
       @Test
       void testUnatuhorized() throws Exception {
          mockMvc.perform(
                  get("/data?param=2.0")
                          .contentType(MediaType.APPLICATION_JSON))
                  .andExpect(
                          status().isUnauthorized()); // 1.
       }
    
       @Test
       @WithMockUser // 2.
       void testAuthorized1() throws Exception {
          mockMvc.perform(
                  get("/data?param=2.0")
                          .contentType(MediaType.APPLICATION_JSON))
                  .andExpectAll(
                          status().isOk(),
                          content().string("Hello"));
       }
    
       @Test
       void testAuthorized2() throws Exception {
          mockMvc.perform(
                  get("/data?param=2.0")
                          .with(user("user")) // 3.
                          .contentType(MediaType.APPLICATION_JSON))
                  .andExpectAll(
                          status().isOk(),
                          content().string("Hello"));
       }
    }
    
  • Modifying controller to:

    import org.springframework.security.web.csrf.CsrfToken;
    //...
        @GetMapping(value = "/data")
        public ResponseEntity<String> data(
          @RequestParam(required = false) Double param,
          CsrfToken token /*!!*/) {
              System.err.println(token); // !
              return ResponseEntity.ok("Hello");
        }
    

    Doesn't break the test!

  • But excludeAutoConfiguration = {SecurityAutoConfiguration.class} Does! (with exact same exception cause/message!;) :

    org.springframework.web.util.NestedServletException:
       Request processing failed; nested exception is java.lang.IllegalStateException: 
         No primary or single unique constructor found for interface org.springframework.security.web.csrf.CsrfToken ...
    

So the issue/solution must be in the missing SecurityAutoConfiguration:your-version/ your custom TestConfiguration.

... (digging source code) (CsrfToken, LazyCsrfTokenRepository, SecurityAutoConfiguration, EnableWebSecurity, ...)

You miss a "csrf configuration" in your (test) context, which could be eligible to populate/resolve these arguments!!


Possible fixes:

  • don't excludeAutoConfiguration = {SecurityAutoConfiguration.class} (on your @WebMvc-/SpringBoot-Test)
  • alternatively: Add @EnableWebSecurity annotation to your TestConfiguration.

Both configure a CsrfTokenRepository in your (test) context, which is capable of populating/resolving these "request mapping arguments"...

  • ideally you should use a "real(istic)" security configuration for "integration tests".
  • ...
xerx593
  • 12,237
  • 5
  • 33
  • 64
  • Do you know if is there any way to ignore it or mock the CsrfToken? I didn't want to add the security configuration in this test class, I would have to add authentication and authorization configuration and that is tested in another part of the project. – rMonteiro Apr 23 '23 at 13:38
  • 1
    With an integration (mockmvc/full) test: no chance! You have to provide minimal/real csrf config... – xerx593 Apr 23 '23 at 15:50
  • minimal: `@EnableWebSecurity` on test config!? – xerx593 Apr 23 '23 at 15:55