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:
<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:
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.
@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:
@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.
@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:
@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
:
@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:
@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:
@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:
@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:
@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:
@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:
@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:
@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:
@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:
@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:
@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:
@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:
@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
- Create a Spring Boot application with a REST API that has endpoints with different security requirements (public, authenticated, role-based).
- Write tests using
@WithMockUser
to verify the security rules. - Implement a custom
UserDetailsService
and test it using@WithUserDetails
. - Add method security (
@PreAuthorize
) to a service layer and write tests to verify access control. - 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! :)