HTTP Basic Authentication and Stateless APIs
What Is HTTP Basic Authentication?
HTTP Basic is the simplest authentication scheme defined in the HTTP specification (RFC 7617). The client encodes username:password in Base64 and sends it in the Authorization header with every request:
Authorization: Basic YWxpY2U6c2VjcmV0
↑ Base64("alice:secret")
Important: Base64 is encoding, not encryption. Anyone who intercepts the request can decode the credentials instantly. HTTP Basic must only be used over HTTPS.
sequenceDiagram
participant Client as API Client
participant SSF as Spring Security Filters
participant AM as AuthenticationManager
Client->>SSF: GET /api/data\n(no Authorization header)
SSF-->>Client: 401 Unauthorized\nWWW-Authenticate: Basic realm="MyApp"
Client->>SSF: GET /api/data\nAuthorization: Basic YWxpY2U6c2VjcmV0
SSF->>AM: authenticate(UsernamePasswordToken{alice, secret})
AM-->>SSF: Authentication success
SSF-->>Client: 200 OK with data
On a 401 response, the browser shows a native credentials dialog. In API contexts, the client handles it programmatically.
Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(basic -> basic
.realmName("E-Commerce API") // shown in browser dialog
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
The realmName appears in the WWW-Authenticate response header:
WWW-Authenticate: Basic realm="E-Commerce API"
Custom Authentication Entry Point
By default, Spring Security returns a 401 with WWW-Authenticate: Basic realm="Realm". For REST APIs, you want JSON:
@Component
public class BasicAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// Include realm so clients know Basic is accepted
response.setHeader("WWW-Authenticate", "Basic realm=\"MyApp\"");
response.getWriter().write("""
{
"error": "Unauthorized",
"message": "Valid credentials required",
"path": "%s"
}
""".formatted(request.getRequestURI()));
}
}
http.httpBasic(basic -> basic
.realmName("MyApp")
.authenticationEntryPoint(basicAuthEntryPoint)
);
Stateless vs Stateful with Basic Auth
HTTP Basic is inherently stateless — credentials are sent with every request. By default, Spring Security may still create a session after authentication. For a truly stateless API:
http
.httpBasic(Customizer.withDefaults())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
With STATELESS, Spring Security never creates a HttpSession and never stores the SecurityContext between requests. Each request is independently authenticated from the Authorization header.
When to Use HTTP Basic
| Use case | HTTP Basic | JWT / OAuth2 |
|---|---|---|
| Server-to-server API calls (internal) | Good — simple, no token management | Overkill |
| CLI tools and scripts | Good — easy to implement | Complex |
| Actuator endpoints (internal) | Good | Unnecessary |
| Public REST API | Avoid — credentials in every request | Use JWT |
| User-facing web application | Avoid — poor UX, no logout | Use form login |
| Microservice mesh (secured network) | Acceptable | Prefer mTLS |
HTTP Basic shines for internal APIs where you control both the client and server, and credentials are managed as service accounts.
Combining Basic and Form Login
A single application can support both — web browser users use form login, API clients use HTTP Basic:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// Chain 1: API — HTTP Basic
@Bean
@Order(1)
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.httpBasic(basic -> basic.realmName("API"))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable());
return http.build();
}
// Chain 2: Web — Form login
@Bean
@Order(2)
public SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form.loginPage("/login"))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
In-Memory vs Database User Stores
For testing or simple internal tools, InMemoryUserDetailsManager is sufficient:
@Bean
public UserDetailsService userDetailsService() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
UserDetails serviceAccount = User.builder()
.username("service-account")
.password(encoder.encode("s3cr3t-service-password"))
.roles("SERVICE")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(encoder.encode("admin-password"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(serviceAccount, admin);
}
For production, always use a database-backed UserDetailsService (Article 4).
Security Considerations
Always Use HTTPS
HTTP Basic sends credentials in every request. Without TLS, any network observer can decode them. In production:
// Force HTTPS
http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
Credential Caching
HTTP Basic sends credentials on every request, which means loadUserByUsername() is called on every request. For high-traffic APIs, cache the UserDetails:
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
return new CachingUserDetailsService(manager); // wraps with a cache
}
Or use Spring’s cache abstraction:
@Service
public class CachedUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Cacheable(value = "userDetails", key = "#username")
@Override
public UserDetails loadUserByUsername(String username) {
return userRepository.findByUsername(username)
.map(AppUserDetails::new)
.orElseThrow(() -> new UsernameNotFoundException(username));
}
@CacheEvict(value = "userDetails", key = "#username")
public void evictUser(String username) {}
}
API Key Pattern over Basic Auth
For machine-to-machine communication, an API key in a custom header is often preferable:
X-API-Key: ak_live_abc123def456
This decouples the credential from a user account, allows independent rotation, supports scoping per key, and is easier to audit. Article 4 showed a custom ApiKeyAuthenticationProvider for this pattern.
Summary
- HTTP Basic sends
Authorization: Basic base64(username:password)with every request — simple but requires HTTPS. - Configure with
http.httpBasic()andSessionCreationPolicy.STATELESSfor fully stateless APIs. - Customize the authentication entry point to return JSON instead of the browser’s native dialog.
- Use HTTP Basic for internal service-to-service APIs, CLI tools, and Actuator endpoints — not for public APIs or user-facing applications.
- Cache
UserDetailslookups when using Basic auth on high-traffic APIs.
Next: Article 8 builds a complete JWT authentication system — token generation, validation filter, stateless sessions, and the full request flow.