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 caseHTTP BasicJWT / OAuth2
Server-to-server API calls (internal)Good — simple, no token managementOverkill
CLI tools and scriptsGood — easy to implementComplex
Actuator endpoints (internal)GoodUnnecessary
Public REST APIAvoid — credentials in every requestUse JWT
User-facing web applicationAvoid — poor UX, no logoutUse form login
Microservice mesh (secured network)AcceptablePrefer 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() and SessionCreationPolicy.STATELESS for 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 UserDetails lookups 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.