How to secure an API endpoint with HTTP basic authentication in Spring Boot

Photo by Ash from Modern Afflatus on Unsplash

Problem

Given a Spring Boot application that exposes a REST API, we want to control access to one of its API endpoints using HTTP basic authentication.

HTTP basic authentication

HTTP basic authentication is an extension to the HTTP protocol meant to protect access to a web resource. It works defining a username and password for the resource, and having the client send a header Authorization: Basic <credentials>, where credentials is the string username:password encoded in base64. For more details see https://en.wikipedia.org/wiki/Basic_access_authentication.

Assumptions

We will work based on the following assumptions:

  1. The application is a Spring Boot web application that uses the reactive web stack (Webflux). This means the approach of using SecurityWebFilterChain to configure security for each endpoint is not an option, because it depends on the blocking servlets API so can’t be used in a reactive service.
  2. The credentials will be stored on the cloud using AWS Secrets Manager.
  3. The application has a set of endpoints that are able to receive unsecured requests over HTTP, and these endpoints are under the URL /api/.

Steps

Follow this procedure to protect an endpoint with HTTP basic authentication. assumes the application is a Spring Boot web application that uses the reactive web stack.

  • If you haven’t added the AWS Secrets Manager Java SDK to your project, do it following the instructions at How to integrate a Java application with the AWS Java SDK .
  • If you haven’t configured the application to use SSL to receive its requests, do it following the instructions at Setting up HTTPS / SSL in a Spring Boot application .
    • This is necessary in order to send the credentials securely in the HTTP request. Keep in mind the credentials are sent encoded in base64, but base64 is not encryption. Any third party that intercepts the request in cleartext could easily extract the credentials from the base64-encoded string.
  • Create a strong password to protect the endpoint.
  • Store this password in AWS Secrets Manager following the instructions at Working with AWS Secrets Manager .
  • Create a bean (let’s call it APIPasswordRetriever for this example) that retrieves the API password from the secret management service following the instructions at Working with AWS Secrets Manager .
  • Pick a username to use to access the API. In this example we will use “api-user’.
  • Add the following dependencies to your project:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
  • If your pom inherits from the spring-boot-starter-parent parent, you don’t need to specify the versions. Otherwise, search for the latest versions in https://search.maven.org/classic/.
  • Create a bean of type SecurityWebFilterChain, and construct it using the ServerHttpSecurity builder. You can use the following snippet as guidance:
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

@Configuration
public class ApiSecurityConfig {

    @Autowired
    private APIPasswordRetriever apiPasswordRetriever;

    @Bean
    public SecurityWebFilterChain apiSecurityWebFilterChain(ServerHttpSecurity http) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        UserDetails user = User
            .withUsername("api-user")
            .password(passwordEncoder.encode(apiPasswordRetriever.getApiPassword()))
            .roles("API_USER")
            .build();

        MapReactiveUserDetailsService userDetailsService = new MapReactiveUserDetailsService(user);

        UserDetailsRepositoryReactiveAuthenticationManager authManager =
            new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
        authManager.setPasswordEncoder(passwordEncoder);

        return http
            .csrf().disable()
            .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
            .httpBasic()
            .and()
            .authorizeExchange()
            .pathMatchers("/api/**")
            .hasRole("API_USER")
            .and()
            .authenticationManager(authManager)
            .build();
    }
}
  • Key takeaways:
    • The securityMatcher line specifies the scope of this configuration i.e. which requests it will act upon. Using this approach, you can set up different security settings for different APIs, by creating different SecurityWebFilterChain beans. As long as the securityMatcher lines define disjoint paths, the configurations will not step on each other. If we didn’t include the securityMatcher line, then this SecurityWebFilterChain config would apply to all requests.
    • The pathMatchers line is used to enter into authorization settings for a given URL within that scope. Several pathMatchers lines can be given to configure different URLs.
    • The above configuration enables HTTP basic authentication for all URLs under /api.
    • The role used is an arbitrary string, all that matters is that it matches between the creation of the user database (line .roles("API_USER")) and the hasRole("API_USER") line.
    • We disable CSRF protection. It’s not really applicable in a REST service, which is not browser-facing. Disabling CSRF protection in necessary for POST requests to work without having to send a CSRF token.

Sources

Leave a comment