How to Add API Key Access Method for Spring Projects
Most technical SaaS platforms provide APIs for developers or to support clients and SDKs, and our company is no exception. Although saving OAuth2 tokens in browsers is convenient, tokens have expiration times and are issued by third parties, which can be a potential risk for clients and SDKs. Often, issues with third-party authentication services cause users to be unable to log in. Therefore, providing API Key access has become a better choice.
There are two approaches to adding API Key access:
- Reuse the existing Web API, introducing API Key authentication in parallel with the existing OAuth2-based services
- Start a completely separate service, providing consistent services through code sharing.
Both options have their pros and cons. In the early and middle stages of a product, when web and API access methods are roughly the same, the first approach may be better. However, when a project enters the middle and later stages, complete isolation may be a better choice considering the differences between web and API in terms of functionality and security. Since most of the web functionalities are consistent, we decided to add a parallel API Key authentication method on top of the existing OAuth2 authentication in the backend, making it more convenient for users.
This article mainly introduces the best practices for adding API Key authentication to existing Spring projects.
Implementation
This method is only applicable to Spring Security <= 5.2.2 (Spring Boot <2.2.5). The latest Spring Security 5.7 (Spring Boot ^2.3.0) has bugs, and the API will undergo significant changes, making it unsuitable for this method. See the end of the article for details.
Obtaining the API Key and Generating AuthenticationToken
@Component("apiKeyFilter")
public class APIKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("application-api-key");
if (authorization != null && SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
emptyContext.setAuthentication(new APIKeyAuthenticationToken(authorization));
SecurityContextHolder.setContext(emptyContext);
}
filterChain.doFilter(request, response);
}
}
Get the API Key by checking the HttpRequest header, wrap it in a custom Authentication Token, and place it in the SecurityContext. Note that because Servlets operate in a multi-threaded environment, you should first create a new empty context and then set it back using setContext, rather than directly using SecurityContextHolder.getContext().setAuthentication(token)
, which can lead to race conditions.
Custom APIKeyAuthenticationProvider
public class APIKeyAuthenticationProvider implements AuthenticationProvider {
UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String apiKey = (String) authentication.getCredentials();
UserDetails userdetails = null;
userdetails = userDetailsService.loadUserByAPIKey(authentication.getPrincipal().toString());
APIKeyAuthenticationToken newAuth = new APIKeyAuthenticationToken(userdetails, userdetails.getAuthorities());
newAuth.setAuthenticated(true);
return newAuth;
}
@Override
public boolean supports(Class<?> authentication) {
return APIKeyAuthenticationToken.class.isAssignableFrom
ProviderManager
will match all AuthenticationProviders
based on the Token in the SecurityContext
, and the supports
method is used for matching. Once matched, the authenticate
method can be called for authentication. Here, you can obtain user information by querying the database or calling other services. Note that when handling exception
s, any requests that fail authentication must be returned as an exception of the AuthenticationException
subclass, so that Spring Filter can capture the exception and return the corresponding error message.
APIKeyAuthenticationProvider Registery
After custom this provider, the next step is register it into filter chain.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
final APIKeyFilter apiKeyFilter;
public SecurityConfiguration(APIKeyFilter apiKeyFilter) {
this.apiKeyFilter = apiKeyFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new APIKeyAuthenticationProvider());
}
}
Work to be done
After Spring Security 5.7, WebSecurityConfigurerAdapter
has been deprecated. The code modified according to the official documentation does not work properly. After the Token is set in the filter, ProviderManager
did not trigger the call to our custom APIKeyAuthenticationProvider
. This is probably a bug. In the community, there is an issue that combines the AuthenticationManager
and AuthenticationProvider
interfaces, so the solution to the problem with version 5.7 has been temporarily postponed.
Conclusion
Compared with other languages, Java has a rich set of components and frameworks, but this is a double-edged sword: having many components allows for faster development speed, but also increases the learning cost. Spring Security can quickly complete 90% of the work, but the remaining 10% of special requirements may take three to five times longer to configure and debug.
I prefer the philosophy of being precise and efficient: if you haven’t started using Java or Spring, I recommend choosing a simpler language and framework, such as Golang or Python; if you have already started using Spring Security, it is best to study it thoroughly and make full use of its features. Of course, as an open-source framework, Spring Security’s design and implementation are worth learning.
Reference
-
Source code:https://github.com/kidylee/spring-new-authentication
-
Spring Security Custom Authentication Provider Accessed 29 Aug. 2022.
-
Architecture :: Spring Security. Accessed 29 Aug. 2022.
-
Servlet Authentication Architecture :: Spring Security Accessed 29 Aug. 2022.
-
Spring Security without the WebSecurityConfigurerAdapter. 21 Feb. 2022 Accessed 29 Aug. 2022.