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 *