39

What do I need in order to unit test the hasRole part of a PreAuthorize annotation on a controller method?

My test should succeed because the logged in user only has one of the two roles, but instead it fails with the following assertion error:

java.lang.AssertionError: Status

Expected :401

Actual :200

I have the following method in MyController:

@PreAuthorize(value = "hasRole('MY_ROLE') and hasRole('MY_SECOND_ROLE')")
@RequestMapping(value = "/myurl", method = RequestMethod.GET)
public String loadPage(Model model, Authentication authentication, HttpSession session) {
    ...stuff to do...
}

I created the following abstract-security-test.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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-3.2.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">

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

    <security:authentication-manager alias="authManager">
        <security:authentication-provider>
            <security:user-service>
                <security:user name="missingsecondrole" password="user" authorities="MY_ROLE" />
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>

</beans>

And in my unit test I have this:

@ContextConfiguration("classpath:/spring/abstract-security-test.xml")
public class MyTest {
    private final MyController myController = new MyController();
    @Autowired
    private AuthenticationManager manager;

    @Test
    public void testValidUserWithInvalidRoleFails() throws Exception {
        MockMvc mockMvc = standaloneSetup(myController).setViewResolvers(viewResolver()).build();

        Authentication auth = login("missingsecondrole", "user");

        mockMvc.perform(get("/myurl")
            .session(session)
            .flashAttr(MODEL_ATTRIBUTE_NAME, new ModelMap())
            .principal(auth)).andExpect(status().isUnauthorized());
    }

    protected Authentication login(String name, String password) {
        Authentication auth = new UsernamePasswordAuthenticationToken(name, password);
        SecurityContextHolder.getContext().setAuthentication(manager.authenticate(auth));
        return auth;
    }

    private ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("WEB-INF/views");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}
Community
  • 1
  • 1
edwardmlyte
  • 15,937
  • 23
  • 58
  • 83

6 Answers6

35

UPDATE

Spring Security 4 provides comprehensive support for integrating with MockMvc. For example:

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class SecurityMockMvcTests {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

    @Test
    public void withUserRequestPostProcessor() {
        mvc
            .perform(get("/admin").with(user("admin").roles("USER","ADMIN")))
            ...
    }

    @WithMockUser(roles="ADMIN")
    @Test
    public void withMockUser() {
        mvc
            .perform(get("/admin"))
            ...
    }

 ...

The Problem

The problem is that setting the SecurityContextHolder does not work in this instance. The reason is that the SecurityContextPersistenceFilter will use the SecurityContextRepository to try and figure out the SecurityContext from the HttpServletRequest (by default it uses the HttpSession). The SecurityContext it finds (or doesn't find) will override the SecurityContext you have set on the SecurityContextHolder.

The Solution

To ensure the request is authenticated you need to associate your SecurityContext using the SecurityContextRepository that you are leveraging. The default is the HttpSessionSecurityContextRepository. An example method that will allow you to mock being logged in by a user is below:

private SecurityContextRepository repository = 
      new HttpSessionSecurityContextRepository();

private void login(SecurityContext securityContext, HttpServletRequest request) {
    HttpServletResponse response = new MockHttpServletResponse();

    HttpRequestResponseHolder requestResponseHolder = 
          new HttpRequestResponseHolder(request, response);
    repository.loadContext(requestResponseHolder);

    request = requestResponseHolder.getRequest();
    response = requestResponseHolder.getResponse();

    repository.saveContext(securityContext, request, response);
}

The details of how to use this might still a bit vague since you might not know how to access the HttpServletRequest in MockMvc, but keep reading as there is a better solution.

Making it easier

If you want to make this and other Security related interactions with MockMvc easier, you can refer to the gs-spring-security-3.2 sample application. Within the project you will find some utilities for working with Spring Security and MockMvc called SecurityRequestPostProcessors. To use them you can copy that previously mentioned class into your project. Using this utility will allow you to write something like this instead:

RequestBuilder request = get("/110")
    .with(user(rob).roles("USER"));

mvc
    .perform(request)
    .andExpect(status().isUnAuthorized());

NOTE: There is no need to set the principal on the request as Spring Security establishes the Principal for you as long as a user is authenticated.

You can find additional examples in SecurityTests. This project will also assist in other integrations between MockMvc and Spring Security (i.e. setting up the request with the CSRF token when performing a POST).

Not included by default?

You might ask why this is not included by default. The answer is that we simply did not have time for the 3.2 timeline. All the code in the sample will work fine, but we weren't confident enough on naming conventions and exactly how it integrated to release this. You can track SEC-2015 which is scheduled to come out with Spring Security 4.0.0.M1.

Update

Your MockMvc instance needs to also contain the springSecurityFilterChain. To do so, you can use the following:

@Autowired
private Filter springSecurityFilterChain;

@Test
public void testValidUserWithInvalidRoleFails() throws Exception {
    MockMvc mockMvc = standaloneSetup(myController)
        .addFilters(springSecurityFilterChain)
        .setViewResolvers(viewResolver())
        .build();
    ...

For the @Autowired to work, you need to ensure to include your security configuration that makes the springSecurityFilterChain in your @ContextConfiguration. For your current setup, this means "classpath:/spring/abstract-security-test.xml" should contain your <http ..> portion of your security configuration (and all the dependent beans). Alternatively, you can include a second file(s) in the @ContextConfiguration that has your <http ..> portion of your security configuration (and all the dependent beans).

Rob Winch
  • 21,440
  • 2
  • 59
  • 76
  • Thanks for the full explanation Rob. However after downloading the gs-spring-security-3.2 3 of the SecurityTests fail: inboxShowsOnlyRobsMessages, validUsernamePassword, robCannotAccessLukesMessage. Which all seem to be based around users and roles. Similarly, implementing your suggestion didn't change the issue and I still don't get the 401 status I expect so the test fails. (note: I ran those tests in intelliJ 13.1 against java 6 and 7 with the same results. But I presume that shouldn't affect the tests success rate.). Any other thoughts? – edwardmlyte Mar 21 '14 at 10:33
  • You are correct that the tests weren't passing. I had accidentally had removed some of the security from master instead of pushing it to the presentation branch (i.e. I have a branch that is the start of my presentation & master *should* be the completed version). Please give it a try with the updated from master. IntelliJ should not cause any issues, but let's try to get the sample I provided working first since I know exactly what code is there. – Rob Winch Mar 21 '14 at 14:10
  • 1
    So the tests now work. However I'm still unable to solve my problem. Regardless of the user I pass in with whatever roles, it will always succeed in logging in. Which part of your tests is key for checking those roles? Is it the springSecurityFilterChain on the mockMvc builder? Where is that autowired from as I couldn't find a matching class in WebMvcConfiguration. – edwardmlyte Mar 25 '14 at 10:21
  • Is there a way to include the import for `user` into the answer, please? – acarlstein Mar 06 '23 at 20:42
2

Just to add to Rob's solution above, as of December 20, 2014, there is a bug in the SecurityRequestPostProcessors class on the master branch from Rob's answer above that prevents the assigned roles from being populated.

A quick fix is to comment out the following line of code (currently line 181) in the roles(String... roles) method of the UserRequestPostProcessor inner static class of SecurityRequestPostProcessors:

// List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(roles.length);.

You need to comment out the local variable, NOT the member variable.

Alternatively, you may insert this line just before returning from the method:

this.authorities = authorities;

P.S I would have added this as a comment had I had enough reputation.

Akio Hamasaki
  • 525
  • 6
  • 11
1

MockMvcBuilders.standaloneSetup gets a MyController instantiated manually ( without Spring and therefore without AOP). Therefore the PreAuthorize is not intercepted and security check is skipped. You can therefore @Autowire your controller and pass it to MockMvcBuilders.standaloneSetup to mock any services passed to the controller (as it's sometimes needed) use @MockBean so that every instance of the service gets replaced with the Mock.

user1928596
  • 1,503
  • 16
  • 21
1

I had the same issue and I worked on it for 1 week almost to resolve the issue so I want to share my knowledge here maybe it helps someone later. The accepted answer is more or less correct but the main point is you have to declare all injected beans in your abstract-security-test.xml which can be a big pain when you have lots of injections and also you don't want to duplicate everything. So I used an autoBeanMocker to mock all the beans. The class is like this:

import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static org.mockito.Mockito.mock;


public class AutoBeanMocker implements BeanDefinitionRegistryPostProcessor
{

    private Collection<String> mockedDefinitions;

    public AutoBeanMocker()
    {
        mockedDefinitions = new ArrayList<String>();
    }

    private Iterable<Field> findAllAutoWired(Class targetBean)
    {
        List<Field> declaredFields = Arrays.asList(targetBean.getDeclaredFields());
        return declaredFields.stream().filter(input -> input.isAnnotationPresent(Autowired.class) || input.isAnnotationPresent(Resource.class))
                .collect(Collectors.toList());
    }

    private void registerOn(final BeanDefinitionRegistry registry, final String beanName, final Class type)
    {
        RootBeanDefinition definition = new RootBeanDefinition();

        MutablePropertyValues values = new MutablePropertyValues();
        values.addPropertyValue(new PropertyValue("type", type));
        definition.setPropertyValues(values);
        ((DefaultListableBeanFactory) registry).registerSingleton(beanName, mock(type));
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException
    {
        for (String beanName : registry.getBeanDefinitionNames())
        {
            BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
            String beanClassName = beanDefinition.getBeanClassName();
            try
            {
                Class beanClass = Class.forName(beanClassName);
                while (true)
                {
                    for (final Field field : findAllAutoWired(beanClass))
                    {
                        String fieldName = field.getName();
                        boolean invalidType = field.getType().isArray() || field.getType().isPrimitive();
                        if (invalidType)
                        {
                            continue;
                        }
                        if (!registry.isBeanNameInUse(fieldName))
                        {
                            registerOn(registry, fieldName, field.getType());
                            mockedDefinitions.add(fieldName);
                            // Now field will be available for autowiring.
                        }
                    }
                    if (beanClass.getSuperclass() != null)
                        beanClass = beanClass.getSuperclass();
                    else
                        break;
                }
            }
            catch (Exception ex)
            {
                Logger.getLogger(AutoBeanMocker.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
    {
        for (String beanName : mockedDefinitions)
        {
            if (!beanFactory.containsBean(beanName))
            {
                Logger.getLogger(AutoBeanMocker.class.getName()).log(Level.SEVERE, "Missing definition %s", beanName);
            }
        }
    }
}

Don't forget to add it to your context configuration xml file.

So now you need to autowire your controller in your test:

@InjectMocks
@Autowire
private MyController myController;

Since I want to mock some beans I also used @InjectMocks on top of my controller and I used MockitoAnnotations.initMocks(this); in my setup() method. Now the last important point that you should know is if you are Autowiring some beans into your controller you need to create setter methods for them otherwise the InjectMocks is not going to work.

Also I didn't need to add the SpringSecurityFilterChain to my controller so I simply defined My mockMvc like this:

mockMvc = standaloneSetup(myController).build();

This is a sample test method:

@Test
public void someTest_expectAccessDeniedException() throws Exception
{
    when(someBean.someMethod(someParameter)).thenReturn(someReturn);

    mockMvc.perform(get("somePath"))
                .andExpect(result -> assertTrue(result.getResolvedException() instanceof AccessDeniedException));
}
Mohsen
  • 4,536
  • 2
  • 27
  • 49
1

Adding @WithMockUser(authorities = ["YOUR_ROLE"]) worked for me. This auto sets the role in Spring's security context when using MockMcv.

@Test
@WithMockUser(authorities = ["YOUR_ROLE"])
void test_role() {


  }
Power
  • 41
  • 5
1

Just an alternative way of testing, just filter class methods with PreAuthorize annotation. I made simple test generator

@Test
public void shouldCheckMethodsPreAuthorizeAnnotationValue()  {
    List<Method> methods = Arrays.stream(YourControllerOrService.class.getDeclaredMethods())
            .filter(method -> AnnotationUtils.getAnnotation(method, PreAuthorize.class) != null)
            .collect(Collectors.toList());

    System.out.println(format("assertEquals(methodsMap.size(), %s);", methods.size()));

    Map<String, String> methodsMap = methods.stream()
            .map(method -> {
                PreAuthorize annotation = AnnotationUtils.getAnnotation(method, PreAuthorize.class);
                System.out.println(format("assertEquals(methodsMap.get(\"%s\"), \"%s\");", method.getName(), annotation.value()));
                return method;
            })
            .collect(Collectors.toMap(
                    Method::getName,
                    method -> Objects.requireNonNull(AnnotationUtils.getAnnotation(method, PreAuthorize.class)).value()
            ));



    assertEquals(methodsMap.size(), 2);
    assertEquals(methodsMap.get("getMethod1"), "hasRole('ROLE_BLA_1')");
    assertEquals(methodsMap.get("getMethod2"), "hasRole('ROLE_BLA_2')");
}

The test covers all @PreAuthorize annotations. just remove unnecessary System.out after you have generated assertions.

Dmitri Algazin
  • 3,332
  • 27
  • 30