Spring Boot 3: Basic Authentication Setup Guide

Basic Authentication in Spring Boot 3 helps the developer secure RESTful web application services from unwanted clients.

Scenario

The developer team creates restful web application services with basic authentication to protect unauthorized access from clients who are not registered.

Discussion

The developer team decided to use built-in basic Authentication in Spring Boot 3 because it is simple to implement.

Implementation

The developer team creates web services with built-in basic Authentication in Spring Boot 3 libraries.

Create web services with basic authentication

1. Add dependency in the pom.xml file.

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    2. Create a web service.

    import com.example.demo.bean.UserProfileBean;
    import jakarta.validation.Valid;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class UserProfilesController {
        @PostMapping(path = "/user-profiles", produces = "application/json")
        public ResponseEntity<String> create(@Valid @RequestBody UserProfileBean userProfile) {
            //custom process
            return new ResponseEntity<>("created", HttpStatus.CREATED);
        }
    
    }

    3. Create UserProfileBean

    import com.example.demo.validator.PersonIdValidate;
    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.NotEmpty;
    import lombok.Data;
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter
    @Setter
    public class UserProfileBean {
    
        @PersonIdValidate
        private String personalId;
    
        @NotEmpty(message = "not.empty")
        private String firstName;
    
        @NotEmpty
        private String lastName;
    
        @Email
        private String email;
    
        @Min(0)
        private long age;
    
        public UserProfileBean(String personalId, String firstName, String lastName, int age, String email){
            this.personalId = personalId;
            this.firstName = firstName;
            this.lastName = lastName;
            this.email =  email;
            this.age = age;
        }
    }

    4. Create Security Configuration

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
    import static org.springframework.security.config.Customizer.withDefaults;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain configure(HttpSecurity http) throws Exception {
            http.csrf(AbstractHttpConfigurer::disable);
            http
                    .authorizeHttpRequests(requests -> requests
                            .anyRequest().authenticated())
                    .httpBasic(withDefaults());
            return http.build();
        }
    }

    5. Add username and password to the application.properties

    spring.security.user.name=your_username
    spring.security.user.password=your_password

    Test case scenario

    The developer team used Postman to test web services.

    1. Test case

    Send a request to path /user-profiles without a username and password.

    Test result

    Web services respond with an HTTP status code 401: Unauthorized.

    2. Test case

    Send a request to path /user-profiles with username and password.

    Test result

    Web services respond to HTTP status 201 Created.

    Custom role using InMemoryUserDetailsManager

    Edit the SecurityConfig Java class and create two roles, ADMIN and USER.
    The ADMIN role can only access specific paths, “/user-profiles”.

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
    import static org.springframework.security.config.Customizer.withDefaults;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        @Bean
        public SecurityFilterChain configure(HttpSecurity http) throws Exception {
            http.csrf(AbstractHttpConfigurer::disable);
            http
                    .authorizeHttpRequests(requests -> requests.requestMatchers(new AntPathRequestMatcher("/user-profiles"))
                            .hasRole("ADMIN").anyRequest().authenticated())
                    .httpBasic(withDefaults());
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService(PasswordEncoder encoder) {
    
            // InMemoryUserDetailsManager
            UserDetails admin = User.withUsername("admin")
                    .password(encoder.encode("admin_password"))
                    .roles("ADMIN", "USER")
                    .build();
    
            UserDetails user = User.withUsername("user")
                    .password(encoder.encode("user_password"))
                    .roles("USER")
                    .build();
    
            return new InMemoryUserDetailsManager(admin, user);
        }
    
        // Password Encoding
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

    Test case scenario

    1. Test case

    Send a request to the path /user-profiles with the role’ user’.

    Test result

    Web services respond to an HTTP status code 403 Forbidden and provide information in return.

    2. Test case

    Send a request to the path /user-profiles with the role’ admin’.

    Test result

    Web services respond to an HTTP status code of 201 (Created) and return information in response.

    Custom exception handling for a standard error format

    1. Create a CustomAccessDenied Java class for handling HTTP status 403 Forbidden.

      import com.example.demo.bean.CommonResponseBean;
      import com.example.demo.bean.ErrorBean;
      import com.google.gson.Gson;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      import org.springframework.security.access.AccessDeniedException;
      import org.springframework.security.web.access.AccessDeniedHandler;
      import org.springframework.stereotype.Component;
      
      import java.io.IOException;
      import java.util.ArrayList;
      import java.util.Date;
      
      @Component
      public class CustomAccessDenied implements AccessDeniedHandler {
      
          @Override
          public void handle(HttpServletRequest request,
                             HttpServletResponse response,
                             AccessDeniedException accessDeniedException) throws IOException, ServletException {
              CommonResponseBean res = new CommonResponseBean();
              res.setTimestamp(new Date().getTime());
              res.setErrors(new ArrayList<>());
              res.getErrors().add(new ErrorBean("E0403", "forbidden"));
              Gson gs = new Gson();
              response.setContentType("application/json");
              response.getWriter().write(gs.toJson(res));
          }
      
      }

      2. Create a CustomAuthenticationEntryPoint Java class to handle HTTP status code 401 (Unauthorized).

      import com.example.demo.bean.CommonResponseBean;
      import com.example.demo.bean.ErrorBean;
      import com.google.gson.Gson;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      import org.springframework.security.web.AuthenticationEntryPoint;
      
      import java.io.IOException;
      import java.util.ArrayList;
      import java.util.Date;
      
      public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
      
          @Override
          public void commence(HttpServletRequest request,
                               HttpServletResponse response,
                               org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException {
              CommonResponseBean res = new CommonResponseBean();
              res.setTimestamp(new Date().getTime());
              res.setErrors(new ArrayList<>());
              res.getErrors().add(new ErrorBean("E0401", "Unauthorized"));
              Gson gs = new Gson();
              response.setContentType("application/json");
              response.getWriter().write(gs.toJson(res));
          }
      }

      3. Create CommonResponseBean Java class.

      import lombok.Getter;
      import lombok.Setter;
      import java.util.List;
      
      @Getter
      @Setter
      public class CommonResponseBean {
          public long timestamp;
          public List<ErrorBean> errors;
      }

      4. Create an Errorbean Java class.

      import lombok.Getter;
      import lombok.Setter;
      
      @Getter
      @Setter
      public class ErrorBean {
          public String  errorCode;
          public String  errorMessage;
          public ErrorBean(String errorCode, String errorMessage){
              this.errorCode = errorCode;
              this.errorMessage = errorMessage;
          }
      }

      5. Edit the SecurityConfig Java class.
      The developer adds exception handling for the Customizer response format.

      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.config.Customizer;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
      import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
      import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
      import org.springframework.security.core.userdetails.User;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.crypto.password.PasswordEncoder;
      import org.springframework.security.provisioning.InMemoryUserDetailsManager;
      import org.springframework.security.web.SecurityFilterChain;
      import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
      
      import static org.springframework.security.config.Customizer.withDefaults;
      
      @Configuration
      @EnableWebSecurity
      public class SecurityConfig {
      
          @Bean
          public SecurityFilterChain configure(HttpSecurity http) throws Exception {
              http.csrf(AbstractHttpConfigurer::disable);
              http
                      .authorizeHttpRequests(requests -> requests.requestMatchers(new AntPathRequestMatcher("/user-profiles"))
                              .hasRole("ADMIN").anyRequest().authenticated())
                      .httpBasic(withDefaults()).exceptionHandling(new Customizer<ExceptionHandlingConfigurer<HttpSecurity>>() {
                          @Override
                          public void customize(ExceptionHandlingConfigurer<HttpSecurity> httpSecurityExceptionHandlingConfigurer) {
                              httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(new CustomAccessDenied());
                              httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
                              
                          }
                      });
              return http.build();
          }
      
          @Bean
          public UserDetailsService userDetailsService(PasswordEncoder encoder) {
      
              // InMemoryUserDetailsManager
              UserDetails admin = User.withUsername("admin")
                      .password(encoder.encode("admin_password"))
                      .roles("ADMIN", "USER")
                      .build();
      
              UserDetails user = User.withUsername("user")
                      .password(encoder.encode("user_password"))
                      .roles("USER")
                      .build();
      
              return new InMemoryUserDetailsManager(admin, user);
          }
      
          // Password Encoding
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      }

      Test case scenario

      1. Test case

      Send a request to path /user-profiles without a username.

      Test result

      Web services respond with an HTTP status code 401, and the response body is defined in CommonResponseBean.

      2. Test case

      Send a request to path /user-profiles with a user role.

      Test result

      Web services respond to HTTP status 403 Forbidden, and the response body is defined in CommonResponseBean.

      3. Test case

      Send a request to path /user-profiles with an admin role.

      Test result

      Web services respond with an HTTP status code 201 (Created) and the response body.

      Conclusion

      The test result shows that Spring Boot provides various customizations. The developer team can customize the built-in basic authentication to meet the client’s requirements, which is basic for a lightweight project, or the junior developer can practice basic authentication mechanisms.

      Finally

      Basic authentication offers simplicity and compatibility. It is simple to implement basic applications. There are more mechanisms for authentication, such as the custom JTW algorithm or OAuth.

      Leave a Comment

      Your email address will not be published. Required fields are marked *