How do I unit test spring security @PreAuthorize(hasRole)?
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).
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.