100

I would like to secure the Spring Boot API so it is accessible only for the clients that has valid API key and secret. However, there is no authentication (standard login with username and password) inside the program as all data is anonymous. All I'm trying to achieve is that all API requests can be used only for specific third party front-end.

I found a lot of articles about how to secure the Spring Boot API with user authentication. But I don't need user authentication. What I am thinking of is just provide my client with API key and secret so he has access to the endpoints.

Could you please suggest me how can I achieve this? Thank you!

Vitalii Oleksiv
  • 1,155
  • 2
  • 9
  • 11

5 Answers5

113

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();
    }

}
riccardo.cardin
  • 7,971
  • 5
  • 57
  • 106
MarkOfHall
  • 3,334
  • 1
  • 26
  • 30
  • 1
    this was quite helpful. I have an application which needs to support both Username/Password and ApiKey based authentication. I had Username/Password working and after reading your posting was able to get ApiKey working. Unfortunately, I seemed to break the Username/Password. I am suspicious it is the ordering of my filters or my use of the same AuthenticationManager for both Username/Password and ApiKey auth. Any advice? – Phillip Stack Apr 12 '19 at 17:42
  • 2
    @PhillipStack You should be able to configure two WebSecurityConfigurerAdapter with different authentication managers ala: https://stackoverflow.com/questions/33603156/spring-security-multiple-http-config-not-working – MarkOfHall Apr 15 '19 at 20:02
  • 1
    If I understood correctly, the APIKey is not private. Anyone using the client can open the developer console and check the header content. Is it right? – marcellorvalle Oct 16 '19 at 16:30
  • 5
    @marcellorvalle Typically, the client of an API secured with an API key is another service. If you're inferring that the client of this API would be a web browser, I would suggest you look into OAuth / JWT tokens for user authorization. – MarkOfHall Oct 21 '19 at 16:03
  • Shouldn't it be the other way around? That the principal should be "N/A", "API Key Holder" or similar and then the credential would be the actual API key? – Mikael Vandmo Jun 24 '20 at 08:30
  • great very helpful – Reza Aug 11 '20 at 10:38
  • 2
    Useful must-read : [REST Security Cheat Sheet / API Keys](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html#api-keys) – Guillaume Husta Nov 02 '21 at 16:42
  • I tried to implement this, but my authenticate() method was not being called. I implemented the Filter-based answer below and it worked perfectly. – Neil Mar 19 '22 at 19:27
  • What if we have a table of API keys to check for each user? – Cugomastik Nov 15 '22 at 09:26
28

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 HttpSessions on your server.

matt forsythe
  • 3,863
  • 1
  • 19
  • 29
  • this really worked for me. but, is it recommended using in production? – Vishal Patel Oct 26 '20 at 13:09
  • 1
    API keys in general are less secure than, say OAuth. But they are simpler, which is part of the appeal. Weather or not this trade-off is worth it depends on your needs and how your application is deployed. My particular application was an internal application, not accepting connections from the outside world, so the trade-off was worth it in my case. But I would not, for example, deploy an API key to a mobile application as the sole security mechanism because any user of the application will be able to get that API key. – matt forsythe Oct 27 '20 at 18:42
  • 1
    @mattforsythe You are correct but api keys are usually meant to be used in private environment. To use it in a mobile application, in theory, you need to create some kind of proxy with your backend. – Wanny Miarelli Jan 08 '22 at 23:16
  • 1
    @WannyMiarelli, correct. As I mentioned in my comment, my application was a private, internal one that did not accept connections from the outside internet. This is what made it ideal in my case. I think we are saying the same thing, right? – matt forsythe Jan 10 '22 at 03:54
  • How can I combine it with a bearer token filter, such that api keys authorizated requests will get a different group than the token requests. In such a way that I can use it with @PreAuthorize. For example some endpoints will be publicly available with an api key, others are protected behind a login – daydr3amer May 17 '22 at 17:37
  • Got it. Change AuthorityUtils.NO_AUTHORITIES to any role you would like to give to your api key – daydr3amer May 17 '22 at 17:54
  • @daydr3amer - glad you worked it out, but do be careful in using API keys for publicly available endpoints. If your intent is to guard your endpoints against unauthorized use, and your API keys get deployed as part of a mobile app (for example) then anyone who can download the app can easily get the key and use it how they like. – matt forsythe May 19 '22 at 15:35
  • @mattforsythe what's the purpose of transient annotation for ApiKeyAuthenticationToken class? – Muhammad Abu Bakr Sep 08 '22 at 05:49
  • 2
    @MuhammadAbuBakr, `@Transient` instructs Spring not to create an HttpSession for this request. In the case of stateless requests using API keys, you don't need an HttpSession being created, and if the client happens to ignore the cookies sent back from the server, each request would end up creating a brand new HttpSession on the server. If you need state stored between requests, and your clients save and honor cookies, feel free to take it out. – matt forsythe Sep 10 '22 at 04:45
12

To build on the answer by @MarkOfHall, WebSecurityConfigurerAdapter has been deprecated (see https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter). So his version of APISecurityConfig will now look like:

package com.fasset.ledger.auth;
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.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;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(1)
public class APISecurityConfig {

@Value("${yourapp.http.auth-token-header-name}")
private String principalRequestHeader;

@Value("${yourapp.http.auth-token}")
private String principalRequestValue;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) 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;
        }
    });
    http.antMatcher("/api/**").
            csrf().disable().
            sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
            and().addFilter(filter).authorizeRequests().anyRequest().authenticated();

    return http.build();
  }
}
sendon1982
  • 9,982
  • 61
  • 44
zawar
  • 155
  • 1
  • 7
9

The answer from the @MarkOfHall is correct and I just want to add a little more details. After you have the code, you will need to add the property values to the application.properties file as below:

yourapp.http.auth-token-header-name=X-API-KEY
yourapp.http.auth-token=abc123

The set the authentication value in the Postman as below:

enter image description here

You can use Postman but if you use cURL request will be similar provided below:

$ curl -H "X-API-KEY: abc123" "http://localhost:8080/api/v1/property/1"

Unless if provide the correct key and value, the app will not work.

Arefe
  • 11,321
  • 18
  • 114
  • 168
6

Building upon the answers of @zawar and @MarkOfHall, and from https://github.com/gregwhitaker/springboot-apikey-example

a contemporary solution as of December 8th 2022 would look like this:

package com.mygloriousapp.auth;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

/**
 * Filter responsible for getting the api key off of incoming requests that need to be authorized.
 */
public class ApiKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {

  private final String headerName;

  public ApiKeyAuthFilter(final String headerName) {
    this.headerName = headerName;
  }

  @Override
  protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
    return request.getHeader(headerName);
  }

  @Override
  protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
    // No credentials when using API key
    return null;
  }
}




package com.mygloriousapp.config;

import com.mygloriousapp.auth.ApiKeyAuthFilter;
import java.util.Objects;
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.BadCredentialsException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig {

  @Value("${app.http.auth-token-header-name}")
  private String principalRequestHeader;

  @Value("${app.http.auth-token}")
  private String principalRequestValue;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ApiKeyAuthFilter filter = new ApiKeyAuthFilter(principalRequestHeader);
    filter.setAuthenticationManager(
        authentication -> {
          String principal = (String) authentication.getPrincipal();
          if (!Objects.equals(principalRequestValue, principal)) {
            throw new BadCredentialsException(
                "The API key was not found or not the expected value.");
          }
          authentication.setAuthenticated(true);
          return authentication;
        });
    http.antMatcher("/**")
        .csrf()
        .disable()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .addFilter(filter)
        .authorizeRequests()
        .anyRequest()
        .authenticated();

    return http.build();
  }
}

The required configuration in application.properties:

app.http.auth-token-header-name=X-API-Key
app.http.auth-token=109353c6-6432-4acf-8e77-ef842f64a664

The dependeny in pom.xml:

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

If you are using Postman, click on the collection and edit the Authorization tab: Postman autorization

Victor Ionescu
  • 1,967
  • 2
  • 21
  • 24