Implementing Multi-Authentication Methods in Spring Boot 3

Implementing Multi-Authentication Methods in Spring Boot 3

Authentication plays a pivotal role in fortifying the security of your Spring Boot 3 applications. In certain projects, the necessity arises to endorse multiple authentication methods tailored for distinct sections of your application.

Within the realm of my ongoing Spring Boot 3 side project, a captivating and commonplace challenge surfaces concerning the authentication of APIs using various methods. Specifically, when grappling with internal APIs prefixed with “/api/internal“. I opt for user authentication via an API Key embedded in the header. Conversely, for the user interfaces of web applications, the preferred authentication method is HttpBasic. The management of diverse authentication mechanisms within a single project stands out as a noteworthy facet of this endeavor.

In the ensuing discourse, I will furnish an illustrative example elucidating the implementation of these diverse authentication approaches.

1. Project Configuration

Ensure that your build.gradle (for Gradle) or pom.xml (for Maven) file includes the requisite dependencies:

Gradle:

implementation 'org.springframework.boot:spring-boot-starter-security'

Maven:

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

2. Security Configuration

Craft a SecurityConfig class to configure Spring Security, allowing for the incorporation of multiple SecurityConfigurerAdapter classes catering to different segments of your application.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
@Configuration
public class SpringSecurityConfig {

    @Autowired
    public APIAuthenticationErrEntrypoint apiAuthenticationErrEntrypoint;

    @Value("${internal.api-key}")
    private String internalApiKey;

    @Bean
    @Order(1)
    public SecurityFilterChain filterChainPrivate(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/internal/**")
            .addFilterBefore(new InternalApiKeyAuthenticationFilter(internalApiKey), ChannelProcessingFilter.class)
            .exceptionHandling((auth) -> {
                auth.authenticationEntryPoint(apiAuthenticationErrEntrypoint);
            })
            .cors(AbstractHttpConfigurer::disable)
            .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain filterChainWebAppication(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers("/login").permitAll()
                .requestMatchers("/**").authenticated()
                .anyRequest().authenticated()
        );

        http.formLogin(authz -> authz
                .loginPage("/login").permitAll()
                .loginProcessingUrl("/login")
        );

        http.logout(authz -> authz
                .deleteCookies("JSESSIONID")
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
        );

        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService());
        return authenticationProvider;
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

}

3. Implementation of Authentication Filters

Construct a custom filter for API Key authentication:

public class InternalApiKeyAuthenticationFilter implements Filter {

    private final String internalApiKey;


    InternalApiKeyAuthenticationFilter(String internalApiKey) {
        this.internalApiKey = internalApiKey;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        String apiKey = httpServletRequest.getHeader("x-api-key");

        if (apiKey == null) {
            unauthorized(httpServletResponse);
            return;
        }

        if (!internalApiKey.equals(apiKey)) {
            unauthorized(httpServletResponse);
            return;
        }

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }

    private void unauthorized(HttpServletResponse httpServletResponse) throws IOException {
        httpServletResponse.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        httpServletResponse.setStatus(401);
        Map<String, Object> response = Map.of("message", "SC_UNAUTHORIZED");
        String responseBody = new ObjectMapper().writeValueAsString(response);
        httpServletResponse.getWriter().write(responseBody);

    }

}

Requests following the pattern /api/internal/** will undergo validation through the InternalApiKeyAuthenticationFilter, extracting the API key from the request header and comparing it with a constant key for verification.

4. Usage in Controller

In your controllers, employ the @PreAuthorize annotation to specify the required roles for different endpoints.

@RestController
@RequestMapping(value = "/api/internal")
public class InternalAPIController {

    @GetMapping(value = "/health")
    public ResponseEntity internalHealthCheck() {
        return ResponseEntity.ok("ok");
    }
}

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @GetMapping
    public String homePage(Model model) {
        return "home";
    }
}

Conclusion

Integrating multiple authentication methods into a Spring Boot project empowers you to cater to diverse sections of your application with specific security requisites. By harnessing the capabilities of Spring Security and custom filters, the seamless integration of API key authentication and HTTP basic authentication within the same project becomes attainable.

Remember to tailor the authentication filter to align with your specific use case and implement validation logic in accordance with your security requirements. This adaptable approach to authentication ensures the security of your application while accommodating a spectrum of authentication needs.

For a comprehensive demonstration of the code, refer to the GitHub repository.

Leave a Reply

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