Spring Security Testing
Introduction
Testing security in your applications is just as important as implementing it correctly. Spring Security provides robust tools and utilities that help developers ensure their security configurations work as expected. By thoroughly testing security implementations, you can prevent potential vulnerabilities and ensure that your application's security mechanisms function correctly.
In this guide, we'll explore how to effectively test Spring Security in your Spring Boot applications. We'll cover both unit testing and integration testing approaches, along with best practices and common patterns.
Understanding Spring Security Test Support
Spring Security provides dedicated test support through the spring-security-test
module. This module offers utilities for setting up security contexts, performing requests with authentication, and testing method security.
To get started with Spring Security testing, add the following dependency to your project:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
For Gradle users:
testImplementation 'org.springframework.security:spring-security-test'
Testing Authentication and Authorization
MockMvc Integration with Spring Security
Spring Security integrates with Spring MVC Test (MockMvc) to provide an easy way to test secured endpoints. Let's look at a basic example:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void whenUnauthenticated_thenUnauthorizedResponse() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
}
@Test
void whenAuthenticatedAsUser_thenSuccess() throws Exception {
mockMvc.perform(get("/api/users")
.with(user("testuser").roles("USER")))
.andExpect(status().isOk());
}
@Test
void whenAuthenticatedAsAdminAccessingAdminEndpoint_thenSuccess() throws Exception {
mockMvc.perform(get("/api/admin")
.with(user("admin").roles("ADMIN")))
.andExpect(status().isOk());
}
@Test
void whenAuthenticatedAsUserAccessingAdminEndpoint_thenForbidden() throws Exception {
mockMvc.perform(get("/api/admin")
.with(user("testuser").roles("USER")))
.andExpect(status().isForbidden());
}
}
In this example, we're testing different authentication scenarios:
- Testing access to a protected endpoint without authentication
- Testing access with a user role to a user endpoint
- Testing access with an admin role to an admin endpoint
- Testing access restriction when a user tries to access an admin endpoint
@WithMockUser Annotation
Spring Security Test provides the @WithMockUser
annotation to simplify authentication in tests:
import org.springframework.security.test.context.support.WithMockUser;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "testuser", roles = {"USER"})
void whenUserAuthenticated_thenSuccess() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void whenAdminAuthenticated_thenAdminEndpointAccessible() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
}
The @WithMockUser
annotation creates a mock UserDetails
object and adds it to the SecurityContext
for the duration of the test.
Custom Authentication with @WithUserDetails
If your application uses custom user details, you can use the @WithUserDetails
annotation:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails("[email protected]") // Loads user from UserDetailsService
void whenAuthenticatedWithCustomUserDetails_thenSuccess() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk());
}
}
This annotation will load a user with the specified username from your application's UserDetailsService
.
Testing Method Security
Spring Security also supports method-level security using annotations like @PreAuthorize
and @PostAuthorize
. Here's how to test these:
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public User createUser(User user) {
// Create user logic
return user;
}
@PreAuthorize("hasRole('USER')")
public List<User> getAllUsers() {
// Get all users logic
return new ArrayList<>();
}
}
To test this service:
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.access.AccessDeniedException;
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
@WithMockUser(roles = "ADMIN")
void whenAdminCreatesUser_thenSuccess() {
User newUser = new User("testuser", "password");
User created = userService.createUser(newUser);
assertNotNull(created);
}
@Test
@WithMockUser(roles = "USER")
void whenUserCreatesUser_thenAccessDenied() {
User newUser = new User("testuser", "password");
assertThrows(AccessDeniedException.class, () -> {
userService.createUser(newUser);
});
}
@Test
@WithMockUser(roles = "USER")
void whenUserGetsAllUsers_thenSuccess() {
List<User> users = userService.getAllUsers();
assertNotNull(users);
}
}
Testing JWT Authentication
If your application uses JWT for authentication, you can test it using a custom JWT token:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
@WebMvcTest(UserController.class)
class JwtAuthenticationTest {
@Autowired
private MockMvc mockMvc;
@Test
void whenValidJwt_thenSuccess() throws Exception {
mockMvc.perform(get("/api/users")
.with(jwt().jwt(builder ->
builder.subject("[email protected]")
.claim("scope", "ROLE_USER")
)))
.andExpect(status().isOk());
}
@Test
void whenJwtWithAdminRole_thenAdminEndpointAccessible() throws Exception {
mockMvc.perform(get("/api/admin")
.with(jwt().jwt(builder ->
builder.subject("[email protected]")
.claim("scope", "ROLE_ADMIN")
)))
.andExpect(status().isOk());
}
}
Real-world Example: E-Commerce API Security Testing
Let's look at a more comprehensive example for an e-commerce application:
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void whenPublicEndpoint_thenNoAuthRequired() throws Exception {
// Setup mock service
when(productService.getPublicProducts())
.thenReturn(Arrays.asList(new Product("1", "Public Product", 10.0)));
mockMvc.perform(get("/api/products/public"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Public Product"));
}
@Test
@WithMockUser(roles = "USER")
void whenAuthenticatedUserGetsProducts_thenSuccess() throws Exception {
// Setup mock service
when(productService.getAllProducts())
.thenReturn(Arrays.asList(
new Product("1", "Product 1", 10.0),
new Product("2", "Product 2", 20.0)
));
mockMvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Product 1"))
.andExpect(jsonPath("$[1].name").value("Product 2"));
}
@Test
@WithMockUser(roles = "ADMIN")
void whenAdminCreatesProduct_thenSuccess() throws Exception {
// Setup mock service
Product newProduct = new Product(null, "New Product", 15.0);
Product savedProduct = new Product("3", "New Product", 15.0);
when(productService.createProduct(any(Product.class))).thenReturn(savedProduct);
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"New Product\",\"price\":15.0}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value("3"))
.andExpect(jsonPath("$.name").value("New Product"));
}
@Test
@WithMockUser(roles = "USER")
void whenUserAttemptsToCreateProduct_thenForbidden() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"New Product\",\"price\":15.0}"))
.andExpect(status().isForbidden());
}
}
This example tests:
- Public endpoints that don't require authentication
- Endpoints that require user authentication
- Admin endpoints that require admin privileges
- Authorization failures when users try to access admin endpoints
Integration Testing with Spring Security
For full integration tests that start the entire Spring context, use @SpringBootTest
with TestRestTemplate
:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SecurityIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
@Test
void whenAccessingPublicEndpoint_thenSuccess() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/public", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
void whenAccessingProtectedEndpointWithoutAuth_thenUnauthorized() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/users", String.class);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}
@Test
void whenAccessingProtectedEndpointWithAuth_thenSuccess() {
// Create credentials
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth("user", "password");
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:" + port + "/api/users",
HttpMethod.GET,
new HttpEntity<>(headers),
String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
Testing CSRF Protection
Spring Security enables CSRF protection by default. Here's how to test endpoints with CSRF tokens:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@WebMvcTest(UserController.class)
class CsrfProtectionTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser
void whenPostWithoutCsrf_thenForbidden() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"newuser\",\"password\":\"password\"}"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser
void whenPostWithCsrf_thenSuccess() throws Exception {
mockMvc.perform(post("/api/users")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"newuser\",\"password\":\"password\"}"))
.andExpect(status().isCreated());
}
}
Best Practices for Spring Security Testing
-
Test both positive and negative scenarios: Ensure endpoints return appropriate responses for both authorized and unauthorized access.
-
Test with different user roles: Verify that your authorization rules work correctly for different user roles.
-
Test CSRF protection: Confirm that CSRF protection works as expected for state-changing operations.
-
Test authentication mechanisms: Test all authentication methods your application uses (form-based, JWT, OAuth, etc.).
-
Test security headers: Verify that security headers like
X-XSS-Protection
andContent-Security-Policy
are correctly set. -
Test method-level security: Don't forget to test annotations like
@PreAuthorize
on your service methods. -
Use realistic data: When possible, use realistic data and scenarios that match your production environment.
Summary
Spring Security testing is essential for ensuring that your application's security mechanisms function correctly. The spring-security-test
module provides numerous utilities to make testing authentication, authorization, and other security features straightforward.
We've covered:
- Setting up Spring Security testing
- Testing authentication with
@WithMockUser
and other annotations - Testing authorization for different user roles
- Integration testing with security
- Testing CSRF protection
- Best practices for effective security testing
By implementing comprehensive security tests, you can detect vulnerabilities early and ensure that your application properly protects user data and resources.
Additional Resources and Exercises
Resources
- Spring Security Testing Documentation
- Spring Security Testing Cheat Sheet
- Testing Web Applications with Spring Security
Practice Exercises
-
Basic Authentication Testing: Create a small application with form-based authentication and write tests to verify that authentication works correctly.
-
Role-Based Security: Implement a REST API with different endpoints protected by different roles, and write tests to verify access control.
-
JWT Authentication Testing: Extend your application to use JWT for authentication, and write tests that create and validate JWT tokens.
-
Custom UserDetailsService Testing: Implement a custom
UserDetailsService
that loads users from a database, and write tests to ensure it integrates correctly with Spring Security. -
Method Security Testing: Add method security annotations (
@PreAuthorize
,@Secured
) to your service layer and write tests to verify they work correctly.
Happy testing, and remember that security testing is just as important as the security implementation itself!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)