--- name: unit-test-security-authorization description: Unit tests for Spring Security with @PreAuthorize, @Secured, @RolesAllowed. Test role-based access control and authorization policies. Use when validating security configurations and access control logic. category: testing tags: [junit-5, spring-security, authorization, roles, preauthorize, mockmvc] version: 1.0.1 --- # Unit Testing Security and Authorization Test Spring Security authorization logic using @PreAuthorize, @Secured, and custom permission evaluators. Verify access control decisions without full security infrastructure. ## When to Use This Skill Use this skill when: - Testing @PreAuthorize and @Secured method-level security - Testing role-based access control (RBAC) - Testing custom permission evaluators - Verifying access denied scenarios - Testing authorization with authenticated principals - Want fast authorization tests without full Spring Security context ## Setup: Security Testing ### Maven ```xml org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-test test org.springframework.security spring-security-test test ``` ### Gradle ```kotlin dependencies { implementation("org.springframework.boot:spring-boot-starter-security") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") } ``` ## Basic Pattern: Testing @PreAuthorize ### Simple Role-Based Access Control ```java // Service with security annotations @Service public class UserService { @PreAuthorize("hasRole('ADMIN')") public void deleteUser(Long userId) { // delete logic } @PreAuthorize("hasRole('USER')") public User getCurrentUser() { // get user logic } @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')") public List listAllUsers() { // list logic } } // Unit test import org.junit.jupiter.api.Test; import org.springframework.security.test.context.support.WithMockUser; import static org.assertj.core.api.Assertions.*; class UserServiceSecurityTest { @Test @WithMockUser(roles = "ADMIN") void shouldAllowAdminToDeleteUser() { UserService service = new UserService(); assertThatCode(() -> service.deleteUser(1L)) .doesNotThrowAnyException(); } @Test @WithMockUser(roles = "USER") void shouldDenyUserFromDeletingUser() { UserService service = new UserService(); assertThatThrownBy(() -> service.deleteUser(1L)) .isInstanceOf(AccessDeniedException.class); } @Test @WithMockUser(roles = "ADMIN") void shouldAllowAdminAndManagerToListUsers() { UserService service = new UserService(); assertThatCode(() -> service.listAllUsers()) .doesNotThrowAnyException(); } @Test void shouldDenyAnonymousUserAccess() { UserService service = new UserService(); assertThatThrownBy(() -> service.deleteUser(1L)) .isInstanceOf(AccessDeniedException.class); } } ``` ## Testing @Secured Annotation ### Legacy Security Configuration ```java @Service public class OrderService { @Secured("ROLE_ADMIN") public Order approveOrder(Long orderId) { // approval logic } @Secured({"ROLE_ADMIN", "ROLE_MANAGER"}) public List getOrders() { // get orders } } class OrderSecurityTest { @Test @WithMockUser(roles = "ADMIN") void shouldAllowAdminToApproveOrder() { OrderService service = new OrderService(); assertThatCode(() -> service.approveOrder(1L)) .doesNotThrowAnyException(); } @Test @WithMockUser(roles = "USER") void shouldDenyUserFromApprovingOrder() { OrderService service = new OrderService(); assertThatThrownBy(() -> service.approveOrder(1L)) .isInstanceOf(AccessDeniedException.class); } } ``` ## Testing Controller Security with MockMvc ### Secure REST Endpoints ```java @RestController @RequestMapping("/api/admin") public class AdminController { @GetMapping("/users") @PreAuthorize("hasRole('ADMIN')") public List listAllUsers() { // logic } @DeleteMapping("/users/{id}") @PreAuthorize("hasRole('ADMIN')") public void deleteUser(@PathVariable Long id) { // delete logic } } // Testing with MockMvc import org.springframework.security.test.context.support.WithMockUser; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class AdminControllerSecurityTest { private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders .standaloneSetup(new AdminController()) .apply(springSecurity()) .build(); } @Test @WithMockUser(roles = "ADMIN") void shouldAllowAdminToListUsers() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isOk()); } @Test @WithMockUser(roles = "USER") void shouldDenyUserFromListingUsers() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isForbidden()); } @Test void shouldDenyAnonymousAccessToAdminEndpoint() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(roles = "ADMIN") void shouldAllowAdminToDeleteUser() throws Exception { mockMvc.perform(delete("/api/admin/users/1")) .andExpect(status().isOk()); } } ``` ## Testing Expression-Based Authorization ### Complex Permission Expressions ```java @Service public class DocumentService { @PreAuthorize("hasRole('ADMIN') or authentication.principal.username == #owner") public Document getDocument(String owner, Long docId) { // get document } @PreAuthorize("hasPermission(#docId, 'Document', 'WRITE')") public void updateDocument(Long docId, String content) { // update logic } @PreAuthorize("#userId == authentication.principal.id") public UserProfile getUserProfile(Long userId) { // get profile } } class ExpressionBasedSecurityTest { @Test @WithMockUser(username = "alice", roles = "ADMIN") void shouldAllowAdminToAccessAnyDocument() { DocumentService service = new DocumentService(); assertThatCode(() -> service.getDocument("bob", 1L)) .doesNotThrowAnyException(); } @Test @WithMockUser(username = "alice") void shouldAllowOwnerToAccessOwnDocument() { DocumentService service = new DocumentService(); assertThatCode(() -> service.getDocument("alice", 1L)) .doesNotThrowAnyException(); } @Test @WithMockUser(username = "alice") void shouldDenyUserAccessToOtherUserDocument() { DocumentService service = new DocumentService(); assertThatThrownBy(() -> service.getDocument("bob", 1L)) .isInstanceOf(AccessDeniedException.class); } @Test @WithMockUser(username = "alice", id = "1") void shouldAllowUserToAccessOwnProfile() { DocumentService service = new DocumentService(); assertThatCode(() -> service.getUserProfile(1L)) .doesNotThrowAnyException(); } @Test @WithMockUser(username = "alice", id = "1") void shouldDenyUserAccessToOtherProfile() { DocumentService service = new DocumentService(); assertThatThrownBy(() -> service.getUserProfile(999L)) .isInstanceOf(AccessDeniedException.class); } } ``` ## Testing Custom Permission Evaluator ### Create and Test Custom Permission Logic ```java // Custom permission evaluator @Component public class DocumentPermissionEvaluator implements PermissionEvaluator { private final DocumentRepository documentRepository; public DocumentPermissionEvaluator(DocumentRepository documentRepository) { this.documentRepository = documentRepository; } @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { if (authentication == null) return false; Document document = (Document) targetDomainObject; String userUsername = authentication.getName(); return document.getOwner().getUsername().equals(userUsername) || userHasRole(authentication, "ADMIN"); } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { if (authentication == null) return false; if (!"Document".equals(targetType)) return false; Document document = documentRepository.findById((Long) targetId).orElse(null); if (document == null) return false; return hasPermission(authentication, document, permission); } private boolean userHasRole(Authentication authentication, String role) { return authentication.getAuthorities().stream() .anyMatch(auth -> auth.getAuthority().equals("ROLE_" + role)); } } // Unit test for custom evaluator class DocumentPermissionEvaluatorTest { private DocumentPermissionEvaluator evaluator; private DocumentRepository documentRepository; private Authentication adminAuth; private Authentication userAuth; private Document document; @BeforeEach void setUp() { documentRepository = mock(DocumentRepository.class); evaluator = new DocumentPermissionEvaluator(documentRepository); document = new Document(1L, "Test Doc", new User("alice")); adminAuth = new UsernamePasswordAuthenticationToken( "admin", null, List.of(new SimpleGrantedAuthority("ROLE_ADMIN")) ); userAuth = new UsernamePasswordAuthenticationToken( "alice", null, List.of(new SimpleGrantedAuthority("ROLE_USER")) ); } @Test void shouldGrantPermissionToDocumentOwner() { boolean hasPermission = evaluator.hasPermission(userAuth, document, "WRITE"); assertThat(hasPermission).isTrue(); } @Test void shouldDenyPermissionToNonOwner() { Authentication otherUserAuth = new UsernamePasswordAuthenticationToken( "bob", null, List.of(new SimpleGrantedAuthority("ROLE_USER")) ); boolean hasPermission = evaluator.hasPermission(otherUserAuth, document, "WRITE"); assertThat(hasPermission).isFalse(); } @Test void shouldGrantPermissionToAdmin() { boolean hasPermission = evaluator.hasPermission(adminAuth, document, "WRITE"); assertThat(hasPermission).isTrue(); } @Test void shouldDenyNullAuthentication() { boolean hasPermission = evaluator.hasPermission(null, document, "WRITE"); assertThat(hasPermission).isFalse(); } } ``` ## Testing Multiple Roles ### Parameterized Role Testing ```java import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class RoleBasedAccessTest { private AdminService service; @BeforeEach void setUp() { service = new AdminService(); } @ParameterizedTest @ValueSource(strings = {"ADMIN", "SUPER_ADMIN", "SYSTEM"}) @WithMockUser(roles = "ADMIN") void shouldAllowPrivilegedRolesToDeleteUser(String role) { assertThatCode(() -> service.deleteUser(1L)) .doesNotThrowAnyException(); } @ParameterizedTest @ValueSource(strings = {"USER", "GUEST", "READONLY"}) void shouldDenyUnprivilegedRolesToDeleteUser(String role) { assertThatThrownBy(() -> service.deleteUser(1L)) .isInstanceOf(AccessDeniedException.class); } } ``` ## Best Practices - **Use @WithMockUser** for setting authenticated user context - **Test both allow and deny cases** for each security rule - **Test with different roles** to verify role-based decisions - **Test expression-based security** comprehensively - **Mock external dependencies** (permission evaluators, etc.) - **Test anonymous access separately** from authenticated access - **Use @EnableGlobalMethodSecurity** in configuration for method-level security ## Common Pitfalls - Forgetting to enable method security in test configuration - Not testing both allow and deny scenarios - Testing framework code instead of authorization logic - Not handling null authentication in tests - Mixing authentication and authorization tests unnecessarily ## Troubleshooting **AccessDeniedException not thrown**: Ensure `@EnableGlobalMethodSecurity(prePostEnabled = true)` is configured. **@WithMockUser not working**: Verify Spring Security test dependencies are on classpath. **Custom PermissionEvaluator not invoked**: Check `@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)`. ## References - [Spring Security Method Security](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc-method) - [Spring Security Testing](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test) - [@WithMockUser Documentation](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/test/context/support/WithMockUser.html)