Java 11, Spring Boot 2.1.3, Spring 5.1.5
I have a Spring Boot project in which certain endpoints are guarded by an API key. This works just fine at the moment with this
In your requirement, as there is no ROLES(Different client's having deifferent access level) UserDetailService is not required.
APIKeyFilter is enough to work with X509 and API key.
Consider APIKeyFilter
extends X509AuthenticationFilter
, If there is a request without valid certificate then filter chain will be broken and error response of 403
/Forbidden
will be sent.
If certificate is valid then filter chain continues and authentication will be carried out. While validating what we have is only two methods from authentication object
getPrincipal()
- header:"x-api-key"
getCredential()
- certificate subject
. Where subject is (EMAIL=, CN=, OU=, O=, L=, ST=, C=)
(APIKeyFilter should be configured to return principal and credential object)
You can use principal(Your API key) for validating api key sent by client. and
You can use credentials(certificate subject) as a enhancement to identify each client seperately and if required you can grant different authorities for different client.
Recalling your requirement
1. API V1 - Accessed only if Certificate and API key valid.
2. Other APIs - No restrictions
To achieve the above said requirement, necessary codes given below
public class APIKeyFilter extends X509AuthenticationFilter
{
private String principalRequestHeader;
public APIKeyFilter(String principalRequestHeader)
{
this.principalRequestHeader = principalRequestHeader;
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request)
{
return request.getHeader(principalRequestHeader);
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request)
{
X509Certificate[] certs = (X509Certificate[]) request
.getAttribute("javax.servlet.request.X509Certificate");
if(certs.length > 0)
{
return certs[0].getSubjectDN();
}
return super.getPreAuthenticatedCredentials(request);
}
}
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String API_KEY_HEADER = "x-api-key";
private String apiKey = "SomeKey1234567890";
@Override
protected void configure(HttpSecurity http) throws Exception
{
APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);
filter.setAuthenticationManager(authentication -> {
if(authentication.getPrincipal() == null) // required if you configure http
{
throw new BadCredentialsException("Access Denied.");
}
String apiKey = (String) authentication.getPrincipal();
if (authentication.getPrincipal() != null && this.apiKey.equals(apiKey))
{
authentication.setAuthenticated(true);
return authentication;
}
else
{
throw new BadCredentialsException("Access Denied.");
}
});
http.antMatcher("/v1/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(filter)
.authorizeRequests()
.anyRequest()
.authenticated();
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
}
https - used for data encryption (ssl certificate sent by server to client)
X509 - used for client identification (ssl certificates generated by using server ssl certificate but different for different clients)
API key - shared secret key for security check.
For verification purpose lets assume you have 3 versions as given below
@RestController
public class HelloController
{
@RequestMapping(path = "/v1/hello")
public String helloV1()
{
return "HELLO Version 1";
}
@RequestMapping(path = "/v0.9/hello")
public String helloV0Dot9()
{
return "HELLO Version 0.9";
}
@RequestMapping(path = "/v0.8/hello")
public String helloV0Dot8()
{
return "HELLO Version 0.8";
}
}
Below given response in different cases.
CASE 1.a Version 1 with valid X509 and API key in header
curl -ik --cert pavel.crt --key myPrivateKey.pem -H "x-api-key:SomeKey1234567890" "https://localhost:8443/v1/hello"
Response
HTTP/1.1 200
HELLO Version 1
curl -ik --cert pavel.crt --key myPrivateKey.pem "https://localhost:8443/v1/hello"
Response
HTTP/1.1 403
{"timestamp":"2019-09-13T11:53:29.269+0000","status":403,"error":"Forbidden","message":"Access Denied","path":"/v1/hello"}
2. Version X without X509 and without API key in header.
curl "https://localhost:8443/v0.9/hello"
If server certificate is self signed certificate(Certificate is invalid without CA i.e, Certification Authority)
curl performs SSL certificate verification by default, using a "bundle"
of Certificate Authority (CA) public keys (CA certs). If the default
bundle file isn't adequate, you can specify an alternate file
using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
the bundle, the certificate verification probably failed due to a
problem with the certificate (it might be expired, or the name might
not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
the -k (or --insecure) option.
curl "https://localhost:8443/v0.9/hello"
HELLO Version 0.9
curl "https://localhost:8443/v0.8/hello"
Note: Testing Hack if you don't have CA certified SSL certificate in dev environmentHELLO Version 0.8
Use the server certificate(.crt) and serverPrivateKey(.pem file) along with request as given below
curl -ik --cert server.crt --key serverPrivateKey.pem "https://localhost:8443/v0.9/hello"
This can also be verified in Mozilla(for self signed certificate) and can be verified the same in google chrome(if CA certified SSL)
Screen shot given, During first time access
After adding certificate sent by server.