Spring test and security: how to simulate authentication?

I tried to figure out how to unit test if my URLs of my controllers are properly protected. Just in case, someone changes the situation and accidentally deletes the security settings.

My controller method is as follows:

@RequestMapping("/api/v1/resource/test") @Secured("ROLE_USER") public @ResonseBody String test() { return "test"; } 

I created WebTestEnvironment as follows:

 import javax.annotation.Resource; import javax.naming.NamingException; import javax.sql.DataSource; import org.junit.Before; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.FilterChainProxy; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/security.xml", "file:src/main/webapp/WEB-INF/spring/applicationContext.xml", "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" }) public class WebappTestEnvironment2 { @Resource private FilterChainProxy springSecurityFilterChain; @Autowired @Qualifier("databaseUserService") protected UserDetailsService userDetailsService; @Autowired private WebApplicationContext wac; @Autowired protected DataSource dataSource; protected MockMvc mockMvc; protected final Logger logger = LoggerFactory.getLogger(this.getClass()); protected UsernamePasswordAuthenticationToken getPrincipal(String username) { UserDetails user = this.userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( user, user.getPassword(), user.getAuthorities()); return authentication; } @Before public void setupMockMvc() throws NamingException { // setup mock MVC this.mockMvc = MockMvcBuilders .webAppContextSetup(this.wac) .addFilters(this.springSecurityFilterChain) .build(); } } 

In my actual test, I tried to do something like this:

 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import eu.ubicon.webapp.test.WebappTestEnvironment; public class CopyOfClaimTest extends WebappTestEnvironment { @Test public void signedIn() throws Exception { UsernamePasswordAuthenticationToken principal = this.getPrincipal("test1"); SecurityContextHolder.getContext().setAuthentication(principal); super.mockMvc .perform( get("/api/v1/resource/test") // .principal(principal) .session(session)) .andExpect(status().isOk()); } } 

I chose this here:

However, if you look carefully, this helps only when sending actual requests to URLs, but only when testing services at the function level. In my case, the exception "access denied" was thrown:

 org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE] ... 

The following two log messages should be noted, mainly saying that no user has been authenticated, indicating that the Principal installation is not working or that it has been overwritten.

 14:20:34.454 [main] DEBUG ossaiaMethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER] 14:20:34.454 [main] DEBUG ossaiaMethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS 
+47
spring security junit model-view-controller testing
Mar 04 '13 at 14:11
source share
7 answers

It turned out that the SecurityContextPersistenceFilter , which is part of the Spring security filter chain, always resets my SecurityContext , which I set to call SecurityContextHolder.getContext().setAuthentication(principal) (or using the .principal(principal) method). This filter sets the SecurityContext to the SecurityContextHolder with the SecurityContext from the SecurityContextRepository TOP that I installed earlier. By default, the repository is HttpSessionSecurityContextRepository . HttpSessionSecurityContextRepository checks the given HttpRequest and tries to access the corresponding HttpSession . If it exists, it will try to read the SecurityContext from the HttpSession . If this fails, the repository generates an empty SecurityContext .

So my solution is to pass an HttpSession along with the request that contains the SecurityContext :

 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test; import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import eu.ubicon.webapp.test.WebappTestEnvironment; public class Test extends WebappTestEnvironment { public static class MockSecurityContext implements SecurityContext { private static final long serialVersionUID = -1386535243513362694L; private Authentication authentication; public MockSecurityContext(Authentication authentication) { this.authentication = authentication; } @Override public Authentication getAuthentication() { return this.authentication; } @Override public void setAuthentication(Authentication authentication) { this.authentication = authentication; } } @Test public void signedIn() throws Exception { UsernamePasswordAuthenticationToken principal = this.getPrincipal("test1"); MockHttpSession session = new MockHttpSession(); session.setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, new MockSecurityContext(principal)); super.mockMvc .perform( get("/api/v1/resource/test") .session(session)) .andExpect(status().isOk()); } } 
+37
Mar 04 '13 at 14:11
source share

Add to pom.xml:

  <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>4.0.0.RC2</version> </dependency> 

and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors to request authorization. See an example using https://github.com/rwinch/spring-security-test-blog ( https://jira.spring.io/browse/SEC-2592 ).

Update:

4.0.0.RC2 works for spring-security 3.x. For spring-security 4, spring-security-test becomes part of spring-security ( http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test , the version is the same).

Setting changed: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

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

Basic authentication example: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication .

+24
May 14 '14 at 14:57
source share

Streamlining for the answer I could not find one to be simple and flexible at the same time, then I found Spring Security Link and I realized that there are almost perfect solutions. AOP solutions are often the largest for testing, and Spring provides it with @WithMockUser , @WithUserDetails and @WithSecurityContext in this artifact:

 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>4.2.2.RELEASE</version> <scope>test</scope> </dependency> 

In most cases, @WithUserDetails gathers the flexibility and power I need.

How does @WithUserDetails work?

Basically you just need to create a custom UserDetailsService with all the possible user profiles that you want to test. for example

 @TestConfiguration public class SpringSecurityWebAuxTestConfig { @Bean @Primary public UserDetailsService userDetailsService() { User basicUser = new UserImpl("Basic User", "user@company.com", "password"); UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList( new SimpleGrantedAuthority("ROLE_USER"), new SimpleGrantedAuthority("PERM_FOO_READ") )); User managerUser = new UserImpl("Manager User", "manager@company.com", "password"); UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList( new SimpleGrantedAuthority("ROLE_MANAGER"), new SimpleGrantedAuthority("PERM_FOO_READ"), new SimpleGrantedAuthority("PERM_FOO_WRITE"), new SimpleGrantedAuthority("PERM_FOO_MANAGE") )); return new InMemoryUserDetailsManager(Arrays.asList( basicActiveUser, managerActiveUser )); } } 

Now we have our users, so imagine that we want to test access control for this controller function:

 @RestController @RequestMapping("/foo") public class FooController { @Secured("ROLE_MANAGER") @GetMapping("/salute") public String saluteYourManager(@AuthenticationPrincipal User activeUser) { return String.format("Hi %s. Foo salutes you!", activeUser.getUsername()); } } 

Here we have the mapped route function / foo / salute , and we are testing role-based security with the @Secured annotation, although you can test @PreAuthorize and @PostAuthorize as well. Let's create two tests: one to check if a valid user can see this salute response, and the other to check if it is really forbidden.

 @RunWith(SpringRunner.class) @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SpringSecurityWebAuxTestConfig.class ) @AutoConfigureMockMvc public class WebApplicationSecurityTest { @Autowired private MockMvc mockMvc; @Test @WithUserDetails("manager@company.com") public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isOk()) .andExpect(content().string(containsString("manager@company.com"))); } @Test @WithUserDetails("user@company.com") public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isForbidden()); } } 

As you can see, we have imported SpringSecurityWebAuxTestConfig to provide testing for our users. Each of them was used in the corresponding test case, simply using simple annotation, reducing code and complexity.

Better use @WithMockUser for easier role-based protection

As you can see, @WithUserDetails has all the flexibility needed for most of your applications. It allows users to be used with any GrantedAuthority, such as roles or permissions. But if you just work with roles, testing can be even easier and you can avoid creating a custom UserDetailsService . In such cases, specify a simple combination of user, password and roles using @WithMockUser .

 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @WithSecurityContext( factory = WithMockUserSecurityContextFactory.class ) public @interface WithMockUser { String value() default "user"; String username() default ""; String[] roles() default {"USER"}; String password() default "password"; } 

Annotations define default values ​​for a very simple user. As in our case, the route we are testing requires the authenticated user to be a manager, we can refuse to use SpringSecurityWebAuxTestConfig and do this.

 @Test @WithMockUser(roles = "MANAGER") public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute") .accept(MediaType.ALL)) .andExpect(status().isOk()) .andExpect(content().string(containsString("user"))); } 

Note that now instead of the user manager@company.com, we get the default @WithMockUser : user ; but it doesn’t matter because we really care about his role: ROLE_MANAGER .

Conclusion

As you can see with annotations like @WithUserDetails and @WithMockUser , we can switch between different scripts of authenticated users without creating classes that are alienated from our architecture, just for simple tests. He also recommended that you see how @WithSecurityContext works for even more flexibility.

+13
May 11 '17 at 16:24
source share

Here is an example for those who want to test Spring MockMvc Security Config using basic Base64 authentication.

 String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes())); this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); 

Maven Dependency

  <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.3</version> </dependency> 
+5
Feb 25 '14 at 2:09
source share

Options to avoid using SecurityContextHolder in tests:

  • Option 1 : use mocks - I mean mock SecurityContextHolder using some mock library - e.g. EasyMock
  • Option 2 : wrap call SecurityContextHolder.get... in your code in some service - for example, in SecurityServiceImpl with the getCurrentPrincipal method that implements the SecurityService interface, and then in your tests you can simply create a mock implementation of this interface that returns the desired principal without access to SecurityContextHolder .
+2
Mar 04 '13 at 17:38
source share

Short answer:

 @Autowired private WebApplicationContext webApplicationContext; @Autowired private Filter springSecurityFilterChain; @Before public void setUp() throws Exception { final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path"); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext) .defaultRequest(defaultRequestBuilder) .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest())) .apply(springSecurity(springSecurityFilterChain)) .build(); } private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder, final MockHttpServletRequest request) { requestBuilder.session((MockHttpSession) request.getSession()); return request; } 

After running formLogin from the spring security test, each of your requests will be automatically called as a registered user.

Long answer:

Check out this solution (spring 4 answer): How to log in with spring 3.2 new mvc test

+1
Nov 02 '17 at 7:32 on
source share

Since Spring 4.0+, the best solution is to annotate the testing method using @WithMockUser

 @Test @WithMockUser(username = "user1", password = "pwd", roles = "USER") public void mytest1() throws Exception { mockMvc.perform(get("/someApi")) .andExpect(status().isOk()); } 

Remember to add the following dependency to your project.

 'org.springframework.security:spring-security-test:4.2.3.RELEASE' 
+1
Nov 30 '17 at 13:51
source share



All Articles