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

  • @WithMockUser is the fastest way to simulate authentication; @WithUserDetails loads real users from your UserDetailsService
  • Create meta-annotations (@WithMockAdmin) to reduce boilerplate across large test suites
  • Use WithSecurityContextFactory for custom Authentication types (JWT, OAuth2)
  • csrf() in MockMvc simulates a valid CSRF token — test both the presence and absence of it
  • SecurityMockMvcRequestPostProcessors provides jwt(), oauth2Login(), oidcLogin(), and httpBasic() 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.