Monday 12 December 2016

JUnit based on Spring MVC Test framework fails with AuthenticationCredentialsNotFoundException


After adding the second servlet and a servlet mapping to the web.xml configuration of a Spring-based web application, a JUnit test that relied on the Spring MVC Test framework started to fail.
The unit test was used to verify proper functioning of controller security layer that is based on Spring Security framework (v3.2.9 at the time).

The JUnit code (fragments):

@ContextConfiguration(loader = WebContextLoader.class, locations = {
"classpath:spring/application-context.xml",
"classpath:spring/servlet-context.xml",
"classpath:spring/application-security.xml"})
public class AuthenticationIntegrationTest extends AbstractTransactionalJUnit4SpringContextTests {
    @Autowired
    private WebApplicationContext restApplicationContext;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;    

    private MockMvc mockMvc;

...

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(restApplicationContext)
                .addFilter(springSecurityFilterChain, "/*")
                .build();
    }

    @Test
    public void testCorrectUsernamePassword() throws Exception {
        String username = "vitali@vtesc.ca";
        String password = "password";
       
        ResultActions actions = mockMvc.perform(post("/user/register").header("Authorization", createBasicAuthenticationCredentials(username, password)));
    }
}

The test started to fail with the AuthenticationCredentialsNotFoundException as the root cause.
The change that caused the failure was introduced in order to split request security filtering into 2 distinct filter chains. The existing configuration for securing RESTful calls with Basic authentication needed to be amended to add a separate handling of requests supporting the Web user interface of the application.
That necessitated adding a second <security:http> configuration to the application-security.xml context:

<!-- REST -->
<http pattern="/rest/**" entry-point-ref="basicAuthEntryPoint" authentication-manager-ref="restAuthManager">
...
</http>

<!-- Web UI -->
<http pattern="/web/**" entry-point-ref="preAuthEntryPoint" authentication-manager-ref="webAuthManager">
        <custom-filter position="PRE_AUTH_FILTER" ref="preAuthFilter" />
        <!-- Must be disabled in order for the webAccessDeniedHandler be invoked by Spring Security -->
        <anonymous enabled="false"/>
        <access-denied-handler ref="webAccessDeniedHandler"/>
</http>

The pattern="/rest/**"  attribute was also introduced at the same time to the original <http> configuration element.

That is what ultimately caused the test to fail since the JUnit was not using Servlet path.
It is important to note that Spring MVC Test Framework runs outside of a web container and has neither dependency nor is using the web.xml.
When testing with MockMvc, it is not required to specify the context path or Servlet path when submitting requests to the controllers under test.
For example, when testing this controller:

    @RequestMapping(value = "/user/register", method = RequestMethod.POST, headers = "accept=application/json,text/*", produces = "application/json")
    @PreAuthorize("hasPermission(null, 'ROLE_USER')")
    @ResponseBody
    public RegistrationResponse register(@RequestBody(required=false) UserDeviceLog userDeviceLog) {
...
it would be sufficient to send request only specifying the mapping:
mockMvc.perform(post("/user/register").header("Authorization", createBasicAuthenticationCredentials(username, password)))

However, when access to the controllers is protected by Spring Security and the pattern is specified in the <security:http> configuration, the Spring MVC Test Framework will fully respect the processing flow failing requests that do not provide a correct Servlet path.

Resolution:
1. Specify a correct Servlet path in the request URL and also add the mapping by passing the path to the servletPath(String) method of the MockHttpServletRequestBuilder class:

mockMvc.perform(post("/rest/user/register").servletPath("/rest").header("Authorization", createBasicAuthenticationCredentials(username, password)));

2. Configure the MockMvc instance with the security filter mapping that matches the pattern specified in the application-security.xml configuration:
    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(restApplicationContext)
                .addFilter(springSecurityFilterChain, "/rest/*")
                .build();
    }

<end>

No comments:

Post a Comment