Course topics

By WebNest Studio

Spring Boot Tutorial

Testing Secured Endpoints

Security configuration is code, and like all code it breaks — usually when someone adds a new endpoint or reorders a rule. The only reliable way to know that /api/admin/** really rejects ordinary users is an automated test that tries it.

Spring Security's test support lets you run requests as any user, with any roles, or with any JWT, without a real login flow. This lesson covers @WithMockUser, MockMvc request post-processors for users, JWTs, OAuth2 logins and CSRF tokens, and testing method security directly on services.

Test Dependencies in Spring Boot 4

Spring Boot 4 splits test support per technology. For a web API with security, add spring-boot-starter-webmvc-test and spring-security-test with test scope. @WebMvcTest loads only the web layer — controllers, advice, filters and your SecurityFilterChain — so security tests stay fast. Remember to @Import your security configuration class if it is not picked up automatically, and replace service dependencies with @MockitoBean (which replaced the removed @MockBean).

Choosing How to Represent the User

Pick the tool that matches how your application authenticates.

  • @WithMockUser(roles = "ADMIN") — annotation on the test method; creates a simple authenticated user.
  • @WithUserDetails("asha") — loads a real user from your UserDetailsService bean.
  • .with(user("asha").roles("USER")) — the same as @WithMockUser but per request.
  • .with(jwt().authorities(...)) — a JwtAuthenticationToken for resource servers; lets you set claims too.
  • .with(oauth2Login()) / .with(oidcLogin()) — simulate social login.
  • .with(csrf()) — adds a valid CSRF token for POST/PUT/DELETE when CSRF is enabled.

What to Assert

For every protected area, test at least three cases: anonymous (expect 401 or a login redirect), authenticated without permission (expect 403), and authenticated with permission (expect success). Add tests for ownership rules ("user A cannot read user B's order") because those are the checks most often forgotten.

Testing Method Security Without HTTP

@PreAuthorize rules on services can be tested with @SpringBootTest (or a small slice that includes the service and @EnableMethodSecurity) and @WithMockUser. Call the service method directly and assert that it either returns or throws AccessDeniedException.

Examples

Test dependencies (pom.xml)

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
Output
(Versions are managed by the Spring Boot parent; no version tags needed.)

Web-layer security tests with @WithMockUser and request post-processors

Java
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.*;

@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminControllerSecurityTest {

    @Autowired MockMvc mvc;
    @MockitoBean ReportService reportService;

    @Test
    void anonymousUsersAreRejected() throws Exception {
        mvc.perform(get("/api/admin/reports")).andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(roles = "USER")
    void ordinaryUsersAreForbidden() throws Exception {
        mvc.perform(get("/api/admin/reports")).andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void adminsCanReadReports() throws Exception {
        when(reportService.summary()).thenReturn(new Summary(42));
        mvc.perform(get("/api/admin/reports"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.orders").value(42));
    }

    @Test
    void postWithoutCsrfTokenIsRejected() throws Exception {
        mvc.perform(post("/admin/settings").with(user("admin").roles("ADMIN")))
           .andExpect(status().isForbidden());
        mvc.perform(post("/admin/settings").with(user("admin").roles("ADMIN")).with(csrf()))
           .andExpect(status().is3xxRedirection());
    }
}
Output
AdminControllerSecurityTest
  ✔ anonymousUsersAreRejected
  ✔ ordinaryUsersAreForbidden
  ✔ adminsCanReadReports
  ✔ postWithoutCsrfTokenIsRejected
Tests run: 4, Failures: 0

Testing a JWT resource server with claims and scopes

Java
@WebMvcTest(OrderController.class)
@Import(ResourceServerConfig.class)   // the SecurityFilterChain only, without the key-loading beans
class OrderControllerJwtTest {

    @Autowired MockMvc mvc;
    @MockitoBean JwtDecoder jwtDecoder;   // jwt() bypasses decoding, so no real keys are needed

    @Test
    void userScopeCanListOwnOrders() throws Exception {
        mvc.perform(get("/api/orders")
                .with(jwt().jwt(j -> j.subject("asha"))
                           .authorities(new SimpleGrantedAuthority("SCOPE_user"))))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$[0]").value("Order #1001 for asha"));
    }

    @Test
    void userScopeCannotDelete() throws Exception {
        mvc.perform(delete("/api/orders/5")
                .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_user"))))
           .andExpect(status().isForbidden());
    }
}
Output
OrderControllerJwtTest
  ✔ userScopeCanListOwnOrders
  ✔ userScopeCannotDelete

Testing @PreAuthorize ownership rules on a service

Java
@SpringBootTest
class OrderServiceSecurityTest {

    @Autowired OrderService orderService;

    @Test
    @WithMockUser(username = "asha@webnest.in")
    void userCanReadOwnOrders() {
        assertThatNoException().isThrownBy(() -> orderService.findForCustomer("asha@webnest.in"));
    }

    @Test
    @WithMockUser(username = "asha@webnest.in")
    void userCannotReadSomeoneElsesOrders() {
        assertThatThrownBy(() -> orderService.findForCustomer("ravi@webnest.in"))
            .isInstanceOf(AccessDeniedException.class);
    }

    @Test
    @WithMockUser(username = "admin", roles = "ADMIN")
    void adminCanReadAnyOrders() {
        assertThatNoException().isThrownBy(() -> orderService.findForCustomer("ravi@webnest.in"));
    }
}
Output
OrderServiceSecurityTest
  ✔ userCanReadOwnOrders
  ✔ userCannotReadSomeoneElsesOrders
  ✔ adminCanReadAnyOrders

Common Mistakes

  • Testing only the "happy path" as an admin and never asserting that ordinary or anonymous users are rejected.
  • Forgetting .with(csrf()) on POST tests and concluding that authorization is broken when the 403 actually comes from the CSRF filter.
  • Using @WebMvcTest without importing the real SecurityConfig, so tests run against Spring Boot's default security instead of yours.
  • Still using @MockBean, which was removed in Spring Boot 4 — use @MockitoBean.
  • Using @WithMockUser(roles = "ROLE_ADMIN"); the roles attribute adds the prefix itself, so this produces ROLE_ROLE_ADMIN.

Key Points to Remember

  • Add spring-security-test and the per-technology test starter such as spring-boot-starter-webmvc-test.
  • Use @WithMockUser, user(), jwt(), oauth2Login() and csrf() to simulate any authentication state.
  • For each protected area, test anonymous, forbidden and allowed cases.
  • Test ownership rules on services directly and assert AccessDeniedException.
  • Import your real security configuration into slice tests.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.