Spring Test & Security: How to mock authentication?

282,616

Solution 1

It turned out that the SecurityContextPersistenceFilter, which is part of the Spring Security filter chain, always resets my SecurityContext, which I set calling SecurityContextHolder.getContext().setAuthentication(principal) (or by using the .principal(principal) method). This filter sets the SecurityContext in the SecurityContextHolder with a SecurityContext from a SecurityContextRepository OVERWRITING the one I set earlier. The repository is a HttpSessionSecurityContextRepository by default. The HttpSessionSecurityContextRepository inspects 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.

Thus, my solution is to pass a HttpSession along with the request, which holds 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());
    }
}

Solution 2

Seaching for answer I couldn't find any to be easy and flexible at the same time, then I found the Spring Security Reference and I realized there are near to perfect solutions. AOP solutions often are the greatest ones 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 @WithUserDetails works?

Basically you just need to create a custom UserDetailsService with all the possible users profiles you want to test. E.g

@TestConfiguration
public class SpringSecurityWebAuxTestConfig {

    @Bean
    @Primary
    public UserDetailsService userDetailsService() {
        User basicUser = new UserImpl("Basic User", "[email protected]", "password");
        UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                new SimpleGrantedAuthority("ROLE_USER"),
                new SimpleGrantedAuthority("PERM_FOO_READ")
        ));

        User managerUser = new UserImpl("Manager User", "[email protected]", "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 ready, so imagine we want to test the access control to 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 a get mapped function to the route /foo/salute and we are testing a 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's actually forbidden.

@RunWith(SpringRunner.class)
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("[email protected]")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("[email protected]")));
    }

    @Test
    @WithUserDetails("[email protected]")
    public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isForbidden());
    }
}

As you see we imported SpringSecurityWebAuxTestConfig to provide our users for testing. Each one used on its corresponding test case just by using a straightforward annotation, reducing code and complexity.

Better use @WithMockUser for simpler Role Based Security

As you see @WithUserDetails has all the flexibility you need for most of your applications. It allows you to use custom users with any GrantedAuthority, like roles or permissions. But if you are just working with roles, testing can be even easier and you could avoid constructing a custom UserDetailsService. In such cases, specify a simple combination of user, password and roles with @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";
}

The annotation defines default values for a very basic user. As in our case the route we are testing just requires that the authenticated user be a manager, we can quit using 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")));
}

Notice that now instead of the user [email protected] we are getting the default provided by @WithMockUser: user; yet it won't matter because what we really care about is his role: ROLE_MANAGER.

Conclusions

As you see with annotations like @WithUserDetails and @WithMockUser we can switch between different authenticated users scenarios without building classes alienated from our architecture just for making simple tests. Its also recommended you to see how @WithSecurityContext works for even more flexibility.

Solution 3

Since Spring 4.0+, the best solution is to annotate the test method with @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'

Solution 4

Add in 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 for authorization request. See the sample usage at 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 become part of spring-security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, version is the same).

Setting Up is 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();
}

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

Solution 5

Here is an example for those who want to Test Spring MockMvc Security Config using Base64 basic 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>
Share:
282,616

Related videos on Youtube

Martin Becker
Author by

Martin Becker

Updated on August 14, 2021

Comments

  • Martin Becker
    Martin Becker almost 3 years

    I was trying to figure out how to unit test if my the URLs of my controllers are properly secured. Just in case someone changes things around and accidentally removes security settings.

    My controller method looks like this:

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

    I set up a WebTestEnvironment like so:

    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 picked this up here:

    Yet if one looks closely this only helps when not sending actual requests to URLs, but only when testing services on a function level. In my case an "access denied" exception 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 are noteworthy basically saying that no user was authenticated indicating that setting the Principal did not work, or that it was overwritten.

    14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - 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 o.s.s.a.i.a.MethodSecurityInterceptor - 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
    
  • Rob Winch
    Rob Winch over 11 years
    We have not yet added official support for Spring Security. See jira.springsource.org/browse/SEC-2015 An outline for what it will look like is specified in github.com/SpringSource/spring-test-mvc/blob/master/src/test‌​/…
  • Martin Becker
    Martin Becker over 11 years
    Mh, maybe I don't get the whole picture. My problem was that the SecurityContextPersistenceFilter replaces the SecurityContext using a SecurityContext from a HttpSessionSecurityContextRepository, which in turn reads the SecurityContext from the corresponding HttpSession. Thus the solution using the session. Regarding the call to the SecurityContextHolder: I edited my answer so that I am not using a call to the SecurityContextHolder anymore. But also without introducing any wrapping or extra mocking libraries. Do you think, this is a better solution?
  • Martin Becker
    Martin Becker over 11 years
    I don't think creating an Authentication object and adding a session with the corresponding attribute is that bad. Do you think this is a valid "work around"? Direct support on the other hand would be great of course. Looks pretty neat. Thanks for the link!
  • Pavla Nováková
    Pavla Nováková over 11 years
    Sorry I didn't understand exactly what you were looking for and I cannot provide better answer than the solution you came up with and - it seems to be a good option.
  • Martin Becker
    Martin Becker over 11 years
    All right, thanks. I will accept my proposal as a solution for now.
  • Tanvir
    Tanvir about 9 years
    great solution. worked for me! just a minor issue with the naming of the protected method getPrincipal() which to my opinion is a bit misleading. ideally it should have been named getAuthentication(). likewise, in your signedIn() test, the local variable should be named auth or authentication instead of principal
  • Ian Newland
    Ian Newland over 7 years
    This also fixed my issue with getting a 404 when trying to login via a login security filter. Thanks!
  • Sanjeev
    Sanjeev almost 7 years
    Hi, while testing as mentioned by GKislin. I am getting following error "Authentication failed UserDetailsService returned null, which is an interface contract violation" . Any suggestion please. final AuthenticationRequest auth = new AuthenticationRequest(); auth.setUsername(userId); auth.setPassword(password); mockMvc.perform(post("/api/auth/").content(json(auth)).conte‌​ntType(MediaType.APP‌​LICATION_JSON));
  • user2992476
    user2992476 over 6 years
    What is "getPrincipal("test1") ¿?? Could you expline where is it that? Thanks in advance
  • TuGordoBello
    TuGordoBello over 5 years
    Spring is amazing. Thanks
  • supertramp
    supertramp over 5 years
    Good answer. Moreover - you do not need to use mockMvc, but in case if you are using e.g. PagingAndSortingRepository from springframework.data - you can just call methods from the repository directly (which are annotated with EL @PreAuthorize(......))
  • bluelurker
    bluelurker almost 5 years
    @user2992476 It probably returns an object of type UsernamePasswordAuthenticationToken. Alternatively, you create GrantedAuthority and construct this object.
  • newhouse
    newhouse almost 5 years
    If you have an own UserDetailsService, wouldn't be more convenient to mock out it or its internal state (e.g. the repository behind that service impl)? I didn't have to write anything, just mocking out an existing user retrieval from the repo (plus noop password encoder settings all around the integration test env).
  • ch271828n
    ch271828n over 4 years
    How to mock multiple users? For instance, the first request is sent by tom, while the second is by jerry?
  • EliuX
    EliuX over 4 years
    You can create a function where your test is with tom and create another test with the same logic and test it with Jerry. There will be a particular outcome for each test so there will be different assertions and if a test fails it will tell you by its name which user/role did not work. Remember that in a request the user can be only one, so to specify multiple users in a request does not make sense.
  • ch271828n
    ch271828n over 4 years
    Sorry I mean such example scenario: We test that, tom creates a secret article, then jerry tries to read that, and jerry should not see it (since it is secret). So in this case, it is one unit test...
  • EliuX
    EliuX about 4 years
    It looks pretty much like the BasicUser and Manager User scenario given in the answer. The key concept is that instead of caring about the users we actually care about their roles, but each of those tests, located in the same unit test, actually represent different queries. done by different users (with different roles) to the same endpoint.
  • Leonardo Pinto
    Leonardo Pinto about 3 years
    Thanks, it gets better with @WithMockUser(roles = "YOUR_ROLE")
  • Kulbhushan Singh
    Kulbhushan Singh almost 3 years
    How to add orgId as well
  • Abdullah Khaled
    Abdullah Khaled over 2 years
    just a small reminder that you don't have to explicitly add the version to your spring dependencies