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:
A GitHub sample repository link is provided at the end of this article.
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.
@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")
.
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.
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é