Skip to main content

Spring Test Security

Introduction

When building secure applications with Spring, testing security configurations is a crucial aspect of your testing strategy. Spring Test Security provides tools and utilities to help you write tests for applications that use Spring Security, enabling you to verify authentication, authorization, and other security-related functionality without compromising the integrity of your test environment.

In this guide, you'll learn how to effectively test Spring applications that incorporate Spring Security. We'll cover setting up a secure test environment, mocking authentication, testing secured endpoints, and verifying authorization rules.

Prerequisites

Before diving into Spring Test Security, you should have:

  • Basic knowledge of Spring Framework
  • Familiarity with Spring Security concepts
  • Understanding of JUnit and testing fundamentals

Adding Spring Security Test Dependencies

First, let's add the necessary dependencies to your project:

For Maven:

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

For Gradle:

groovy
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

Setting Up a Secure Test Environment

Let's create a simple secured REST application that we'll use as an example for our tests. We'll have a UserController that provides different endpoints with different security requirements.

java
@RestController
@RequestMapping("/api/users")
public class UserController {

@GetMapping("/public")
public String publicEndpoint() {
return "This is a public endpoint";
}

@GetMapping("/authenticated")
public String authenticatedEndpoint() {
return "This endpoint requires authentication";
}

@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
return "This endpoint requires ADMIN role";
}

@GetMapping("/user")
@PreAuthorize("hasRole('USER')")
public String userEndpoint() {
return "This endpoint requires USER role";
}
}

Our security configuration might look like this:

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/users/public").permitAll()
.requestMatchers("/api/users/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());

return http.build();
}
}

Testing Authentication with @WithMockUser

The @WithMockUser annotation is one of the simplest ways to mock authentication for your tests. It creates a mock SecurityContext with a UserDetails object representing the user you specify.

java
@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
public void publicEndpoint_ShouldBeAccessibleToAll() throws Exception {
mockMvc.perform(get("/api/users/public"))
.andExpect(status().isOk())
.andExpect(content().string("This is a public endpoint"));
}

@Test
public void authenticatedEndpoint_ShouldRequireAuthentication() throws Exception {
mockMvc.perform(get("/api/users/authenticated"))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockUser
public void authenticatedEndpoint_WithAuthentication_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/authenticated"))
.andExpect(status().isOk())
.andExpect(content().string("This endpoint requires authentication"));
}
}

By default, @WithMockUser creates a user with username "user", password "password", and role "USER". You can customize these attributes:

java
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
public void adminEndpoint_WithAdminRole_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/admin"))
.andExpect(status().isOk())
.andExpect(content().string("This endpoint requires ADMIN role"));
}

@Test
@WithMockUser(roles = {"USER"})
public void adminEndpoint_WithUserRole_ShouldReturnForbidden() throws Exception {
mockMvc.perform(get("/api/users/admin"))
.andExpect(status().isForbidden());
}

Testing with Custom User Details

If your application uses a custom implementation of UserDetails, you can use @WithUserDetails annotation to load a user from your UserDetailsService.

First, let's create a test configuration that provides a test UserDetailsService:

java
@TestConfiguration
public class TestSecurityConfig {

@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();

UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN", "USER")
.build();

return new InMemoryUserDetailsManager(user, admin);
}
}

Then, use @WithUserDetails in your tests:

java
@WebMvcTest(UserController.class)
@Import(TestSecurityConfig.class)
class UserControllerWithDetailsTest {

@Autowired
private MockMvc mockMvc;

@Test
@WithUserDetails("admin")
public void adminEndpoint_WithAdminUser_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/admin"))
.andExpect(status().isOk());
}

@Test
@WithUserDetails("user")
public void userEndpoint_WithUserDetails_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/user"))
.andExpect(status().isOk());
}
}

Testing with Custom Authentication

For more complex authentication scenarios, you can use the SecurityMockMvcRequestPostProcessors class to customize your test requests:

java
@WebMvcTest(UserController.class)
class CustomAuthenticationTest {

@Autowired
private MockMvc mockMvc;

@Test
public void authenticatedEndpoint_WithHttpBasic_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/authenticated")
.with(httpBasic("testuser", "password")))
.andExpect(status().isOk());
}

@Test
public void adminEndpoint_WithCustomAuth_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/admin")
.with(user("admin").roles("ADMIN")))
.andExpect(status().isOk());
}
}

Testing Method Security

Spring Security allows you to secure methods using annotations like @PreAuthorize, @PostAuthorize, @Secured, etc. To test these scenarios:

java
@Service
public class UserService {

@PreAuthorize("hasRole('ADMIN')")
public String getAdminData() {
return "Admin data";
}

@PreAuthorize("hasRole('USER')")
public String getUserData() {
return "User data";
}
}

Testing this service:

java
@SpringBootTest
class UserServiceTest {

@Autowired
private UserService userService;

@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccessAdminData() {
String result = userService.getAdminData();
assertEquals("Admin data", result);
}

@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdminData() {
assertThrows(AccessDeniedException.class, () -> {
userService.getAdminData();
});
}

@Test
@WithMockUser(roles = "USER")
void userCanAccessUserData() {
String result = userService.getUserData();
assertEquals("User data", result);
}
}

Testing with OAuth2 and JWT

For applications using OAuth2 or JWT-based authentication, Spring Security Test provides specialized support:

java
@WebMvcTest(UserController.class)
class OAuth2SecurityTest {

@Autowired
private MockMvc mockMvc;

@Test
public void authenticatedEndpoint_WithOAuth2_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/authenticated")
.with(oauth2Login()))
.andExpect(status().isOk());
}

@Test
public void adminEndpoint_WithJwt_ShouldSucceed() throws Exception {
mockMvc.perform(get("/api/users/admin")
.with(jwt().authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))))
.andExpect(status().isOk());
}
}

Working with CSRF Protection

Spring Security enables CSRF protection by default for non-GET requests. When testing POST, PUT, or DELETE endpoints, you'll need to include CSRF tokens:

java
@WebMvcTest(UserController.class)
class CsrfTest {

@Autowired
private MockMvc mockMvc;

@Test
@WithMockUser
public void createUser_WithCsrf_ShouldSucceed() throws Exception {
mockMvc.perform(post("/api/users")
.content("{\"name\":\"John\",\"email\":\"[email protected]\"}")
.contentType(MediaType.APPLICATION_JSON)
.with(csrf()))
.andExpect(status().isOk());
}

@Test
@WithMockUser
public void createUser_WithoutCsrf_ShouldFail() throws Exception {
mockMvc.perform(post("/api/users")
.content("{\"name\":\"John\",\"email\":\"[email protected]\"}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden());
}
}

Real-World Example: Testing a Complete Secured API

Let's put everything together to test a more realistic secured API. We'll create a simple blog API with different permission levels.

First, our entity and repository:

java
@Entity
public class BlogPost {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
private String author;
// getters and setters
}

public interface BlogRepository extends JpaRepository<BlogPost, Long> {
}

Then our service with security rules:

java
@Service
public class BlogService {

private final BlogRepository blogRepository;

public BlogService(BlogRepository blogRepository) {
this.blogRepository = blogRepository;
}

@PreAuthorize("permitAll()")
public List<BlogPost> getAllPosts() {
return blogRepository.findAll();
}

@PreAuthorize("isAuthenticated()")
public BlogPost getPost(Long id) {
return blogRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found"));
}

@PreAuthorize("hasRole('AUTHOR') or hasRole('ADMIN')")
public BlogPost createPost(BlogPost post) {
return blogRepository.save(post);
}

@PreAuthorize("hasRole('ADMIN') or (hasRole('AUTHOR') and principal.username == #post.author)")
public BlogPost updatePost(BlogPost post) {
return blogRepository.save(post);
}

@PreAuthorize("hasRole('ADMIN')")
public void deletePost(Long id) {
blogRepository.deleteById(id);
}
}

Now, let's write comprehensive tests for this service:

java
@SpringBootTest
class BlogServiceSecurityTest {

@Autowired
private BlogService blogService;

@MockBean
private BlogRepository blogRepository;

private BlogPost samplePost;

@BeforeEach
void setUp() {
samplePost = new BlogPost();
samplePost.setId(1L);
samplePost.setTitle("Test Post");
samplePost.setContent("Test Content");
samplePost.setAuthor("user1");

given(blogRepository.findAll()).willReturn(List.of(samplePost));
given(blogRepository.findById(1L)).willReturn(Optional.of(samplePost));
given(blogRepository.save(any(BlogPost.class))).willAnswer(i -> i.getArgument(0));
}

@Test
@WithAnonymousUser
void anonymousUsers_CanGetAllPosts() {
List<BlogPost> posts = blogService.getAllPosts();
assertEquals(1, posts.size());
}

@Test
@WithAnonymousUser
void anonymousUsers_CannotGetSinglePost() {
assertThrows(AccessDeniedException.class, () -> {
blogService.getPost(1L);
});
}

@Test
@WithMockUser
void authenticatedUsers_CanGetSinglePost() {
BlogPost post = blogService.getPost(1L);
assertEquals("Test Post", post.getTitle());
}

@Test
@WithMockUser
void regularUsers_CannotCreatePosts() {
assertThrows(AccessDeniedException.class, () -> {
blogService.createPost(new BlogPost());
});
}

@Test
@WithMockUser(roles = "AUTHOR")
void authors_CanCreatePosts() {
BlogPost newPost = new BlogPost();
newPost.setTitle("New Post");

BlogPost created = blogService.createPost(newPost);
assertNotNull(created);
}

@Test
@WithMockUser(username = "user1", roles = "AUTHOR")
void authors_CanUpdateTheirOwnPosts() {
samplePost.setTitle("Updated Title");
BlogPost updated = blogService.updatePost(samplePost);
assertEquals("Updated Title", updated.getTitle());
}

@Test
@WithMockUser(username = "another_user", roles = "AUTHOR")
void authors_CannotUpdateOthersPosts() {
assertThrows(AccessDeniedException.class, () -> {
blogService.updatePost(samplePost);
});
}

@Test
@WithMockUser(roles = "ADMIN")
void admins_CanDeletePosts() {
assertDoesNotThrow(() -> {
blogService.deletePost(1L);
});
}
}

Common Pitfalls and Best Practices

1. Test Configuration Isolation

Ensure your test security configuration doesn't interfere with your application security configuration:

java
@TestConfiguration
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class TestSecurityConfig {
// Test security beans
}

2. Using @WithSecurityContext for Complex Authentication

For complex authentication scenarios, implement a custom @WithSecurityContext annotation:

java
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithCustomUserSecurityContextFactory.class)
public @interface WithCustomUser {
String username() default "user";
String[] roles() default {"USER"};
String[] permissions() default {};
}

public class WithCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomUser> {
@Override
public SecurityContext createSecurityContext(WithCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();

// Create your custom Authentication object
CustomUserDetails principal = new CustomUserDetails(customUser.username(),
customUser.roles(),
customUser.permissions());
Authentication auth = new UsernamePasswordAuthenticationToken(principal,
"password",
principal.getAuthorities());

context.setAuthentication(auth);
return context;
}
}

3. Testing with Integration Tests

For complete end-to-end testing with security, use @SpringBootTest with a real server:

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FullSecurityIntegrationTest {

@Autowired
private TestRestTemplate restTemplate;

@Test
void publicEndpoint_NoAuth_ShouldSucceed() {
ResponseEntity<String> response = restTemplate.getForEntity("/api/users/public", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}

@Test
void authenticatedEndpoint_WithAuth_ShouldSucceed() {
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("user", "password");

ResponseEntity<String> response = restTemplate.exchange(
"/api/users/authenticated",
HttpMethod.GET,
new HttpEntity<>(headers),
String.class
);

assertEquals(HttpStatus.OK, response.getStatusCode());
}
}

Summary

In this guide, you've learned how to effectively test Spring applications with security enabled:

  • Using @WithMockUser for simple authentication mocking
  • Working with @WithUserDetails for custom user details
  • Testing method-level security with @PreAuthorize and similar annotations
  • Handling CSRF protection in tests
  • Implementing tests for OAuth2 and JWT authentication
  • Creating comprehensive tests for a secured REST API
  • Following best practices for Spring Security testing

By incorporating these testing techniques into your development workflow, you can ensure that your application's security configuration is working as expected, protecting sensitive resources while allowing legitimate access.

Additional Resources

Here are some resources to deepen your understanding of Spring Test Security:

Exercises

  1. Create a Spring Boot application with a REST API that has endpoints with different security requirements (public, authenticated, role-based).
  2. Write tests using @WithMockUser to verify the security rules.
  3. Implement a custom UserDetailsService and test it using @WithUserDetails.
  4. Add method security (@PreAuthorize) to a service layer and write tests to verify access control.
  5. Create a test that uses JWT authentication with different claims and verify the access based on these claims.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)