Skip to content

How to Test Spring Boot APIs with Spring Security Enabled During Development

I was building a multi-tenant RBAC application with Spring Boot, and everything was going smoothly until I tried to test my APIs. Every request returned 401 Unauthorized. I knew Spring Security was doing its job, but I needed a way to test different user roles and scenarios without constantly logging in and managing tokens manually.

The Problem

Here’s what I was dealing with:

  • Every API endpoint required authentication
  • I needed to test different roles (USER, ADMIN, TENANT_ADMIN)
  • JWT tokens kept expiring during manual testing
  • I couldn’t easily test unauthorized access scenarios

I wanted to keep security enabled but make my development workflow efficient. After all, disabling security entirely defeats the purpose of testing the actual behavior.

Approach 1: Spring Security Test Annotations (For Unit Tests)

The cleanest solution for automated tests is using Spring Security’s built-in test annotations. These let you simulate authenticated users without dealing with actual tokens.

ApiSecurityTests.java
@SpringBootTest
@AutoConfigureMockMvc
class ApiSecurityTests {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "user", roles = {"USER"})
void testUserEndpoint() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdminEndpoints() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
void unauthenticatedUserIsRejected() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isUnauthorized());
}
}

This approach works great for automated tests, but what about manual testing with Postman or curl?

Approach 2: Development Profile with Relaxed Security

For manual testing during development, I created a separate security configuration that only activates in the dev profile.

DevSecurityConfig.java
@Configuration
@EnableWebSecurity
@Profile("dev")
public class DevSecurityConfig {
@Bean
public SecurityFilterChain devFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.csrf(csrf -> csrf.disable());
return http.build();
}
}

This works, but I quickly realized it wasn’t ideal for testing role-based access control. I was bypassing the actual security logic, which meant I wasn’t testing what would happen in production.

Approach 3: Dev-Only Token Generation Endpoint

The best middle ground I found was creating a development-only endpoint that generates test tokens with specific roles. This keeps security enabled while making manual testing much easier.

DevTokenController.java
@RestController
@RequestMapping("/api/dev")
@Profile({"dev", "test"})
public class DevTokenController {
@Autowired
private JwtService jwtService;
@GetMapping("/test-users")
public List<Map<String, Object>> getTestUsers() {
return List.of(
Map.of("username", "user", "password", "pass", "roles", List.of("USER")),
Map.of("username", "admin", "password", "pass", "roles", List.of("ADMIN", "USER")),
Map.of("username", "tenant-admin", "password", "pass", "roles", List.of("TENANT_ADMIN", "USER"))
);
}
@PostMapping("/generate-token")
public Map<String, String> generateToken(@RequestParam String username,
@RequestParam String[] roles) {
String token = jwtService.generateToken(
username,
Arrays.asList(roles),
Duration.ofHours(24)
);
return Map.of(
"token", token,
"Authorization", "Bearer " + token
);
}
}

Now I can quickly generate tokens for different roles:

Token Generation Example
# Get a regular user token
curl -X POST "http://localhost:8080/api/dev/generate-token?username=user&roles=USER"
# Get an admin token
curl -X POST "http://localhost:8080/api/dev/generate-token?username=admin&roles=ADMIN,USER"
# Use the token
curl -H "Authorization: Bearer <token>" http://localhost:8080/api/admin/users

Approach 4: Using @WithUserDetails for Realistic Tests

When I needed to test with actual UserDetails implementation (for custom user properties or authorities), I used @WithUserDetails instead of @WithMockUser.

TestSecurityConfig.java
@TestConfiguration
public class TestSecurityConfig {
@Bean
@Primary
public UserDetailsService testUserDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password("{noop}password")
.roles("USER")
.build(),
User.withUsername("admin")
.password("{noop}password")
.roles("ADMIN", "USER")
.build(),
User.withUsername("tenant-admin")
.password("{noop}password")
.roles("TENANT_ADMIN", "USER")
.build()
);
}
}
RoleBasedApiTests.java
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestSecurityConfig.class)
class RoleBasedApiTests {
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails(value = "admin", userDetailsServiceBeanName = "testUserDetailsService")
void adminCanAccessAllEndpoints() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
}

What I Learned

Testing Strategy Overview
+------------------------+------------------+----------------------+
| Scenario | Recommended | Why |
+------------------------+------------------+----------------------+
| Unit/Integration Tests | @WithMockUser | Simple, no setup |
| Custom UserDetails | @WithUserDetails | Tests actual impl |
| Manual API Testing | Dev token | Keeps security on |
| Quick Dev Iteration | Permit all | Fastest but risky |
+------------------------+------------------+----------------------+

Common mistakes I made:

  1. Disabling security entirely - I initially turned off security for all dev profiles, which meant my tests weren’t catching real security issues.

  2. Not testing all role permutations - I only tested happy paths, missing cases where users shouldn’t have access.

  3. Hardcoded tokens - I copied tokens from login responses, but they kept expiring mid-testing.

  4. Forgetting unauthorized scenarios - I only tested what authenticated users could do, not what happened without authentication.

Security Considerations

The dev token endpoint is a powerful tool but requires safeguards:

DevTokenController.java
@RestController
@RequestMapping("/api/dev")
@Profile({"dev", "test"}) // NEVER activate in production
@ConditionalOnProperty(name = "app.dev.testing.enabled", havingValue = "true")
public class DevTokenController {
// Only runs when explicitly enabled
}

I also added a startup warning:

DevSecurityWarning.java
@Component
@Profile("dev")
public class DevSecurityWarning implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger log = LoggerFactory.getLogger(DevSecurityWarning.class);
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.warn("========================================");
log.warn("DEV PROFILE ACTIVE - Security relaxed");
log.warn("NEVER use this profile in production!");
log.warn("========================================");
}
}

Final Thoughts

Testing Spring Boot APIs with security enabled doesn’t have to be painful. The key is choosing the right tool for each scenario:

  • Use @WithMockUser for quick unit tests
  • Use @WithUserDetails when you need realistic user details
  • Create dev-only token endpoints for manual testing
  • Keep security enabled in all environments, just with different configurations

The dev token endpoint approach gave me the best balance: I could test with real authentication logic while avoiding the friction of managing tokens manually. And by restricting it to dev/test profiles with explicit warnings, I ensured it would never leak into production.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments