Testing Method Security in Spring Boot

Method security testing is the process of testing the security of each method to make sure that only authorized users can access them. By testing the method-level security, we can ensure that our application is secure and that users can only perform the actions that they are authorized to do.

Let’s say we built an application that has different roles assigned to users. We want each role to be able to perform certain actions, and to do that, we need to give them the authorization to access the corresponding methods. This authorization process requires the role to be authenticated before the method can be executed. Authentication is how we confirm the identity of the user who is attempting to access a method or resource, while authorization determines what actions they are allowed to perform.

Spring Security is a framework that provides built-in authentication and authorization support. It also supports other security features for Java applications. There are two ways to carry out authentication and authorization; using HttpSecurity and with @PreAuthorize. For this tutorial, we will focus on @PreAuthorize, which is placed on controller methods.

In this tutorial, we will be looking at carrying out unit testing for a Spring Boot application that uses method-level security. This is done by running tests against a specific user, and for this, we need the JUnit annotations @WithMockUser, @WithAnonymousUser, and @WithUserDetails. These annotations are used to mock a user on which we can run tests against.

Spring Security Test Dependency

We start by adding the spring-security-test dependency to the pom.xml file.

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

Method Under Test

The below code example contains a getAllUsers() method that requires authentication for a user to access it.  In this example, authorization is needed to view all users.

public class UserImplementation implements UserService {
    UserRepo userRepo;
    
    @PreAuthorize("isAuthenticated()")
    public List<User> getAllUsers() {
        return userRepo.findAll();
    }
}

Test Methods

When we write a test that isn’t authenticated, it should return AuthenticationCredentialsNotFoundException.class

@Test
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

To test a method that requires authentication, we can use some of the following annotations in Spring Security:

@WithMockUser annotation

@WithMockUser annotation is used to mock a user whose default name is “user” and who has a password “password“. In the Spring Security context, the user has a default role of “USER“. Also, the @WithMockUser annotation simulates that the “user” has logged in.

@Test
@WithMockUser
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

@WithMockUser can also be implemented with a personalized username.

@Test
@WithMockUser("lizzie")
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

In this specific case, the annotation creates a mocked user with the username “lizzie” and with default user authorities. This means that any test method annotated with @WithMockUser("lizzie") will be executed as if the user with username “lizzie” is currently logged in and has the default authorities.

@WithMockUser can also be implemented with a personalized username, password, and role.

@Test
@WithMockUser(username = "lizzie", password = "password12", role = {"USER"}
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

Just like with the @WithMockUser("lizzie"), the above code example allows you to simulate a user who is authenticated in the system without actually going through the authentication process.

In this case, the annotation creates a mocked user with the username “lizzie”, password “password12”, and with the role “USER”. This means that any test method annotated with @WithMockUser(username = "lizzie", password = "password12", role = {"USER"}) will be executed as if the user with the specified username and password is currently logged in and has the specified role.

@WithAnonymousUser

@WithAnnonymousUser annotation simulates an anonymous user. It is equivalent to @WithMockUser(roles = {“ANONYMOUS”}) and @WithMockUser(authorities = {“ROLE_ANONYMOUS”}). We use this annotation in scenarios where we need to test for anonymous users.

@Test
@WithAnonymousUser
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

@WithUserDetails

@WithMockUser is a helpful tool when using Spring Security for testing. It’s important to note that it may not always work, especially when applications expect a specific Authentication principal. In such cases, you can customize the “UserDetailsService” to load users from a custom service. Users loaded using this custom service are often more reliable than the ones created with @WithMockUser.

On the other hand, @WithUserDetails requires an existing user. This means that you need to have a user already set up in the system for this annotation to work. Unlike @WithMockUser, it creates a real user with its associated authorities, which can be useful for testing scenarios where you need to test the behavior of your application for specific users.

Assuming the UserDetailsService is exposed as a bean, the principal returned will be the default username of user.

@Test
@WithUserDetails
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

Like with @WithMockUser, we can customize the username, and the principal returned will be of the customized username.

@Test
@WithUserDetails("lizzie")
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

We can also further customize our test by using the specific bean name. In this scenario, the username provided is looked up using the specified bean. In the example below, “lizzie” is looked up using the UserDetailsService with the bean name “myUserDetailsService”.

@Test
@WithUserDatils(value="lizzie", userDetailsServiceBeanName = "myUserDetailsService")
public void getAllUsers() throws Exception {
    List<User> users = userService.getAllUsers();
    System.out.println(users);
}

All these annotations can be placed at the class level so that all the test cases within the class use the same user.

Conclusion

I hope this tutorial was helpful to you.

If you would like to learn more about the @PreAuthorized annotation, check out this tutorial Spring Security @PreAuthorize Annotation Example or Spring Method-Level Security with @PreAuthorize.