Testing Spring Security: @WithMockUser, MockMvc, and SecurityMockMvc
Why Security Tests Are Non-Negotiable
A security configuration that is never tested is a security configuration that is probably wrong. URL patterns have subtle ordering rules. Method security annotations are silently ignored if @EnableMethodSecurity is missing. CSRF tokens must be present in the right form. Testing is the only way to know your authorization rules are actually enforced.
Test Dependencies
<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>
@WithMockUser
The simplest way to simulate an authenticated user. Sets up a SecurityContext with a UsernamePasswordAuthenticationToken containing the specified authorities.
@SpringBootTest
@AutoConfigureMockMvc
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void regularUserCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/admin/users"))
.andExpect(status().isForbidden());
}
@Test
void unauthenticatedUserIsRedirectedToLogin() throws Exception {
mockMvc.perform(get("/admin/users"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
}
@WithMockUser Parameters
@WithMockUser(
username = "alice", // default: "user"
password = "password", // default: "password"
roles = {"ADMIN", "MANAGER"}, // adds ROLE_ prefix
authorities = {"user:read"} // exact strings, no prefix
)
roles and authorities are mutually exclusive in practice — use authorities when you need exact authority strings without the ROLE_ prefix.
@WithUserDetails
@WithMockUser creates a synthetic user. @WithUserDetails loads a real user from your UserDetailsService — use it when tests need to verify behavior tied to the actual user’s authorities or custom UserDetails properties.
@SpringBootTest
@AutoConfigureMockMvc
class DocumentControllerTest {
@Test
@WithUserDetails(value = "alice@example.com", userDetailsServiceBeanName = "userDetailsServiceImpl")
void userCanReadOwnDocuments() throws Exception {
mockMvc.perform(get("/api/documents"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].ownerId").value("alice@example.com"));
}
}
The user alice@example.com must exist in your test database (or be present in a mocked UserDetailsService).
Custom @WithMockUser Annotations
Avoid repeating @WithMockUser(roles = "ADMIN") on every test by creating a meta-annotation:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "admin", roles = "ADMIN")
public @interface WithMockAdmin {}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "alice", roles = "USER", authorities = "user:read")
public @interface WithMockRegularUser {}
@Test
@WithMockAdmin
void adminCanDeleteUsers() throws Exception {
mockMvc.perform(delete("/api/users/42"))
.andExpect(status().isNoContent());
}
Custom SecurityContext Factory
For complex authentication objects (OAuth2 tokens, custom Authentication implementations), use WithSecurityContextFactory:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockJwtUserFactory.class)
public @interface WithMockJwtUser {
String username() default "user";
String[] scopes() default {"read"};
String[] roles() default {"ROLE_USER"};
}
public class WithMockJwtUserFactory implements WithSecurityContextFactory<WithMockJwtUser> {
@Override
public SecurityContext createSecurityContext(WithMockJwtUser annotation) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String scope : annotation.scopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
}
for (String role : annotation.roles()) {
authorities.add(new SimpleGrantedAuthority(role));
}
Jwt jwt = Jwt.withTokenValue("test-token")
.header("alg", "RS256")
.claim("sub", annotation.username())
.claim("scope", String.join(" ", annotation.scopes()))
.issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(3600))
.build();
JwtAuthenticationToken auth = new JwtAuthenticationToken(jwt, authorities, annotation.username());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(auth);
return context;
}
}
Usage:
@Test
@WithMockJwtUser(username = "alice", scopes = {"read", "write"}, roles = {"ROLE_USER"})
void userWithWriteScopeCanCreateResource() throws Exception {
mockMvc.perform(post("/api/resources")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"test\"}"))
.andExpect(status().isCreated());
}
Testing CSRF
By default, MockMvc disables CSRF. To test that CSRF is enforced:
@SpringBootTest
@AutoConfigureMockMvc
class CsrfTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser
void postWithoutCsrfTokenIsRejected() throws Exception {
mockMvc.perform(post("/account/update")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("name", "Alice"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser
void postWithCsrfTokenSucceeds() throws Exception {
mockMvc.perform(post("/account/update")
.with(csrf()) // SecurityMockMvcRequestPostProcessors.csrf()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("name", "Alice"))
.andExpect(status().isOk());
}
}
Import csrf() from org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.
SecurityMockMvcRequestPostProcessors
SecurityMockMvcRequestPostProcessors provides request-scoped authentication and other security utilities:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
// Authenticate as a specific user for one request
mockMvc.perform(get("/api/profile")
.with(user("alice").roles("USER")))
.andExpect(status().isOk());
// Authenticate with HTTP Basic credentials
mockMvc.perform(get("/api/protected")
.with(httpBasic("alice", "password")))
.andExpect(status().isOk());
// JWT Bearer token
mockMvc.perform(get("/api/protected")
.with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_read"))))
.andExpect(status().isOk());
// OAuth2 login
mockMvc.perform(get("/dashboard")
.with(oauth2Login().authorities(new SimpleGrantedAuthority("ROLE_USER"))))
.andExpect(status().isOk());
// OIDC login
mockMvc.perform(get("/dashboard")
.with(oidcLogin().idToken(token -> token.claim("email", "alice@example.com"))))
.andExpect(status().isOk());
Testing Method Security
Method security tests require the full Spring context and work at the service layer, not HTTP:
@SpringBootTest
class UserServiceSecurityTest {
@Autowired
private UserService userService;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanGetAllUsers() {
assertDoesNotThrow(() -> userService.getAllUsers());
}
@Test
@WithMockUser(roles = "USER")
void regularUserCannotGetAllUsers() {
assertThrows(AccessDeniedException.class, () -> userService.getAllUsers());
}
@Test
@WithMockUser(username = "alice", roles = "USER")
void userCanReadOwnProfile() {
assertDoesNotThrow(() -> userService.getUserByUsername("alice"));
}
@Test
@WithMockUser(username = "alice", roles = "USER")
void userCannotReadOtherProfile() {
assertThrows(AccessDeniedException.class,
() -> userService.getUserByUsername("bob"));
}
}
Testing OAuth2 Resource Server (JWT)
@SpringBootTest
@AutoConfigureMockMvc
class ApiSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void unauthenticatedRequestReturns401() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
}
@Test
void requestWithValidJwtSucceeds() throws Exception {
mockMvc.perform(get("/api/users")
.with(jwt()
.authorities(new SimpleGrantedAuthority("ROLE_USER"))
.jwt(token -> token.subject("alice@example.com"))
))
.andExpect(status().isOk());
}
@Test
void requestWithoutRequiredScopeReturns403() throws Exception {
mockMvc.perform(post("/api/users")
.with(jwt()
.authorities(new SimpleGrantedAuthority("SCOPE_read")) // missing write
)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"bob\"}"))
.andExpect(status().isForbidden());
}
@Test
void requestWithWriteScopeSucceeds() throws Exception {
mockMvc.perform(post("/api/users")
.with(jwt()
.authorities(
new SimpleGrantedAuthority("SCOPE_read"),
new SimpleGrantedAuthority("SCOPE_write")
)
)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"bob\"}"))
.andExpect(status().isCreated());
}
}
Testing the Security Config Directly
Verify that your SecurityFilterChain rules work correctly with a slice test:
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private UserDetailsService userDetailsService;
@Test
void publicEndpointIsAccessibleWithoutAuth() throws Exception {
mockMvc.perform(get("/api/users/public-profile/alice"))
.andExpect(status().isOk());
}
@Test
void privateEndpointRequiresAuthentication() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser
void authenticatedUserCanAccessPrivateEndpoint() throws Exception {
when(userService.getProfile(any())).thenReturn(new UserProfile());
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk());
}
}
Test Coverage Checklist
For every protected endpoint, write tests covering:
- Unauthenticated access → 401 or redirect to login
- Authenticated but wrong role → 403
- Authenticated with correct role → 2xx
- State-changing requests without CSRF → 403 (for form-based apps)
- State-changing requests with CSRF → 2xx
- For method security: both allowed and denied subjects
For URL authorization rules, test exact boundaries:
@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdminPrefix() throws Exception {
mockMvc.perform(get("/admin")).andExpect(status().isForbidden());
mockMvc.perform(get("/admin/")).andExpect(status().isForbidden());
mockMvc.perform(get("/admin/anything")).andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccessAdminPrefix() throws Exception {
mockMvc.perform(get("/admin")).andExpect(status().isOk());
}
Key Takeaways
@WithMockUseris the fastest way to simulate authentication;@WithUserDetailsloads real users from yourUserDetailsService- Create meta-annotations (
@WithMockAdmin) to reduce boilerplate across large test suites - Use
WithSecurityContextFactoryfor customAuthenticationtypes (JWT, OAuth2) csrf()in MockMvc simulates a valid CSRF token — test both the presence and absence of itSecurityMockMvcRequestPostProcessorsprovidesjwt(),oauth2Login(),oidcLogin(), andhttpBasic()for request-level authentication- Test method security at the service layer, not the HTTP layer — use
assertThrows(AccessDeniedException.class, ...)
Next: Actuator Security and Production Hardening — lock down Spring Boot Actuator endpoints and apply production security patterns.