Home Java Spring Security Basic Auth: Config & Best Practices
Intermediate 5 min · July 14, 2026

Spring Security Basic Auth: Config & Best Practices

Learn how to configure Spring Security Basic Auth for production APIs.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+
  • Spring Boot 3.x project
  • Basic understanding of Spring Security concepts
  • Familiarity with REST APIs
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Spring Security Basic Authentication Configuration and?

Spring Security Basic Auth is a configuration that allows clients to authenticate by sending a username and password in the HTTP Authorization header, validated by Spring Security's filter chain.

Think of Basic Auth like a bouncer checking a VIP list.
Plain-English First

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.

SecurityConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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();
    }
}
⚠ Never Use NoOpPasswordEncoder
📊 Production Insight
In Spring Boot 3.1, the default security configuration changed to require explicit security filter chain. If you upgrade from 2.x, your old WebSecurityConfigurerAdapter won't work. Use the SecurityFilterChain bean approach shown above.
🎯 Key Takeaway
Always use BCryptPasswordEncoder, disable CSRF for stateless APIs, and set session creation to STATELESS.

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.

``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.

CustomEntryPoint.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
@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
}
💡Consistent Error Responses
📊 Production Insight
If you're using Spring Boot Actuator, ensure the /actuator endpoints are either public or use a separate security configuration. Mixing Basic Auth with Actuator can cause unexpected lockouts.
🎯 Key Takeaway
Customize the authentication entry point to return JSON instead of the default HTML for API consistency.

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.

```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.

CustomUserDetailsService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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())
        );
    }
}
🔥Password Encoding Consistency
📊 Production Insight
A common pitfall: returning a 500 error instead of 401 when the user is not found. Make sure your loadUserByUsername throws UsernameNotFoundException, not a generic exception.
🎯 Key Takeaway
Implement UserDetailsService to load users from a database. Always store BCrypt hashes, and handle the UsernameNotFoundException gracefully.

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.

DelegatingPasswordEncoderExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    encoders.put("sha256", new StandardPasswordEncoder()); // deprecated but for legacy
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}
⚠ HTTPS Is Not Optional
📊 Production Insight
When rotating credentials, implement a grace period. Store the old password hash alongside the new one, and accept both for a configurable window. Then remove the old hash.
🎯 Key Takeaway
Be aware of default CSRF protection, filter chain ordering, and the need for HTTPS. Use DelegatingPasswordEncoder for smooth password migration.

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.

ControllerTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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());
    }
}
💡Test Both Mock and Real Auth
📊 Production Insight
If you use @SpringBootTest with a random port, remember to include the Authorization header in your TestRestTemplate requests. Otherwise, you'll get 401.
🎯 Key Takeaway
Test security with both @WithMockUser and actual Basic Auth headers. Use @WebMvcTest for unit tests and @SpringBootTest for integration tests.

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).

  • 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%.

CachingUserDetailsService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class CachingUserDetailsService implements UserDetailsService {

    private final UserDetailsService delegate;
    private final CacheManager cacheManager;

    public CachingUserDetailsService(UserDetailsService delegate, CacheManager cacheManager) {
        this.delegate = delegate;
        this.cacheManager = cacheManager;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Cache cache = cacheManager.getCache("users");
        UserDetails user = cache.get(username, UserDetails.class);
        if (user == null) {
            user = delegate.loadUserByUsername(username);
            cache.put(username, user);
        }
        return user;
    }
}
🔥Cache Invalidation
📊 Production Insight
BCrypt with strength 10 is standard, but on high-traffic systems, consider strength 8 or use Argon2. Measure performance with realistic load tests.
🎯 Key Takeaway
Cache UserDetails to reduce database load. Use a fast but secure password encoder. Rate-limit authentication attempts.
● Production incidentPOST-MORTEMseverity: high

The Case of the Leaked Default Credentials

Symptom
Production API started returning 401 for legitimate users. Investigation showed the staging environment's credentials were being used against production.
Assumption
The dev assumed that the default 'user' password generated by Spring Boot was only for local development and would never be used in production.
Root cause
The application.properties file had spring.security.user.name and spring.security.user.password set to the same values as the default, and the team accidentally deployed that config to production. An attacker brute-forced the default password and gained access.
Fix
Removed default credentials from properties, enforced strong passwords via environment variables, and added a startup check that fails if credentials are weak or default.
Key lesson
  • 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.
Production debug guideSymptom to Action3 entries
Symptom · 01
401 Unauthorized with valid credentials
Fix
Check if the password encoder matches the stored password format. Common mismatch: using NoOpPasswordEncoder in dev but BCrypt in prod.
Symptom · 02
403 Forbidden after successful authentication
Fix
Verify that CSRF is disabled for stateless APIs. Also check that the user has the required role/authority.
Symptom · 03
401 with 'Authentication method not supported'
Fix
Ensure the SecurityFilterChain includes .httpBasic() and that no other filter (like JWT) is blocking the header.
★ Quick Debug Cheat SheetImmediate steps when Basic Auth fails
401 Unauthorized
Immediate action
Check the Authorization header value; decode Base64 to verify username:password.
Commands
echo 'dXNlcjpwYXNz' | base64 -d
curl -v -u user:pass http://localhost:8080/api
Fix now
Ensure password encoder matches stored password hash.
403 Forbidden+
Immediate action
Check if CSRF is disabled and user has correct role.
Commands
Check SecurityFilterChain for .csrf().disable()
Check user authorities: SecurityContextHolder.getContext().getAuthentication().getAuthorities()
Fix now
Add .csrf().disable() for stateless APIs or grant proper role.
Authentication method not supported+
Immediate action
Verify httpBasic() is configured in the security chain.
Commands
Check @EnableWebSecurity and SecurityFilterChain bean
Check for conflicting filters (e.g., JWT filter before Basic Auth)
Fix now
Ensure .httpBasic() is present and no earlier filter consumes the header.
AspectBasic AuthJWTOAuth2
ComplexityLowMediumHigh
StateStateless (credentials per request)Stateless (token per request)Stateless (token per request)
SecurityRequires HTTPSRequires HTTPS, token can be revokedRequires HTTPS, supports scopes
Use CaseInternal APIs, simple setupsSPA, mobile appsThird-party access, microservices
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
SecurityConfig.java@ConfigurationSetting Up Spring Security with Basic Auth
CustomEntryPoint.java@BeanCustomizing the Authentication Entry Point
CustomUserDetailsService.java@ServiceDatabase-Backed User Details Service
DelegatingPasswordEncoderExample.java@BeanWhat the Official Docs Won't Tell You
ControllerTest.java@WebMvcTest(SomeController.class)Testing Basic Auth Endpoints
CachingUserDetailsService.java@ServicePerformance Considerations

Key takeaways

1
Configure Spring Security with SecurityFilterChain, disable CSRF, and set stateless sessions for Basic Auth.
2
Always use BCryptPasswordEncoder and never store plain-text passwords.
3
Customize the authentication entry point to return JSON for API consistency.
4
Cache UserDetails to improve performance and implement credential rotation with a grace period.
5
Test both with mock users and actual Basic Auth headers.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you configure Spring Security for Basic Auth in a Spring Boot 3.x...
Q02SENIOR
What are the security implications of using Basic Auth, and how do you m...
Q03SENIOR
Explain how DelegatingPasswordEncoder works and when you would use it.
Q01 of 03JUNIOR

How do you configure Spring Security for Basic Auth in a Spring Boot 3.x application?

ANSWER
Create a SecurityFilterChain bean that configures httpBasic(), disables CSRF, sets session management to stateless, and defines authorization rules. Also provide a UserDetailsService and a PasswordEncoder bean (e.g., BCryptPasswordEncoder).
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Is Basic Auth secure for production APIs?
02
How do I disable CSRF for my REST API?
03
What password encoder should I use?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Security. Mark it forged?

5 min read · try the examples if you haven't

Previous
Spring Security Form Login Configuration and Custom Login Page
11 / 31 · Spring Security
Next
Custom AuthenticationProvider in Spring Security: Dao and Custom Providers