Introduction:

In the realm of modern web applications, securing REST APIs is a critical aspect of ensuring data privacy and preventing unauthorized access. Spring Security provides several mechanisms to safeguard your APIs, one of which is API key and secret authentication. This approach involves using a unique token (API key) and a corresponding secret value to authenticate requests, adding an extra layer of security to your application.

Use Case Example:

Consider a scenario where you have developed a Spring Boot application that exposes various REST APIs for managing user data. To prevent unauthorized access and maintain data integrity, you want to implement API key and secret authentication. Only clients with a valid API key and secret combination should be able to access and interact with your APIs.

Required Dependencies:

To get started, add the following dependency to your project’s pom.xml file:

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

Implementation of Authentication with API Key and Secret :

1. Custom Authentication Filter

First, we’ll create a custom filter that extends the GenericFilterBean class. This filter will intercept incoming requests, extract the API key from the request headers, and perform authentication accordingly.

Java
public class AuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        try {
            Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest) request);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (Exception exp) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
            PrintWriter writer = httpResponse.getWriter();
            writer.print(exp.getMessage());
            writer.flush();
            writer.close();
        }

        filterChain.doFilter(request, response);
    }
}

The AuthenticationFilter class extends the GenericFilterBean class, which is a simple implementation of the javax.servlet.Filter interface. This filter is responsible for intercepting incoming requests and performing authentication based on the provided API key.

In the doFilter method, we first try to obtain an Authentication object by calling the AuthenticationService.getAuthentication method, passing the current HttpServletRequest object. This method will validate the API key present in the request headers and return an Authentication object if the API key is valid.

If the API key is valid, we set the Authentication object in the SecurityContextHolder for the current thread. This way, the authenticated principal (in this case, the API key) is available for subsequent processing in the filter chain.

If an exception occurs during the authentication process (e.g., an invalid API key), we handle the exception by setting the response status to HttpServletResponse.SC_UNAUTHORIZED (401), setting the response content type to application/json, and writing the exception message to the response writer.

Finally, we call filterChain.doFilter to pass the request and response objects to the next filter in the chain.

2. Authentication Service

Next, we’ll create an AuthenticationService class that will handle the API key validation and authentication process.

Java
public class AuthenticationService {

    private static final String AUTH_TOKEN_HEADER_NAME = "X-API-KEY";
    private static final String AUTH_TOKEN = "Jstobigdata";

    public static Authentication getAuthentication(HttpServletRequest request) {
        String apiKey = request.getHeader(AUTH_TOKEN_HEADER_NAME);
        if (apiKey == null || !apiKey.equals(AUTH_TOKEN)) {
            throw new BadCredentialsException("Invalid API Key");
        }

        return new ApiKeyAuthentication(apiKey, AuthorityUtils.NO_AUTHORITIES);
    }
}

In this example, we check if the X-API-KEY header is present in the request and matches the expected secret value (Jstobigdata). If the API key is invalid, we throw a BadCredentialsException. Otherwise, we create a new ApiKeyAuthentication object to represent the authenticated principal.

The AuthenticationService class contains a static method getAuthentication that performs the actual API key validation and authentication process.

First, we define two constants: AUTH_TOKEN_HEADER_NAME and AUTH_TOKEN. The AUTH_TOKEN_HEADER_NAME constant specifies the name of the request header that should contain the API key (in this case, X-API-KEY). The AUTH_TOKEN constant holds the expected secret value for the API key (in this example, it’s hardcoded as Jstobigdata).

In the getAuthentication method, we retrieve the value of the X-API-KEY header from the HttpServletRequest object. If the header is null or its value doesn’t match the expected secret (AUTH_TOKEN), we throw a BadCredentialsException to indicate that the API key is invalid.

If the API key is valid, we create a new instance of the ApiKeyAuthentication class, passing the API key value and an empty collection of authorities (since we’re not dealing with role-based authorization in this example). The ApiKeyAuthentication object represents the authenticated principal.

3. ApiKeyAuthentication

We’ll extend the AbstractAuthenticationToken class to create our custom ApiKeyAuthentication object.

Java
public class ApiKeyAuthentication extends AbstractAuthenticationToken {
    private final String apiKey;

    public ApiKeyAuthentication(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;
    }
}

This class holds the API key value and represents the authenticated principal.

The ApiKeyAuthentication class extends the AbstractAuthenticationToken class, which is part of the Spring Security framework. This class represents the authenticated principal (in this case, the API key).

In the constructor, we call the superclass constructor with an empty collection of authorities (since we’re not dealing with role-based authorization) and set the apiKey field with the provided API key value. We also call the setAuthenticated(true) method to mark the Authentication object as authenticated.

The getCredentials method returns null because we don’t have any separate credential object in this case (the API key itself serves as the principal).

The getPrincipal method returns the apiKey value, which represents the authenticated principal.

4. Security Configuration

Finally, we’ll configure Spring Security by creating a SecurityFilterChain bean and registering our custom AuthenticationFilter.

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
                authorizationManagerRequestMatcherRegistry.requestMatchers("/**").authenticated())
            .httpBasic(Customizer.withDefaults())
            .sessionManagement(httpSecuritySessionManagementConfigurer ->
                httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

In this configuration, we disable CSRF protection for simplicity, authorize all requests to require authentication, enable basic authentication, and set the session creation policy to stateless (since we’re dealing with REST APIs). We also register our custom AuthenticationFilter before the UsernamePasswordAuthenticationFilter.

In the SecurityConfig class, we create a SecurityFilterChain bean by configuring the HttpSecurity object.

First, we disable CSRF protection for simplicity (not recommended in production environments).

Then, we configure authorization rules using the authorizeHttpRequests method. In this example, we require authentication for all requests (requestMatchers("/**").authenticated()).

Next, we enable basic authentication using the httpBasic method with default settings.

We set the session creation policy to SessionCreationPolicy.STATELESS since we’re dealing with stateless REST APIs.

Finally, we register our custom AuthenticationFilter by calling the addFilterBefore method and specifying that it should be executed before the UsernamePasswordAuthenticationFilter.

At the end, we call the build method to create the SecurityFilterChain bean.

5. Resource Controller

Here’s an example of a simple ResourceController that exposes a /home endpoint:

Java
@RestController
public class ResourceController {

    @GetMapping("/home")
    public String homeEndpoint() {
        return "Jstobigdata!";
    }
}

6. Disable default auto-configuration:

We need to discard the security auto-configuration. To do this, we exclude the SecurityAutoConfiguration and UserDetailsServiceAutoConfiguration classes:

Java
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class})
public class CustomAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiKeySecretAuthApplication.class, args);
    }
}

Now, the application is ready to test.

Testing:

  • First, we try to access the /home endpoint without providing the API key. We expect a 401 Unauthorized response because authentication is required, and we didn’t provide the necessary API key.
  • Next, we include the X-API-KEY header with the correct secret value (Jstobigdata). This time, we expect a 200 OK response with the “Jstobigdata!” message, indicating successful authentication and access to the protected resource.

To test the secured API, you can use a tool like cURL or Postman. First, try accessing the /home endpoint without providing the API key:

curl --location --request GET 'http://localhost:8080/home'

You should receive a 401 Unauthorized response.

Next, include the X-API-KEY header with the correct secret value:

curl --location --request GET 'http://localhost:8080/home' \
--header 'X-API-KEY: Jstobigdata'

This time, you should receive a 200 OK response with the “Jstobigdata!” message, indicating successful authentication and access.

Conclusion:

Implementing API key and secret authentication in your Spring Boot application is an effective way to secure your REST APIs and protect sensitive data from unauthorized access. By following the steps outlined in this guide, you can easily integrate this authentication mechanism into your application, ensuring that only authorized clients with valid API keys and secrets can interact with your APIs.

Remember, while API key and secret authentication provides a layer of security, it should be combined with other best practices, such as SSL/TLS encryption, rate limiting, and regular key rotation, to further enhance the overall security of your application.

By |Last Updated: May 26th, 2024|Categories: Spring Security|

Table of Contents