Skip to main content

Week 6 Day 5 — Security Testing

Goal

Today I want to understand how to test Spring Security.

Main questions:

  1. Why do security tests matter?
  2. What dependency do I need?
  3. How do I test secured controllers with MockMvc?
  4. What is @WithMockUser?
  5. How do I test roles and authorities?
  6. How do I test 401 and 403?
  7. How do I test CSRF?
  8. How do I test HTTP Basic?
  9. How do I test method security?
  10. How do I test @PreAuthorize?
  11. How do I test custom principal logic?
  12. What are common security test bugs?

1. Quick Review from Week 6 Day 4

In Day 4, I learned:

  • CSRF means Cross-Site Request Forgery.
  • CSRF matters especially with browser cookies and sessions.
  • Stateless bearer-token APIs commonly disable CSRF, but not blindly.
  • CORS controls which browser origins may call the API.
  • Session security stores authentication state on the server.
  • Stateless security authenticates every request independently.
  • SessionCreationPolicy.STATELESS means no HTTP session for auth state.
  • JWT has header, payload, and signature.
  • JWT is signed, not necessarily encrypted.
  • Bearer token means possession grants access.
  • Public endpoints should be explicit.
  • Everything else should usually be authenticated.

Memory sentence:

Public by exception, protected by default.

Today I learn how to prove security rules with tests.


2. Why Security Tests Matter

Security bugs are dangerous.

Examples:

public endpoint accidentally protected
protected endpoint accidentally public
USER can access ADMIN endpoint
missing CSRF token still accepted
POST request fails in tests with 403
method security not enabled
@PreAuthorize not tested
JWT authorities mapped incorrectly
custom principal missing in test

Security tests help check:

401 when not authenticated
403 when authenticated but not allowed
200 when allowed
CSRF behavior
roles and authorities
method security
custom authorization logic

Memory sentence:

Security tests prove that allowed users are allowed and forbidden users are blocked.


3. Security Testing Dependency

Add this test dependency:

testImplementation("org.springframework.security:spring-security-test")

In Maven:

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

This gives useful test tools:

@WithMockUser
@WithAnonymousUser
@WithUserDetails
SecurityMockMvcRequestPostProcessors
csrf()
httpBasic()
user()
jwt()
oauth2Login()

Memory sentence:

spring-security-test gives test helpers for Spring Security.


4. Types of Security Tests

Common security test types:

controller security tests
URL authorization tests
CSRF tests
method security tests
JWT/resource server tests
custom permission tests
integration tests

Example questions:

Can anonymous user access public endpoint?
Can anonymous user access protected endpoint?
Can USER access user endpoint?
Can USER access admin endpoint?
Can ADMIN access admin endpoint?
Does POST fail without CSRF when CSRF is enabled?
Does @PreAuthorize block unauthorized service call?

Memory sentence:

Test both successful access and denied access.


5. MockMvc Security Testing

MockMvc lets me test MVC controllers without starting a real server.

Example:

@SpringBootTest
@AutoConfigureMockMvc
class TaskControllerSecurityTest {

@Autowired
private MockMvc mockMvc;
}

With Spring Boot and @AutoConfigureMockMvc, Spring Security filters are usually included.

Then I can test HTTP requests:

mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());

Memory sentence:

MockMvc can test secured endpoints through the Spring Security filter chain.


6. MockMvc with Manual Setup

Sometimes I configure MockMvc manually.

Then I need:

.apply(springSecurity())

Example:

@BeforeEach
void setup(WebApplicationContext context) {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}

This integrates Spring Security with MockMvc.

Memory sentence:

If MockMvc is built manually, apply springSecurity().


7. Test Example Controller

Controller:

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

@GetMapping
public List<String> list() {
return List.of("Task A", "Task B");
}

@PostMapping
public ResponseEntity<String> create(@RequestBody String body) {
return ResponseEntity.status(HttpStatus.CREATED).body("created");
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
return ResponseEntity.noContent().build();
}
}

Security config:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/tasks/**").hasAuthority("TASK_READ")
.requestMatchers(HttpMethod.POST, "/api/tasks/**").hasAuthority("TASK_WRITE")
.requestMatchers(HttpMethod.DELETE, "/api/tasks/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.build();
}

Rules:

GET /api/tasks needs TASK_READ
POST /api/tasks needs TASK_WRITE
DELETE /api/tasks/{id} needs ADMIN role
everything else needs authentication

8. @WithMockUser

@WithMockUser runs a test as a mocked authenticated user.

Example:

@Test
@WithMockUser
void authenticatedUserCanCallEndpoint() throws Exception {
mockMvc.perform(get("/api/profile"))
.andExpect(status().isOk());
}

Default mock user:

username = user
password = password
role = USER
authority = ROLE_USER

Memory sentence:

@WithMockUser creates an authenticated user for the test.


9. @WithMockUser with Roles

Example:

@Test
@WithMockUser(username = "admin@example.com", roles = "ADMIN")
void adminCanDeleteTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1")
.with(csrf()))
.andExpect(status().isNoContent());
}

Important:

roles = "ADMIN"

creates authority:

ROLE_ADMIN

Memory sentence:

@WithMockUser(roles = "ADMIN") gives authority ROLE_ADMIN.


10. @WithMockUser with Authorities

Example:

@Test
@WithMockUser(authorities = "TASK_READ")
void userWithTaskReadCanListTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}

This gives exact authority:

TASK_READ

Memory sentence:

Use authorities when testing fine-grained permissions.


11. Roles vs Authorities in Tests

Role test:

@WithMockUser(roles = "ADMIN")

creates:

ROLE_ADMIN

Authority test:

@WithMockUser(authorities = "ROLE_ADMIN")

creates exactly:

ROLE_ADMIN

Authority test:

@WithMockUser(authorities = "TASK_READ")

creates exactly:

TASK_READ

Common mistake:

@WithMockUser(roles = "ROLE_ADMIN")

Wrong, because roles add ROLE_.

Memory sentence:

In tests, roles add ROLE_; authorities are exact.


12. Testing 401 Unauthorized

401 means:

not authenticated

Test:

@Test
void anonymousCannotAccessProtectedEndpoint() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
}

No @WithMockUser.

No authentication.

Expected result:

401 Unauthorized

Memory sentence:

To test 401, make the request without authentication.


13. Testing 403 Forbidden

403 means:

authenticated but not allowed

Example:

@Test
@WithMockUser(roles = "USER")
void normalUserCannotDeleteTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1")
.with(csrf()))
.andExpect(status().isForbidden());
}

Why?

user is authenticated
but DELETE requires ADMIN

Memory sentence:

To test 403, authenticate as a user without enough permission.


14. Testing 200 OK

Example:

@Test
@WithMockUser(authorities = "TASK_READ")
void userWithTaskReadCanListTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}

Why?

GET /api/tasks requires TASK_READ
mock user has TASK_READ

Memory sentence:

A good security test includes a success case.


15. Testing 201 Created with CSRF

If CSRF is enabled, unsafe HTTP methods need CSRF token.

Unsafe methods:

POST
PUT
PATCH
DELETE

Test:

@Test
@WithMockUser(authorities = "TASK_WRITE")
void userWithTaskWriteCanCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Learn Security Testing"}
"""))
.andExpect(status().isCreated());
}

Memory sentence:

If CSRF is enabled, POST tests need .with(csrf()).


16. Testing Missing CSRF

Example:

@Test
@WithMockUser(authorities = "TASK_WRITE")
void postWithoutCsrfIsForbiddenWhenCsrfEnabled() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Learn Security Testing"}
"""))
.andExpect(status().isForbidden());
}

Expected result:

403 Forbidden

Why?

user is authenticated
user has authority
but CSRF token is missing

Memory sentence:

Missing CSRF token can cause 403 even when the user has permission.


17. Testing Invalid CSRF

Example:

@Test
@WithMockUser(authorities = "TASK_WRITE")
void postWithInvalidCsrfIsForbidden() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf().useInvalidToken())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Invalid CSRF"}
"""))
.andExpect(status().isForbidden());
}

Memory sentence:

Tests can check invalid CSRF tokens too.


18. When CSRF Is Disabled

If security config has:

.csrf(csrf -> csrf.disable())

then POST tests do not need:

.with(csrf())

Example:

@Test
@WithMockUser(authorities = "TASK_WRITE")
void postWorksWithoutCsrfWhenCsrfDisabled() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Stateless API"}
"""))
.andExpect(status().isCreated());
}

Memory sentence:

Whether tests need CSRF depends on security configuration.


19. Testing HTTP Basic

If config enables HTTP Basic:

.httpBasic(Customizer.withDefaults())

I can test with:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;

Example:

@Test
void validBasicAuthCanAccessEndpoint() throws Exception {
mockMvc.perform(get("/api/tasks")
.with(httpBasic("user@example.com", "password")))
.andExpect(status().isOk());
}

This tests real username/password authentication if users are configured.

Memory sentence:

httpBasic() tests Basic authentication through Spring Security.


20. @WithMockUser vs httpBasic

@WithMockUser:

puts a mock authenticated user into security context
does not test password checking
fast and simple
good for authorization tests

httpBasic():

sends username/password
tests authentication flow
uses UserDetailsService and PasswordEncoder
better for login/authentication integration tests

Memory sentence:

Use @WithMockUser for authorization, httpBasic() for authentication flow.


21. Testing Public Endpoints

Public endpoint:

.requestMatchers("/api/auth/**").permitAll()

Test:

@Test
void anonymousCanAccessLoginEndpoint() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"user@example.com","password":"password"}
"""))
.andExpect(status().isOk());
}

If CSRF is enabled, this POST may still need:

.with(csrf())

unless CSRF is disabled or ignored for that endpoint.

Memory sentence:

permitAll does not automatically disable CSRF.


22. Important Trap: permitAll vs CSRF

Config:

.requestMatchers("/api/auth/**").permitAll()

This means:

no authentication required

But if CSRF is enabled:

POST /api/auth/login may still need CSRF token

Memory sentence:

permitAll bypasses authentication, not necessarily CSRF.


23. Testing Anonymous User Explicitly

I can use:

@WithAnonymousUser

Example:

@Test
@WithAnonymousUser
void anonymousCannotAccessTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
}

This makes the anonymous case explicit.

Memory sentence:

@WithAnonymousUser makes anonymous tests clear.


24. Testing Controller with Current User

Controller:

@GetMapping("/api/me")
public MeDto me(Authentication authentication) {
return new MeDto(authentication.getName());
}

Test:

@Test
@WithMockUser(username = "steve@example.com")
void returnsCurrentUser() throws Exception {
mockMvc.perform(get("/api/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("steve@example.com"));
}

DTO:

public record MeDto(String email) {
}

Memory sentence:

@WithMockUser(username = "...") controls authentication.getName().


25. Testing Authorities in Controller

Controller:

@GetMapping("/api/me/authorities")
public List<String> authorities(Authentication authentication) {
return authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.toList();
}

Test:

@Test
@WithMockUser(authorities = {"TASK_READ", "TASK_WRITE"})
void returnsAuthorities() throws Exception {
mockMvc.perform(get("/api/me/authorities"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0]").value("TASK_READ"))
.andExpect(jsonPath("$[1]").value("TASK_WRITE"));
}

Memory sentence:

@WithMockUser(authorities = ...) controls exact authorities.


26. Testing Method Security

Method security protects service methods.

Example service:

@Service
public class AdminService {

@PreAuthorize("hasRole('ADMIN')")
public String adminOnly() {
return "secret";
}
}

Enable method security:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}

Test:

@SpringBootTest
class AdminServiceSecurityTest {

@Autowired
private AdminService adminService;

@Test
@WithMockUser(roles = "ADMIN")
void adminCanCallMethod() {
assertThat(adminService.adminOnly()).isEqualTo("secret");
}

@Test
@WithMockUser(roles = "USER")
void userCannotCallMethod() {
assertThatThrownBy(() -> adminService.adminOnly())
.isInstanceOf(AccessDeniedException.class);
}
}

Memory sentence:

Method security tests call the service method and expect success or AccessDeniedException.


27. Testing @PreAuthorize with Authorities

Service:

@Service
public class TaskService {

@PreAuthorize("hasAuthority('TASK_DELETE')")
public void deleteTask(Long taskId) {
// delete task
}
}

Test allowed:

@Test
@WithMockUser(authorities = "TASK_DELETE")
void userWithAuthorityCanDeleteTask() {
taskService.deleteTask(1L);
}

Test denied:

@Test
@WithMockUser(authorities = "TASK_READ")
void userWithoutAuthorityCannotDeleteTask() {
assertThatThrownBy(() -> taskService.deleteTask(1L))
.isInstanceOf(AccessDeniedException.class);
}

Memory sentence:

Test both matching and non-matching authorities.


28. Testing @PreAuthorize with Method Parameters

Service:

@Service
public class UserProfileService {

@PreAuthorize("#userId == authentication.principal.id")
public String getProfile(Long userId) {
return "profile";
}
}

This needs custom principal with id.

@WithMockUser creates a normal Spring Security user, not my custom principal.

So this test may fail:

@WithMockUser

because:

authentication.principal.id does not exist

Memory sentence:

@WithMockUser is not enough when expressions need custom principal fields.


29. Testing Custom Principal with Request PostProcessor

For controller tests, I can use:

.with(user(customPrincipal))

Custom principal:

AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);

Test:

@Test
void currentUserCanAccessOwnTenant() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);

mockMvc.perform(get("/api/tenants/5/tasks")
.with(user(principal)))
.andExpect(status().isOk());
}

Memory sentence:

Use .with(user(customPrincipal)) when controller needs custom principal.


30. Testing Custom Principal with @WithSecurityContext

For method security tests with custom principal, I can create a custom annotation using:

@WithSecurityContext

Example annotation:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockAppUserSecurityContextFactory.class)
public @interface WithMockAppUser {

long id() default 1L;

long tenantId() default 1L;

String email() default "user@example.com";

String[] authorities() default {"ROLE_USER"};
}

Factory:

public class WithMockAppUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockAppUser> {

@Override
public SecurityContext createSecurityContext(WithMockAppUser annotation) {
List<GrantedAuthority> authorities = Arrays.stream(annotation.authorities())
.map(SimpleGrantedAuthority::new)
.toList();

AppUserPrincipal principal = new AppUserPrincipal(
annotation.id(),
annotation.tenantId(),
annotation.email(),
authorities,
true
);

Authentication authentication =
new UsernamePasswordAuthenticationToken(
principal,
"password",
authorities
);

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);

return context;
}
}

Usage:

@Test
@WithMockAppUser(id = 10L, tenantId = 5L, authorities = {"TASK_READ"})
void canAccessOwnTenantData() {
taskService.findTenantTasks(5L);
}

Memory sentence:

Use custom @WithSecurityContext for custom principal method security tests.


31. Testing Custom Permission Service

Permission service:

@Component("permissionService")
public class PermissionService {

public boolean canAccessTask(Long taskId, Authentication authentication) {
AppUserPrincipal principal =
(AppUserPrincipal) authentication.getPrincipal();

return principal.getTenantId().equals(5L);
}
}

Service:

@PreAuthorize("@permissionService.canAccessTask(#taskId, authentication)")
public TaskDto findTask(Long taskId) {
return new TaskDto(taskId, "Test task");
}

Test:

@Test
@WithMockAppUser(tenantId = 5L)
void userWithPermissionCanAccessTask() {
TaskDto result = taskService.findTask(1L);

assertThat(result.id()).isEqualTo(1L);
}

Denied test:

@Test
@WithMockAppUser(tenantId = 99L)
void userWithoutPermissionCannotAccessTask() {
assertThatThrownBy(() -> taskService.findTask(1L))
.isInstanceOf(AccessDeniedException.class);
}

Memory sentence:

Custom permission logic should have allowed and denied tests.


32. Testing JWT Resource Server with MockMvc

For JWT-based APIs, Spring Security test support can mock a JWT.

Example:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;

Test:

@Test
void userWithJwtCanReadTasks() throws Exception {
mockMvc.perform(get("/api/tasks")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:read")
)))
.andExpect(status().isOk());
}

This does not require a real token.

It creates a mocked JWT authentication for the test.

Memory sentence:

Use jwt() to test resource-server authorization without creating a real JWT.


33. Testing JWT Claims

Example:

@Test
void jwtWithTenantClaimCanAccessTenantEndpoint() throws Exception {
mockMvc.perform(get("/api/tenants/5/tasks")
.with(jwt()
.jwt(jwt -> jwt
.subject("steve@example.com")
.claim("tenantId", 5L)
)
.authorities(new SimpleGrantedAuthority("SCOPE_task:read"))
))
.andExpect(status().isOk());
}

Useful when controller or converter uses JWT claims.

Memory sentence:

JWT tests can mock claims and authorities.


34. Testing Denied JWT Authority

Endpoint requires:

.hasAuthority("SCOPE_task:write")

Test:

@Test
void jwtWithoutWriteScopeCannotCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:read")
))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"New task"}
"""))
.andExpect(status().isForbidden());
}

If CSRF is enabled, add:

.with(csrf())

If stateless JWT config disables CSRF, no CSRF is needed.

Memory sentence:

JWT authority tests should check allowed and forbidden scopes.


35. Testing Login Endpoint

Login controller uses:

AuthenticationManager

Test with real configured user:

@Test
void loginWithValidCredentialsReturnsToken() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"email": "user@example.com",
"password": "password"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists());
}

Bad password:

@Test
void loginWithBadPasswordReturnsUnauthorized() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"email": "user@example.com",
"password": "wrong"
}
"""))
.andExpect(status().isUnauthorized());
}

If CSRF is enabled, login POST may need .with(csrf()).

Memory sentence:

Login tests should check valid and invalid credentials.


36. Testing with @WebMvcTest

@WebMvcTest loads only MVC-related components.

Example:

@WebMvcTest(TaskController.class)
class TaskControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private TaskService taskService;
}

With security enabled, @WebMvcTest can still apply security filters.

If the controller depends on security config or custom beans, I may need:

@Import(SecurityConfig.class)

or mock missing beans.

Memory sentence:

@WebMvcTest is sliced; import or mock security-related dependencies as needed.


37. Testing with @SpringBootTest

@SpringBootTest loads the full application context.

Example:

@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {

@Autowired
private MockMvc mockMvc;
}

Good for:

full security configuration
real filters
method security
repository/service integration
login flow
JWT/resource server config

Slower than @WebMvcTest.

Memory sentence:

Use @SpringBootTest for full security integration tests.


38. Choosing Test Type

Test TypeGood For
@WebMvcTestcontroller + URL security slice
@SpringBootTestfull security integration
service test with @WithMockUsermethod security
MockMvc with jwt()resource server authorization
MockMvc with httpBasic()Basic authentication flow
custom @WithSecurityContextcustom principal tests

Memory sentence:

Choose the smallest test that proves the security rule.


39. Testing @AuthenticationPrincipal

Controller:

@GetMapping("/api/me")
public MeDto me(@AuthenticationPrincipal AppUserPrincipal principal) {
return new MeDto(principal.getId(), principal.getUsername());
}

Test:

@Test
void returnsCustomPrincipalData() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);

mockMvc.perform(get("/api/me")
.with(user(principal)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(10))
.andExpect(jsonPath("$.email").value("steve@example.com"));
}

Memory sentence:

For @AuthenticationPrincipal, test with the same principal type the controller expects.


40. Testing Current User Passed to Service

Controller:

@GetMapping("/api/tasks")
public List<TaskDto> list(@AuthenticationPrincipal AppUserPrincipal principal) {
return taskService.listForTenant(principal.getTenantId());
}

Test:

@Test
void passesTenantIdToService() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);

when(taskService.listForTenant(5L))
.thenReturn(List.of(new TaskDto(1L, "Task A")));

mockMvc.perform(get("/api/tasks")
.with(user(principal)))
.andExpect(status().isOk());

verify(taskService).listForTenant(5L);
}

Memory sentence:

Controller security tests can verify that current-user data is passed correctly.


41. Common Bug: Test Passes Without Security

Bug:

mockMvc = MockMvcBuilders.standaloneSetup(controller).build();

This may not include Spring Security filters.

A test may pass even though real app would block the request.

Fix:

mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();

or use:

@SpringBootTest
@AutoConfigureMockMvc

Memory sentence:

Make sure security filters are actually active in security tests.


42. Common Bug: Expecting 401 but Getting 403

Possible causes:

CSRF token missing
user is authenticated but lacks permission
anonymous handling differs by config
request has invalid auth

Example:

@Test
@WithMockUser
void postFails() throws Exception {
mockMvc.perform(post("/api/tasks"))
.andExpect(status().isUnauthorized());
}

But actual result:

403

Why?

@WithMockUser means authenticated
POST may be missing CSRF
or user lacks TASK_WRITE

Memory sentence:

401 means no authentication; 403 means authenticated but blocked or CSRF denied.


43. Common Bug: Forgetting CSRF in POST Tests

Bug:

@Test
@WithMockUser(authorities = "TASK_WRITE")
void createTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}

Actual result:

403 Forbidden

Fix:

.with(csrf())

if CSRF is enabled.

Memory sentence:

403 on POST with correct authority often means missing CSRF.


44. Common Bug: Using Roles Wrong

Bug:

@WithMockUser(roles = "ROLE_ADMIN")

Expected:

ROLE_ADMIN

But roles add prefix and may become:

ROLE_ROLE_ADMIN

Fix:

@WithMockUser(roles = "ADMIN")

or:

@WithMockUser(authorities = "ROLE_ADMIN")

Memory sentence:

In @WithMockUser, roles should not include ROLE_.


45. Common Bug: Method Security Not Enabled

Service:

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}

Test expects AccessDeniedException, but method runs.

Possible reason:

@EnableMethodSecurity is missing

Fix:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}

Memory sentence:

@PreAuthorize needs method security enabled.


46. Common Bug: Testing Non-Spring Instance

Bad:

TaskService service = new TaskService(repository);
service.deleteTask(1L);

This bypasses Spring proxy.

Method security may not apply.

Correct:

@Autowired
private TaskService taskService;

Use the Spring bean from context.

Memory sentence:

Method security works on Spring-managed beans, not manually created objects.


47. Common Bug: Self-Invocation

Service:

public void outer() {
inner();
}

@PreAuthorize("hasRole('ADMIN')")
public void inner() {
}

If outer() calls inner() inside same class, method security may be bypassed.

Test should not only test inner() directly.

Also test real public entry points.

Memory sentence:

Test the method path that production code really uses.


48. Common Bug: @WithMockUser Does Not Use Database

@WithMockUser creates a mock user.

It does not load user from database.

So this test:

@WithMockUser(username = "real@example.com")

does not prove:

real@example.com exists in database
password is correct
UserDetailsService works

For that, use:

httpBasic()
formLogin()
custom login endpoint
@WithUserDetails
full integration test

Memory sentence:

@WithMockUser tests authorization context, not real login.


49. @WithUserDetails

@WithUserDetails loads a real user through UserDetailsService.

Example:

@Test
@WithUserDetails("user@example.com")
void realUserDetailsCanAccessEndpoint() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}

Requirement:

user must exist in test database or test UserDetailsService

Memory sentence:

@WithUserDetails uses the real UserDetailsService.


50. @WithMockUser vs @WithUserDetails

AnnotationUser SourceGood For
@WithMockUserfake test userauthorization tests
@WithUserDetailsreal UserDetailsServicetests needing real user loading
custom @WithSecurityContextcustom factorycustom principal tests

Memory sentence:

Pick the test user strategy based on what you need to prove.


51. Security Test Checklist

Use this checklist:

[ ] Public endpoints are accessible anonymously.
[ ] Protected endpoints return 401 anonymously.
[ ] Authenticated users can access allowed endpoints.
[ ] Authenticated users without permission get 403.
[ ] Admin endpoints require admin role.
[ ] Role prefix is tested correctly.
[ ] Authority rules are tested correctly.
[ ] POST/PUT/PATCH/DELETE tests include CSRF if CSRF is enabled.
[ ] Missing CSRF is tested where important.
[ ] Method security allowed case is tested.
[ ] Method security denied case is tested.
[ ] Custom permission service is tested.
[ ] Custom principal tests use correct principal type.
[ ] JWT tests include required authorities/scopes.
[ ] Security filters are active in MockMvc tests.
[ ] Tests cover both success and failure paths.

52. Real Exam Question: Security Test Dependency

Question:

Which dependency provides Spring Security test helpers?

Answer:

spring-security-test.


53. Real Exam Question: @WithMockUser

Question:

What does @WithMockUser do?

Answer:

It runs the test with a mocked authenticated user in the security context.


54. Real Exam Question: Default @WithMockUser

Question:

What role does default @WithMockUser have?

Answer:

The default mock user has role USER, which means authority ROLE_USER.


55. Real Exam Question: CSRF in Tests

Question:

How do I add a CSRF token in MockMvc?

Answer:

Use .with(csrf()).


56. Real Exam Question: Missing CSRF

Question:

What status can a POST request return if CSRF is enabled and token is missing?

Answer:

403 Forbidden.


57. Real Exam Question: 401 vs 403 in Tests

Question:

How do I test 401?

Answer:

Send the request without authentication.

Question:

How do I test 403?

Answer:

Authenticate as a user who lacks the required permission, or send an invalid/missing CSRF token for unsafe methods when CSRF is enabled.


58. Real Exam Question: Method Security Test

Question:

How do I test a method protected with @PreAuthorize?

Answer:

Call the Spring-managed service bean with a mock authenticated user and assert either success or AccessDeniedException.


59. Real Exam Question: @WithUserDetails

Question:

What is the difference between @WithMockUser and @WithUserDetails?

Answer:

@WithMockUser creates a fake user. @WithUserDetails loads a real user using UserDetailsService.


60. Interview Answer

Question:

How do you test endpoint authorization with MockMvc?

Good answer:

I use MockMvc with Spring Security test support. For anonymous access, I make the request without authentication and expect 401 for protected endpoints. For allowed users, I use @WithMockUser or request post processors like user() or jwt() with the required roles or authorities and expect 200. For forbidden users, I authenticate with insufficient authorities and expect 403. For unsafe HTTP methods, I include .with(csrf()) if CSRF is enabled.


61. Interview Answer

Question:

What is the difference between @WithMockUser, httpBasic(), and jwt() in tests?

Good answer:

@WithMockUser directly creates a mock authenticated user in the security context, so it is good for authorization tests. httpBasic() sends username and password through the actual Basic authentication flow, so it can test UserDetailsService and PasswordEncoder. jwt() creates a mock JWT authentication, useful for testing OAuth2 Resource Server or bearer-token authorization without generating a real token.


62. Interview Answer

Question:

Why do POST tests often fail with 403?

Good answer:

If CSRF protection is enabled, unsafe HTTP methods such as POST, PUT, PATCH, and DELETE require a valid CSRF token. Even if the user is authenticated and has the right role, the request can fail with 403 if the CSRF token is missing. In MockMvc tests, I add .with(csrf()).


63. Interview Answer

Question:

How do you test method security?

Good answer:

I enable method security with @EnableMethodSecurity, load the Spring-managed service bean, and use annotations like @WithMockUser or a custom @WithSecurityContext to set the security context. Then I test both allowed and denied cases. Allowed users should get the result; denied users should get AccessDeniedException.


64. Interview Answer

Question:

What are common security testing mistakes?

Good answer:

Common mistakes include forgetting spring-security-test, building MockMvc without Spring Security filters, using @WithMockUser with roles = "ROLE_ADMIN", forgetting CSRF for POST tests, expecting 401 when the user is actually authenticated but forbidden, testing manually created services instead of Spring beans, forgetting @EnableMethodSecurity, and using @WithMockUser when the code needs a custom principal.


65. Tiny Code Practice

Security rule:

.requestMatchers(HttpMethod.GET, "/api/tasks/**").hasAuthority("TASK_READ")
.requestMatchers(HttpMethod.POST, "/api/tasks/**").hasAuthority("TASK_WRITE")
.requestMatchers(HttpMethod.DELETE, "/api/tasks/**").hasRole("ADMIN")

Write tests:

@Test
void anonymousCannotReadTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockUser(authorities = "TASK_READ")
void taskReaderCanReadTasks() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(authorities = "TASK_READ")
void taskReaderCannotCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isForbidden());
}

@Test
@WithMockUser(authorities = "TASK_WRITE")
void taskWriterCanCreateTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}

@Test
@WithMockUser(roles = "ADMIN")
void adminCanDeleteTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1")
.with(csrf()))
.andExpect(status().isNoContent());
}

Questions:

  1. Which test checks 401?
  2. Which tests check 403?
  3. Which tests need CSRF?
  4. Why does admin use roles = "ADMIN"?
  5. Why does task reader use authorities = "TASK_READ"?

Answers:

  1. anonymousCannotReadTasks.
  2. taskReaderCannotCreateTask.
  3. POST and DELETE tests if CSRF is enabled.
  4. Because roles = "ADMIN" creates ROLE_ADMIN.
  5. Because the rule checks exact authority TASK_READ.

66. Tiny Bug Practice 1

Problem:

@Test
@WithMockUser(authorities = "TASK_WRITE")
void createTask() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}

Actual result:

403

Question:

What is likely wrong?

Answer:

CSRF token is missing. Add .with(csrf()) if CSRF is enabled.


67. Tiny Bug Practice 2

Problem:

@Test
@WithMockUser(roles = "ROLE_ADMIN")
void adminCanDelete() throws Exception {
mockMvc.perform(delete("/api/tasks/1").with(csrf()))
.andExpect(status().isNoContent());
}

Question:

What is wrong?

Answer:

roles should not include ROLE_. Use:

@WithMockUser(roles = "ADMIN")

or:

@WithMockUser(authorities = "ROLE_ADMIN")

68. Tiny Bug Practice 3

Problem:

TaskService taskService = new TaskService(taskRepository);
taskService.deleteTask(1L);

Question:

Why is this bad for method security testing?

Answer:

The service is manually created, not a Spring-managed bean. Method security proxy is bypassed. Autowire the service bean from Spring context.


69. Tiny Bug Practice 4

Problem:

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}

Test shows USER can still call it.

Question:

What might be missing?

Answer:

@EnableMethodSecurity may be missing, or the method is not called through a Spring-managed proxy.


Practice Questions and Answers

Question 1

Which dependency provides Spring Security test support?

Answer:

spring-security-test.


Question 2

What does @WithMockUser do?

Answer:

It runs the test with a mocked authenticated user in the security context.


Question 3

What user does default @WithMockUser create?

Answer:

A user with username user, password password, and role USER, meaning authority ROLE_USER.


Question 4

What is the difference between roles and authorities in @WithMockUser?

Answer:

roles automatically adds the ROLE_ prefix. authorities uses exact authority strings.


Question 5

How do I test a 401 response?

Answer:

Make the request without authentication and expect status().isUnauthorized().


Question 6

How do I test a 403 response?

Answer:

Authenticate as a user without enough permission and expect status().isForbidden().


Question 7

How do I add CSRF token in MockMvc?

Answer:

Use .with(csrf()).


Question 8

When do POST tests need CSRF?

Answer:

When CSRF protection is enabled and the method is unsafe, such as POST, PUT, PATCH, or DELETE.


Question 9

What does httpBasic() test?

Answer:

It sends username/password through HTTP Basic authentication and can test the authentication flow.


Question 10

What is the difference between @WithMockUser and httpBasic()?

Answer:

@WithMockUser directly creates a mock authenticated user. httpBasic() sends credentials through the real Basic authentication flow.


Question 11

What does @WithUserDetails do?

Answer:

It loads a real user through UserDetailsService.


Question 12

How do I test a JWT resource server endpoint?

Answer:

Use MockMvc with .with(jwt()) and provide required authorities or claims.


Question 13

How do I test method security?

Answer:

Call the Spring-managed service bean with a mock authenticated user and assert success or AccessDeniedException.


Question 14

What exception should be expected when method security denies access?

Answer:

AccessDeniedException.


Question 15

Why might @WithMockUser not work with custom principal expressions?

Answer:

Because @WithMockUser creates a standard mock user, not my custom principal with fields like id or tenantId.


Question 16

How can I test with a custom principal in MockMvc?

Answer:

Use .with(user(customPrincipal)).


Question 17

What is @WithSecurityContext useful for?

Answer:

It is useful for creating custom security context annotations for tests, especially when a custom principal is needed.


Question 18

Why might MockMvc tests pass even though real security would block the request?

Answer:

Because MockMvc may be built without Spring Security filters, for example using standalone setup without applying springSecurity().


Question 19

Why is @WithMockUser(roles = "ROLE_ADMIN") wrong?

Answer:

Because roles adds ROLE_ automatically, so it may create ROLE_ROLE_ADMIN.


Question 20

Why should I test both allowed and denied cases?

Answer:

Because security must prove that correct users are allowed and incorrect users are blocked.

Final Memory Sentences

  • Security tests prove access rules.
  • Add spring-security-test.
  • MockMvc can test secured endpoints.
  • If MockMvc is built manually, apply springSecurity().
  • @WithMockUser creates a mock authenticated user.
  • Default @WithMockUser has role USER.
  • roles = "ADMIN" creates ROLE_ADMIN.
  • authorities = "TASK_READ" creates exact authority TASK_READ.
  • Test 401 with no authentication.
  • Test 403 with authenticated user lacking permission.
  • POST/PUT/PATCH/DELETE need .with(csrf()) if CSRF is enabled.
  • permitAll does not automatically disable CSRF.
  • httpBasic() tests Basic authentication flow.
  • @WithMockUser does not test real password authentication.
  • @WithUserDetails uses real UserDetailsService.
  • jwt() tests JWT resource server authorization.
  • Method security tests should call Spring-managed service beans.
  • Denied method security usually throws AccessDeniedException.
  • @EnableMethodSecurity is required for @PreAuthorize.
  • Custom principals may need .with(user(customPrincipal)) or custom @WithSecurityContext.
  • Always test allowed and denied cases.