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 exceptions, 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