Charles Ouimet
Charles Ouimet

Reputation: 166

How to keep SecurityContext set through WithSecurityContextFactory in Spring Security tests?

I'm using Spring 4.1.5 and Spring Security 4.0.0.RELEASE.

I read http://spring.io/blog/2014/05/07/preview-spring-security-test-method-security (nice article by Rob Winch) and developed my own implementation of WithSecurityContextFactory to be able to test my Spring MVC controllers:

public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {

    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
        final User fakeUser = new User();
        final SecurityUser principal = new SecurityUser(fakeUser);
        final Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", HelpersTest.getAuthorities(customUser.faps()));

        final SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(auth);

        return context;
    }
}

My abstract resource test class is as follow:

    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @ContextConfiguration(locations =
    {
    "classpath:spring/mock-daos-and-scan-for-services.xml",
    "classpath:security.xml",
    "classpath:singletons.xml",
    "classpath:controller-scan.xml",
    "classpath:servlet.xml" })
    @TestExecutionListeners(listeners=
    {
    ServletTestExecutionListener.class,
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    WithSecurityContextTestExcecutionListener.class })

    public abstract class AbstractResourceMockMvcTest {

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private Filter springSecurityFilterChain;

    private MockMvc mockMvc;

    [...]

    @Before
    public void setup() {
        this.mockMvc = 
            MockMvcBuilders.webAppContextSetup(this.getWac())
            .addFilters(springSecurityFilterChain)
            .build();
    }

    [...]

}

Then, my concrete test class inherits from AbstractResourceTest (from above) and it uses the following annotation on a @Test-enabled method:

@WithMockCustomUser(faps={"promotion_read"})

Tracing the code, I can confirm WithMockCustomUserSecurityContextFactory.createSecurityContext() is called and its return value is set in SecurityContextHolder.setContext() (through TestSecurityContextHolder.setContext()).

So far, so good !

However, later in the process, SecurityContextPersistenceFilter.doFilter() calls SecurityContextHolder.setContext() and this overwrites the context set by the test and I lose track of the mocked security context I prepared.

security.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans    http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd
    "
>

    <!-- HTTP security handling -->
    <security:http use-expressions="true">

        <security:logout logout-url="/j_spring_security_logout" invalidate-session="true" logout-success-url="/login.jsp?loggedout=true" />

        <security:custom-filter before="FIRST" ref="multiTenantRequestFilter" />

        <!-- make sure following page are not secured -->

        <security:intercept-url pattern="/*/*/internal/**" access="hasIpAddress('127.0.0.1')" />

        <!-- make sure everything else going through the security filter is secured -->

        <security:intercept-url pattern="/resources/**" access="hasRole('ROLE_USER')" requires-channel="any" />

        <!-- supporting basic authentication for unattended connections (web services) -->

        <security:http-basic />

    </security:http>

    <!-- authentication strategy -->

    <security:authentication-manager alias="authManager">
        <security:authentication-provider user-service-ref="userSecurityService">
            <security:password-encoder ref="passwordEncoder" />
        </security:authentication-provider>
    </security:authentication-manager>

    <!-- custom filter to intercept the tenant name from the login form -->

    <bean id="multiTenantRequestFilter" class="com.meicpg.ti.web.MultiTenantRequestFilter" />

</beans>

servlet.xml:

<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.1.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
    "
>
    <mvc:annotation-driven>
        <!-- Content skipped for StackOverflow question -->
    </mvc:annotation-driven>

    <context:annotation-config />

    <bean id="annotationExceptionResolver" class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver"></bean>

    <security:global-method-security pre-post-annotations="enabled"/>

    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

How can I prevent this security context overwrite ? Does my security.xml contain an obvious flaw I missed ?

PS: I skipped the other context configuration files as they seem irrelevant to the problem.

Thanks in advance !

Upvotes: 3

Views: 3713

Answers (1)

Rob Winch
Rob Winch

Reputation: 21720

Unfortunately that blog post is just for method level security and does not have complete instructions for MockMvc setup (the following blog in the series does). Additionally, the blogs are actually dated (I have updated them to reflect that readers should refer to the reference documentation). You can find updated instructions in the Testing Section of the reference.

In short, update your code to the following:

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

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations =
{
"classpath:spring/mock-daos-and-scan-for-services.xml",
"classpath:security.xml",
"classpath:singletons.xml",
"classpath:controller-scan.xml",
"classpath:servlet.xml" })
public abstract class AbstractResourceMockMvcTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    [...]

    @Before
    public void setup() {
        this.mockMvc = 
            MockMvcBuilders.webAppContextSetup(this.getWac())
            .apply(springSecurity()) 
            .build();
    }

    @Test
    @WithMockCustomUser(faps={"promotion_read"})
    public void myTest() {
        ...
    }

    [...]

}

A few highlights:

  • You no longer need to provide the TestExecutionListeners
  • Use .apply(springSecurity()) instead of adding the spring security filter chain manually

This works because Spring Security's test support i.e. apply(springSecurity()) will override the SecurityContextRepository used by the springSecurityFilterChain to first try the TestSecurityContextHolder.

Upvotes: 4

Related Questions