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; } }
Advertisement
Answer
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).