Implementing Multiple Authentication Methods in a Spring Boot 3
Tags: springboot, java, webdev, springsecurity
Published: 2023-11-29 04:22:55

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'

```

**Maven:**

```xml

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

### 2. **Security Configuration**

Create a **`SecurityConfig`** class to configure Spring Security. You can have multiple **`SecurityConfigurerAdapter`** classes for different parts of your application.

```java
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. **Implement Authentication Filters**

Create a custom filter for API Key authentication:

```java
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);

    }

}
```

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.

```java
@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

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