Implementing Multiple Authentication Methods in a Spring Boot 3

JackyNote ⭐️
3 min readNov 29, 2023

--

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:

<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.

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:

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.

@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

--

--

JackyNote ⭐️

🚀 Software Engineer | Full Stack Java 8 Years of Experience | Tech Enthusiast | Founder of helik.co - Learning AI Assistant | Startup Lover | Coffee Espresso