Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I unit test spring security @PreAuthorize(hasRole)?

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;     } } 
like image 957
edwardmlyte Avatar asked Mar 20 '14 09:03

edwardmlyte


People also ask

What is Spring Security HasRole?

HasRole checks the granted authorities for the currently authenticated principal. So really when you see hasRole("blah") really means hasAuthority("blah"). In the case I've seen, you do this with a class that Implements UserDetails which defines a method called getAuthorities.

What is @PreAuthorize annotation in spring boot?

Spring Security provides method level security using @PreAuthorize and @PostAuthorize annotations. This is expression-based access control. The @PreAuthorize can check for authorization before entering into method. The @PreAuthorize authorizes on the basis of role or the argument which is passed to the method.


2 Answers

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).

like image 99
Rob Winch Avatar answered Oct 06 '22 01:10

Rob Winch


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.

like image 30
Akio Hamasaki Avatar answered Oct 06 '22 01:10

Akio Hamasaki