Securing Spring Boot API with API key and secret
I realize I am a little late to the game on this one, but I also managed to get API keys working with Spring Boot in tandem with user-name/password authentication. I wasn't crazy about the idea of using AbstractPreAuthenticatedProcessingFilter
because in reading the JavaDoc, it seemed like a misuse of that particular class.
I ended up creating a new ApiKeyAuthenticationToken
class along with a pretty simple raw servlet filter to accomplish this:
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
@Transient
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
private String apiKey;
public ApiKeyAuthenticationToken(String apiKey, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.apiKey = apiKey;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return apiKey;
}
}
And the filter
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
public class ApiKeyAuthenticationFilter implements Filter {
static final private String AUTH_METHOD = "api-key";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
String apiKey = getApiKey((HttpServletRequest) request);
if(apiKey != null) {
if(apiKey.equals("my-valid-api-key")) {
ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
SecurityContextHolder.getContext().setAuthentication(apiToken);
} else {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(401);
httpResponse.getWriter().write("Invalid API Key");
return;
}
}
}
chain.doFilter(request, response);
}
private String getApiKey(HttpServletRequest httpRequest) {
String apiKey = null;
String authHeader = httpRequest.getHeader("Authorization");
if(authHeader != null) {
authHeader = authHeader.trim();
if(authHeader.toLowerCase().startsWith(AUTH_METHOD + " ")) {
apiKey = authHeader.substring(AUTH_METHOD.length()).trim();
}
}
return apiKey;
}
}
All that is left at this point is to inject the filter at the proper location in the chain. In my case, I wanted API key authentication to be evaluated before any user-name / password authentication so that it could authenticate the request before the application tried to redirect to a login page:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.addFilterBefore(new ApiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin();
}
One other thing I will say you should watch out for is that your API key authenticated requests don't create and abandon a bunch of HttpSession
s on your server.
Create a filter that grabs what ever header(s) you're using for authentication.
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private String principalRequestHeader;
public APIKeyAuthFilter(String principalRequestHeader) {
this.principalRequestHeader = principalRequestHeader;
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(principalRequestHeader);
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "N/A";
}
}
Configure the filter in your Web Security config.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${yourapp.http.auth-token-header-name}")
private String principalRequestHeader;
@Value("${yourapp.http.auth-token}")
private String principalRequestValue;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
APIKeyAuthFilter filter = new APIKeyAuthFilter(principalRequestHeader);
filter.setAuthenticationManager(new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String principal = (String) authentication.getPrincipal();
if (!principalRequestValue.equals(principal))
{
throw new BadCredentialsException("The API key was not found or not the expected value.");
}
authentication.setAuthenticated(true);
return authentication;
}
});
httpSecurity.
antMatcher("/api/**").
csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
and().addFilter(filter).authorizeRequests().anyRequest().authenticated();
}
}