Spring Security Basic Auth: Config & Best Practices
Learn how to configure Spring Security Basic Auth for production APIs.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 17+
- ✓Spring Boot 3.x project
- ✓Basic understanding of Spring Security concepts
- ✓Familiarity with REST APIs
- Use Spring Security's SecurityFilterChain to configure HTTP Basic auth.
- Stateless session management is critical for APIs.
- Never use default credentials; always externalize secrets.
- Combine with HTTPS to avoid credential exposure.
- Test with tools like curl or Postman, not just unit tests.
Think of Basic Auth like a bouncer checking a VIP list. The client sends a username and password in the request header (like showing ID). The server checks against its list and lets you in or turns you away. It's simple but not very secure unless you use HTTPS—like having the bouncer check IDs over a private radio, not shouting across the street.
If you're building a REST API and need a straightforward authentication mechanism, HTTP Basic Authentication is the old guard that still gets the job done. It's simple: the client sends a username and password encoded in Base64 in the Authorization header. But simple doesn't mean easy to get right in production.
I've seen teams overcomplicate this. They either leave default credentials lying around or forget to disable CSRF for stateless APIs, leading to 403 errors that take hours to debug. In this guide, I'll walk you through configuring Spring Security for Basic Auth with Spring Boot 3.x, covering the security filter chain, password encoding, and stateless sessions. We'll also tackle real-world gotchas—like why you should never trust the default security configuration and how to handle credential rotation gracefully.
By the end, you'll have a production-ready setup that balances simplicity with security. And you'll know exactly what to do when that 2 AM pager goes off because someone's credentials got leaked.
Setting Up Spring Security with Basic Auth
Let's start with the bare minimum. You need the spring-boot-starter-security dependency. In Spring Boot 3.x, that pulls in Spring Security 6.x. Here's the simplest configuration:
```java @Configuration @EnableWebSecurity public class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .httpBasic(Customizer.withDefaults()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .csrf(csrf -> csrf.disable()); return http.build(); }
@Bean public UserDetailsService users() { UserDetails user = User.builder() .username("admin") .password(passwordEncoder().encode("secret")) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ```
Notice that I'm using BCryptPasswordEncoder. Never, ever use NoOpPasswordEncoder in production. It stores passwords in plain text. I've seen it happen when developers copy-paste from outdated blog posts.
The session management is set to STATELESS because Basic Auth credentials are sent with every request—there's no session to maintain. And CSRF is disabled because it's irrelevant for stateless APIs. If you forget that, you'll get 403 errors on every POST request.
For a real application, you'd replace the in-memory user store with a database-backed UserDetailsService. But for now, this gets you off the ground.
Customizing the Authentication Entry Point
By default, when authentication fails, Spring Security returns a generic 401 response with a WWW-Authenticate header. That's fine for browsers but not for APIs. You'll want a JSON response instead.
Here's how to customize the entry point:
``java @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint((request, response, authException) -> { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{\"error\":\"Unauthorized\"}"); }) ) .httpBasic(``Customizer.withDefaults()); // ... rest of config }
This is especially useful when your API consumers expect consistent error formats. I've spent hours debugging why a mobile app kept crashing on 401—turns out the default response body was HTML, and the app expected JSON.
You can also customize the success handler if you need to log successful logins or add extra headers. But keep it simple unless you have specific requirements.
Database-Backed User Details Service
In-memory users are fine for demos, but production requires a database. You'll implement UserDetailsService to load users from your user repository.
Here's a typical implementation:
```java @Service public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; }
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList()) ); } } ```
Make sure your password column stores BCrypt hashes. If you migrate from plain text, you'll need to update all passwords. I once worked with a team that had a mix of MD5 and BCrypt hashes—debugging that was a nightmare.
Also, consider caching the UserDetails if your database is under heavy load. Spring Security's UserCache can help, but be careful with cache invalidation when passwords change.
What the Official Docs Won't Tell You
The Spring Security reference docs are thorough, but they gloss over some real-world pain points. Here are the gotchas I've encountered:
1. The Default Security Configuration Is Dangerous
Spring Boot's auto-configuration creates a default user with a generated password. That's fine for development, but I've seen teams accidentally deploy that to production. The generated password is printed to the console, which can be captured in logs. Always override with explicit configuration.
2. CSRF Protection Is Enabled by Default
If you're building a stateless REST API, you must disable CSRF. The official docs mention it, but many developers miss it. Without .csrf().disable(), every POST/PUT/DELETE request will return 403. The error message is cryptic: "Expected CSRF token not found. Has your session expired?"
3. The Security Filter Chain Order Matters
If you have multiple security configurations (e.g., Basic Auth for some endpoints, JWT for others), the order of @Order annotations is critical. I once spent a day debugging why Basic Auth worked locally but not in production—turns out a misordered filter chain was intercepting requests.
4. PasswordEncoder Upgrades Are Tricky
If you need to upgrade from MD5 to BCrypt, you can't just re-hash existing passwords. Use DelegatingPasswordEncoder with a prefix (e.g., {bcrypt}) to support multiple encoders. The docs show this, but the migration path is painful if you have millions of users.
5. Basic Auth Over HTTPS Is Mandatory
The docs assume you know this, but I've seen APIs deployed without HTTPS. Basic Auth sends credentials in Base64, which is easily decoded. Without TLS, anyone on the network can steal passwords. Use HTTPS in production—no exceptions.
6. Credential Rotation Requires Planning
When you need to rotate credentials (e.g., after a breach), you can't just change the password. Clients may have cached credentials. Implement a grace period where old and new passwords are accepted, then enforce the new one. Spring Security's UserDetailsService can check multiple hashes.
Testing Basic Auth Endpoints
Unit tests for security are essential. Use @WebMvcTest with MockMvc to test your secured endpoints. Here's an example:
```java @WebMvcTest(SomeController.class) class SomeControllerTest {
@Autowired private MockMvc mockMvc;
@Test void testUnauthenticatedRequest() throws Exception { mockMvc.perform(get("/api/private")) .andExpect(status().isUnauthorized()); }
@Test @WithMockUser(username = "admin", roles = "USER") void testAuthenticatedRequest() throws Exception { mockMvc.perform(get("/api/private")) .andExpect(status().isOk()); }
@Test void testBasicAuthSuccess() throws Exception { mockMvc.perform(get("/api/private") .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("admin:secret".getBytes()))) .andExpect(status().isOk()); } } ```
Notice the third test sends the actual Basic Auth header. This tests the full authentication flow, not just mock security. I recommend including both @WithMockUser and real header tests.
Integration tests with @SpringBootTest and TestRestTemplate are also valuable. They verify that the entire security configuration works end-to-end. But keep them separate from unit tests to avoid slow builds.
Performance Considerations
Basic Auth has a performance cost because credentials are sent with every request. The server must decode the Base64 header, load the user from the database (or cache), and verify the password hash. BCrypt verification is intentionally slow (around 10-100ms per check).
To mitigate this:
- Cache UserDetails: Use Spring Security's UserCache or a custom cache (e.g., Redis). Cache the UserDetails object for a configurable TTL. But beware: if a user's password changes, the cache must be invalidated.
- Use a Faster Password Encoder: BCrypt is the standard, but if you need speed, consider Argon2 or PBKDF2 with appropriate parameters. Don't use plain SHA-256—it's too fast and vulnerable to brute force.
- Rate-Limit Authentication Attempts: Implement a rate limiter on the /login endpoint (if you have one) or globally. Basic Auth doesn't have a separate login endpoint, but you can track failed attempts per IP.
- Connection Pooling: Ensure your database connection pool is sized adequately. Each authentication request hits the database unless cached.
I once saw a system collapse under load because every request triggered a full BCrypt verification and a database lookup. Adding a 5-minute cache for UserDetails reduced database load by 90%.
The Case of the Leaked Default Credentials
- Never rely on Spring Boot's auto-generated password for anything beyond local dev.
- Externalize secrets using environment variables or a vault service.
- Add a startup validation to reject weak or default credentials.
- Use separate credentials per environment (dev, staging, prod).
- Rotate credentials regularly and log all authentication failures.
echo 'dXNlcjpwYXNz' | base64 -dcurl -v -u user:pass http://localhost:8080/api| File | Command / Code | Purpose |
|---|---|---|
| SecurityConfig.java | @Configuration | Setting Up Spring Security with Basic Auth |
| CustomEntryPoint.java | @Bean | Customizing the Authentication Entry Point |
| CustomUserDetailsService.java | @Service | Database-Backed User Details Service |
| DelegatingPasswordEncoderExample.java | @Bean | What the Official Docs Won't Tell You |
| ControllerTest.java | @WebMvcTest(SomeController.class) | Testing Basic Auth Endpoints |
| CachingUserDetailsService.java | @Service | Performance Considerations |
Key takeaways
Interview Questions on This Topic
How do you configure Spring Security for Basic Auth in a Spring Boot 3.x application?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Security. Mark it forged?
5 min read · try the examples if you haven't