Spring unit test MockMvc fails when using custom filter in Spring Security
Asked Answered
A

2

13

I have a web application that should only be callable from specific IP addresses. Other than that, there is no need for authentication or for authorization; if you're coming from the right IP, you can see everything.

To that end, searching StackOverflow and other places, I found a number of suggestions for filtering requests by IP address in Spring Security. They all took this form (extending WebSecurityConfigurerAdapter using java configuration):

http.authorizeRequests().anyRequest().access("hasIpAddress('127.0.0.1/0')");

However, that never worked for me; it never rejected any request, no matter what IP address I made the request from. Instead, I implemented my IP filtering with a custom filter like this:

@Configuration
@EnableWebSecurity
@PropertySource("classpath:config.properties")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @Autowired
    private Environment env;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        String ipRange = env.getRequiredProperty("trusted_ips");
        logger.info("@@@@@ SETTING UP SECURITY CONFIGURATION @@@@@@@@@@@@@");
        logger.info("@@ trusted_ips: " + ipRange);
        logger.info("@@@@@ SETTING UP SECURITY CONFIGURATION - END @@@@@@@@@@@@@");
        http.addFilterBefore(new IPSecurityFilter(ipRange), J2eePreAuthenticatedProcessingFilter.class)
            .authorizeRequests().antMatchers("/**").permitAll();
    }
}

My IPSecurityFilter:

public class IPSecurityFilter extends OncePerRequestFilter {
    private static final Logger logger = LoggerFactory.getLogger(IPSecurityFilter.class);
    private String[] ipAddresses;

    public IPSecurityFilter(String strIPAddresses) {
        logger.info("@@@@ Our IP Address whitelist: " + strIPAddresses);
        this.ipAddresses = strIPAddresses.split(",");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.info("Checking whether request should be allowed: " + request.getRequestURI());
        logger.info("@@@ Request is coming from IP address: " + request.getRemoteAddr());
        for (String ipAddress : ipAddresses) {
            if (ipAddress.equals(request.getRemoteAddr())) {
                logger.info("@@@ Allowing request from ip address: " + request.getRemoteAddr());
                return;         // We accept requests from this IP address
            }
        }
        // The remote IP address isn't on our white list; throw an exception
        throw new AccessDeniedException("Access has been denied for your IP address: " + request.getRemoteAddr());
    }
}

This seems to work in that the request is rejected if it originates from an IP Address that isn't on my white list.

However, with this configuration, my unit (using MockMvc) test fails; and it fails in a way that I would never have expected. When the unit test runs, it appears to use the Spring Security configuration correctly and the request passes the security test (the IP white list includes 127.0.0.1 and according to the log that is generated while the test is being run, the request is coming from that IP). However, the request never seems to be routed to my controller.

Here is my test:

@RunWith(SpringRunner.class)
@WebMvcTest()
//@WebMvcTest(value = HandlerController.class)
@AutoConfigureMockMvc
@Import(SecurityConfig.class)
public class HandlerControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void getIndex() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(content().json("{\"services\":[\"OutboundMessageService\"]}", true));
    }
}

And finally, here is my controller (please ignore the idiotic way that I'm generating the JSON return value, it's still very early in development):

@RestController
public class HandlerController {
    private static final Logger logger = LoggerFactory.getLogger(HandlerController.class);

    @RequestMapping("/")
    public String index() {
        logger.info("### handling a request for / ###");
        return "{\"services\":[\"OutboundMessageService\"]}";
    }
}

And here are the test results:

2017-11-14 08:29:12.151  INFO 25412 --- [           main] c.z.s.controllers.HandlerControllerTest  : Starting HandlerControllerTest on 597NLL1 with PID 25412 (started by User in C:\Development\KnowledgeBin\NewArchitecture\OutboundMessageHandler)
2017-11-14 08:29:12.152  INFO 25412 --- [           main] c.z.s.controllers.HandlerControllerTest  : No active profile set, falling back to default profiles: default
2017-11-14 08:29:12.178  INFO 25412 --- [           main] o.s.w.c.s.GenericWebApplicationContext   : Refreshing org.springframework.web.context.support.GenericWebApplicationContext@209da20d: startup date [Tue Nov 14 08:29:12 MST 2017]; root of context hierarchy
2017-11-14 08:29:13.883  INFO 25412 --- [           main] b.a.s.AuthenticationManagerConfiguration : 

Using default security password: 56e3fab8-f7fb-4fbd-b2d2-e37eae8cef5e

2017-11-14 08:29:13.962  INFO 25412 --- [           main] c.z.services.security.IPSecurityFilter   : @@@@ Our IP Address whitelist: 122.22.22.22,127.0.0.1
2017-11-14 08:29:14.086  INFO 25412 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3f4f9acd, org.springframework.security.web.context.SecurityContextPersistenceFilter@470a9030, org.springframework.security.web.header.HeaderWriterFilter@60c16548, org.springframework.security.web.csrf.CsrfFilter@435ce306, org.springframework.security.web.authentication.logout.LogoutFilter@607b2792, com.zpaper.services.security.IPSecurityFilter@46baf579, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@27494e46, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@36453307, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4bf324f9, org.springframework.security.web.session.SessionManagementFilter@452c8a40, org.springframework.security.web.access.ExceptionTranslationFilter@39ce27f2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@5767b2af]
2017-11-14 08:29:14.183  INFO 25412 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto public java.lang.String com.zpaper.services.controllers.HandlerController.index()
2017-11-14 08:29:14.184  INFO 25412 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/OutboundMessageService]}" onto public java.lang.String com.zpaper.services.controllers.HandlerController.outboundMessage()
2017-11-14 08:29:14.189  INFO 25412 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-11-14 08:29:14.190  INFO 25412 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-11-14 08:29:14.243  INFO 25412 --- [           main] c.z.s.config.HandlerWebConfiguration     : #### My Configuration handler was called ####
2017-11-14 08:29:14.253  INFO 25412 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler]
2017-11-14 08:29:14.313  INFO 25412 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.web.context.support.GenericWebApplicationContext@209da20d: startup date [Tue Nov 14 08:29:12 MST 2017]; root of context hierarchy
2017-11-14 08:29:14.784  INFO 25412 --- [           main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring FrameworkServlet ''
2017-11-14 08:29:14.784  INFO 25412 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : FrameworkServlet '': initialization started
2017-11-14 08:29:14.805  INFO 25412 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : FrameworkServlet '': initialization completed in 21 ms

2017-11-14 08:29:14.897  INFO 25412 --- [           main] c.z.s.controllers.HandlerControllerTest  : Started HandlerControllerTest in 3.095 seconds (JVM running for 3.995)
2017-11-14 08:29:14.981  INFO 25412 --- [           main] c.z.services.security.IPSecurityFilter   : Checking whether request should be allowed: /
2017-11-14 08:29:14.981  INFO 25412 --- [           main] c.z.services.security.IPSecurityFilter   : @@@ Request is coming from IP address: 127.0.0.1
2017-11-14 08:29:14.981  INFO 25412 --- [           main] c.z.services.security.IPSecurityFilter   : @@@ Allowing request from ip address: 127.0.0.1

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /
       Parameters = {}
          Headers = {Accept=[application/json]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 3.363 sec <<< FAILURE! - in com.zpaper.services.controllers.HandlerControllerTest
getIndex(com.zpaper.services.controllers.HandlerControllerTest)  Time elapsed: 0.12 sec  <<< ERROR!
org.json.JSONException: Unparsable JSON string: 
    at org.skyscreamer.jsonassert.JSONParser.parseJSON(JSONParser.java:42)

As can be seen in the log messages, the IP filter is being invoked and is allowing the request to continue. However, the debug string that is being emitted in my handler is nowhere to be seen and the return body is blank. Can anyone tell me why my security filter would prevent MockMvc from having its request successfully routed to my controller?

Final Note: if I use the http.authorizeRequests().anyRequest().access("hasIpAddress('127.0.0.1/0')"); configuration that I first listed or completely get rid of Spring Security by removing my SecurityConfig class, the request is routed successfully to my handler.

Armond answered 14/11, 2017 at 15:54 Comment(1)
I figured out how to make it work. I was not able to find a single article that answered my question but by taking different suggestions in different blog posts, I came up with this which works for me:Armond
A
16

I figured out how to make the test work. I was not able to find a single article that answered my question but by taking different suggestions from multiple blog posts, I came up with this which works for me:

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HandlerController.class)
public class HandlerControllerTest {

    private MockMvc mvc;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Before
    public void setUp() {
//        mvc = MockMvcBuilders.standaloneSetup(new HandlerController()).build();
        mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getIndex() throws Exception {
        mvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(content().json("{\"services\":[\"OutboundMessageService\"]}", true));
    }

    @Test
    public void getMessageService() throws Exception {
        mvc.perform(get("/OutboundMessageService").accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(content().json("{\"status\": \"SUCCESS\"}", true));
    }
}

As you can see, I am no longer auto-wiring the MockMvc object and allowing it be automatically set up but am instead setting it up myself in the setUp() method. The commented-out line in the setUp() method works to successfully test my controller also but it doesn't route the request through my Spring Security IP address filter. I'm leaving it in so that users that don't need to test Spring Security can see an alternate method to set up the MockMvc object. The uncommented line sets up a MockMvc object such that it runs the request through both the security filters and my controller.

Armond answered 21/11, 2017 at 18:8 Comment(3)
Thanks !! I ran into this when I put in my XSS Protection filter.Gable
but is there any way to do the setup only once? if we had many tests, this would introduce a lot of unwanted load - building the mockMvc each timePtarmigan
@DanielPop, use @BeforeClass instead of @Before.Yonah
H
6

I know it is late but for others looking for the answer, you can add that filter to the MockMvc object like this:

    @Autowired
    private MockMvc mvc;

    @Autowired
    private YourCustomFilter filter;
    
    @Before
    public void setUp() {
        mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                             .addFilter(filter).build();
    }
Hinch answered 20/2, 2022 at 18:0 Comment(1)
I tried using this piece of code but getting error. Can you please check this link #74204160Clot

© 2022 - 2024 — McMap. All rights reserved.