Article

Security annotations in Spring tests

Simplify user authentication in Spring tests with standard security annotations or even your own.

Spring Test Spring Security Spring Boot
Intermediate
Florian Beaufumé
Florian Beaufumé
Published 8 Dec 2023 - Last updated 10 Mar 2024 - 5 min read
Security annotations in Spring tests

Table of contents

Introduction

Integration tests quite often have to call secured business code. For Spring applications, the business code may use security constraints defined by @Secured or @PreAuthorize annotations. The test code, for example in @SpringBootTest annotated classes, must provide matching user information.

To simplify this task, Spring Security offers several useful standard annotations such as @WithMockUser, @WithUserDetails and @WithSecurityContext.

In this article I will describe these annotations. I will also show how we can alias these annotations with shorter custom annotations such as @WithAdministrator, @WithInMemoryUser and @WithDatabaseUser.

Note that this article is part of a test oriented series:

  1. Getting started with Spring tests provides best practices and tips to get started with Spring tests.
  2. Switching between H2 and Testcontainers in Spring tests describes how to easily switch between H2 and Testcontainers in Spring tests.
  3. Security annotations in Spring tests (this article) focuses on security annotations used in Spring tests.

A GitHub sample repository link is provided at the end of this article.

@WithMockUser

Spring Security offers several annotations for tests that call secured code: @WithMockUser, @WithUserDetails, @WithSecurityContext. They are increasingly powerful and flexible.

To use them, we declare the spring-security-test Maven dependency in addition to the usual spring-boot-starter-test:

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

@WithMockUser can be used when the security constraints require little information, such as a username, some roles and/or some authorities:

@Test
@WithMockUser(username = "administrator", roles = {"ADMINISTRATOR"})
public void modifyItemAsAdmin() {
// Call some business code that requires the ADMINISTRATOR role, e.g. protected by
// @Secured("ROLE_ADMINISTRATOR") or @PreAuthorize("hasRole('ADMINISTRATOR')")
}

@WithMockUser(username = "administrator", roles = {"ADMINISTRATOR"}) is nice, but can feel verbose when used multiple times in the tests. Instead, we can define a custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "administrator", roles = {"ADMINISTRATOR"})
public @interface WithAdministrator {
}

Then use it in the test method:

@Test
@WithAdministrator
public void modifyItemAsAdmin() {
// Call some business code that requires the ADMINISTRATOR role
}

Of course, we can use this for as many custom annotations as we need, e.g. @WithAccountManager, @WithOperator, etc.

@WithUserDetails

@WithMockUser can be used to provide a very limited set of user attributes. But sometimes the business code or its security constraints need additional user attributes (first name, last name, profile, email, date of birth, etc). That's where we can use @WithUserDetails.

To use these additional attributes, application often implement a custom org.springframework.security.core.userdetails.UserDetails class and a custom org.springframework.security.core.userdetails.UserDetailsService:

public class CustomUserDetails implements UserDetails {

private String username

// Other attributes, constructor from a User entity, getters, etc
}
@Service
public class DatabaseUserDetailsService implements UserDetailsService {

@Autowired private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username);

if (user == null) {
throw new UsernameNotFoundException(username);
}

return new CustomUserDetails(user);
}

Then, an integration test class may insert a user in the database in a @BeforeAll method and leverage @WithUserDetails to use that user in some test methods:

@SpringBootTest
public class ItemServiceTest {

@BeforeAll
public static beforeAll(@Autowired UserRepository userRepository) {
userRepository.save(new User("paul-smith", "Paul", "Smith", "ADMIN", "paul.smith@acme.com"));
}

@AfterAll
public static afterAll(@Autowired UserRepository userRepository) {
// Leave a clean database for other test classes
userRepository.deleteAll();
}

@Test
@WithUserDetails("paul-smith")
public void createItemAsPaulSmith() {
// Call some business code that requires a CustomUserDetails
}
}

This is very useful if the business code needs to work on a persistent user, for example to load some other business data associated to that user through JPA relations. But sometimes we do not need a full blown persistent user, only a CustomUserDetails. In that case we can simplify the test code.

To do so, we create a new custom UserDetailsService dedicated to tests (therefore in src/test/java) that uses in-memory users (or use org.springframework.security.provisioning.InMemoryUserDetailsManager from Spring):

@Service
public class InMemoryUserDetailsService implements UserDetailsService {

private final Map<String, UserDetails> users = new HashMap<>();

public InMemoryUserDetailsService() {
users.put("paul-smith", new CustomUserDetails(
new User("paul-smith", "Paul", "Smith", "ADMIN", "paul.smith@acme.com")));
// Add other users, as needed
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.get(username);
}
}

Then, we simplify the test class by removing the insert/delete code in the @BeforeAll/@AfterAll methods and by using the new UserDetailsService in @WithUserDetails:

@SpringBootTest
public class ItemServiceTest {

@Test
@WithUserDetails(value = "paul-smith", userDetailsServiceBeanName = "inMemoryUserDetailsService")
public void createItemAsPaulSmith() {
// Call some business code that requires a specific user
}
}

Now we can switch between database users and in-memory users by simply pointing to the right UserDetailsService in the userDetailsServiceBeanName attribute.

A bit of warning, at this point we have two custom UserDetailsService implementations in our application. As a consequence using @WithUserDetails("paul-smith") will fail since Spring cannot choose the right service to use. The userDetailsServiceBeanName attribute is now mandatory. To pick a database user, simply use @WithUserDetails(value = "paul-smith", userDetailsServiceBeanName = "databaseUserDetailsService").

@WithSecurityContext

In the previous section, we saw how we can use @WithUserDetails to authenticate with a database user or with an in-memory user by simply setting the userDetailsServiceBeanName attribute to the right value. In this section, I will show how to write custom annotations in order to get rid of that userDetailsServiceBeanName attribute.

@WithSecurityContext is a standard Spring Security annotation that can be used when we need a custom authentication in tests. It allows us to manually create Authentication instances and set them in the current SecurityContext of Spring. As an example, and also because this article focuses on security annotations, I will show how this can be applied to write a custom annotation (I named it @WithInMemoryUser for clarity purpose) that loads users from the memory and that does not need the userDetailsServiceBeanName attribute.

First, we create the custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithInMemoryUserSecurityContextFactory.class)
public @interface WithInMemoryUser {

String value(); // This is the username
}

Then, we write the WithInMemoryUserSecurityContextFactory factory mentioned in the @WithSecurityContext annotation:

public class WithInMemoryUserSecurityContextFactory implements WithSecurityContextFactory<WithInMemoryUser> {

@Autowired private InMemoryUserDetailsService inMemoryUserDetailsService;

@Override
public SecurityContext createSecurityContext(WithInMemoryUser annotation) {
// Load the user details using the values from the annotation and from a chosen user detail service
// Note that the actual return type is CustomUserDetails
UserDetails userDetails = inMemoryUserDetailsService.loadUserByUsername(annotation.value());

// Build the authentication and set it to the Spring Security context
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, userDetails.getPassword(), userDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);

return context;
}
}

Finally, we can use the annotation in tests:

@Test
@WithInMemoryUser("paul-smith")
public void createItemAsPaulSmith() {
// Call some business code that requires a specific user
}

Of course, we can use this approach to write another annotation, for example @WithDatabaseUser that load users from the database, by changing the implementation of the createSecurityContext method. Then we can mix and match @WithInMemoryUser and WithDatabaseUser as needed in the test code.

Conclusion

In this article, I showed how the Spring Security annotations such as @WithMockUser, @WithUserDetails and @WithSecurityContext can be used to simplify users authentication in tests. I also showed how we can write our own custom annotations to further simplify the test code.

A sample project is available in GitHub, see spring-tests-security-annotations.

© 2007-2024 Florian Beaufumé