DEV Community

Cover image for Implementing Multiple Authentication Methods in a Spring Boot 3
Jacky
Jacky

Posted on

Implementing Multiple Authentication Methods in a Spring Boot 3

Authentication is a critical aspect of securing your Spring Boot applications. In some projects, you might encounter the need to support multiple authentication methods for different parts of your application.

In my ongoing Spring Boot side project, I've encountered a fascinating and common challenge related to authenticating APIs using various methods. Specifically, when dealing with internal APIs prefixed with /api/internal, I opt for user authentication via API Key embedded in the header. Conversely, for web application user interfaces, the preferred authentication method is HttpBasic. Managing multiple authentication mechanisms within a single project has proven to be a noteworthy aspect.

In this discussion, I'll provide an illustrative example of how to implement these diverse authentication approaches.

1. Project Setup

Ensure you have the necessary dependencies in your build.gradle (for Gradle) or pom.xml (for Maven) file:

Gradle:

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

Enter fullscreen mode Exit fullscreen mode

Maven:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

2. Security Configuration

Create a SecurityConfig class to configure Spring Security. You can have multiple SecurityConfigurerAdapter classes for different parts 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);
    }

}
Enter fullscreen mode Exit fullscreen mode

3. Implement Authentication Filters

Create 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);

    }

}
Enter fullscreen mode Exit fullscreen mode

Some requests with pattern /api/internal/** will go through InternalApiKeyAuthenticationFilter and get API key from request header and compare with constant key to verify.

4. Usage in Controller

In your controllers, use @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";
    }
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing multiple authentication methods in a Spring Boot project allows you to cater to different parts of your application with specific security requirements. By leveraging Spring Security and custom filters, you can seamlessly integrate API key authentication and HTTP basic authentication within the same project.

Remember to customize the authentication filter to suit your specific use case, and implement validation logic according to your security requirements. This flexible approach to authentication ensures that your application remains secure while accommodating diverse authentication needs.

Full code demo: https://github.com/jackynote/springboot3-multi-authenticate

Top comments (0)