Sunday, May 25, 2014

Work around to authenticate during unit testing on Spring MVC

Following method is the "usual practice" used in unit testing the authentication on Spring MVC.
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(value = {"classpath:/WEB-INF/Project-servlet.xml", "classpath:/WEB-INF/security.xml", "classpath:/WEB-INF/datasource.xml", "classpath:/WEB-INF/user.xml"})

public class HelloControllerTest extends AbstractJUnit4SpringContextTests {

 private MockMvc mockMvc;
 
 @Autowired
 private WebApplicationContext wac;
 
 @Autowired
 @Qualifier("authServiceProvider4")
 private UserDetailsService userDetailsService;

 @Autowired
 private FilterChainProxy proxy;
 
 @Before
 public void setUp() {
  mockMvc = MockMvcBuilders.webAppContextSetup(wac).addFilter(proxy).build();
 }

 @Test
 public void testUserWithAdminRoleLandOnWelcomeUrl() throws Exception {
  
  UserDetails ud = userDetailsService.loadUserByUsername("user1");
  Authentication auth = new UsernamePasswordAuthenticationToken(ud.getUsername(), ud.getPassword(), ud.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(auth);

  ...

  mockMvc.perform(get("/welcome"))
   .andExpect(status().isOk());

 }
}
Code sample above showing a test method testing on valid user, user1, landing on welcome site which require admin role in order to render the page. Somehow the test were failed, and following failure trace were seen:
java.lang.AssertionError: Status expected:<200> but was:<302>
 at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
 at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
 at org.springframework.test.web.servlet.result.StatusResultMatchers$5.match(StatusResultMatchers.java:546)
 at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:141)
 at org.huahsin.sit.HelloControllerTest.testUserWithAdminRoleLandOnWelcomeUrl(HelloControllerTest.java:101)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:622)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


From what I understand from this answer, the session of user1 were gone missing, I need to hold the session of user1 when MockMvc doing its work. To do this, a mocking session class is require:
    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;
        }
    }
After that create a mock session and pass it to MockMvc to perform its test work.
 @Test
 public void testUserWithAdminRoleLandOnWelcomeUrl() throws Exception {
  
  UserDetails ud = userDetailsService.loadUserByUsername("user1");
  Authentication auth = new UsernamePasswordAuthenticationToken(ud.getUsername(), ud.getPassword(), ud.getAuthorities());
  SecurityContextHolder.getContext().setAuthentication(auth);

  MockHttpSession session = new MockHttpSession();
  session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, new MockSecurityContext(auth));

  mockMvc.perform(get("/welcome").session(session))
   .andExpect(status().isOk());

 }

Friday, May 23, 2014

Why @Secured and @PreAuthorize isn't working?

As mention in the title, I'm so curious to know why the both annotation are not working? Since there were so many examples and tutorials available on the Internet, but why none of them showing me that configuring Spring Security in such a way isn't a wise move? Which way? The finding is as follow.

As according to the Spring MVC development standard, the Spring Security configuration and servlet configuration were separated. Thus I was thinking that those security stuff together with <global-method-security> must be separated out from regular Spring configuration module and put them in Spring security configuration module.

To make the picture clearer. I have the Spring security configure in such a way:

 
 

  
   
   
  
   ...
  
   

   ...
   
  

  
  
   
   

   ...

  
 

And those non security stuff would place in regular Spring configuration module shown as below:


   

   
      
         /WEB-INF/pages/
      

      
         .jsp
      
   

And last, I have both Spring configuration declare in web.xml


   ...

   
      contextConfigLocation
      
         /WEB-INF/security.xml
         /WEB-INF/datasource.xml
         /WEB-INF/WebEngineering-servlet.xml
      
   

...


Everything were run perfect and no error. Just that @Secured will never work. Initially I though there is a bug in @Secured for Spring Security 3.1, but this is not true because the annotation were still not working even though I have upgraded to Spring Security 3.2. Someone in the forum suggest me to use @PreAuthorize simply because @PreAuthorize is newer than @Secured. I tried that, unfortunately @PreAuthorise is still working. I have tried many ways, keep searching the solutions, reading the same article again and again to make sure I didn't miss out the important point. Until my last try before I nearly give up (and before I start blaming Spring framework), I take out <global-method-security> from Spring security module, and put it in the regular Spring configuration module.

And it works!!

So this is the final amendment I made on the regular Spring configuration:


   
   

   
      
         /WEB-INF/pages/
      

      
         .jsp
      
   

And now both @Secured and @PreAuthorize are living in my code happily ever after. :)