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:
<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.
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.
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.
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
.
@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:
@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:
@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.