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 code:
@Component("securityConfig")
@ConfigurationProperties("project.security")
@EnableWebSecurity
@Order(1)
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOG = LoggerFactory.getLogger(SecurityJavaConfig.class);
private static final String API_KEY_HEADER = "x-api-key";
private String apiKey;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);
filter.setAuthenticationManager(authentication -> {
String apiKey = (String) authentication.getPrincipal();
if (this.apiKey != null && !this.apiKey.isEmpty() && this.apiKey.equals(apiKey)) {
authentication.setAuthenticated(true);
return authentication;
} else {
throw new BadCredentialsException("Access Denied.");
}
});
httpSecurity
.antMatcher("/v1/**")
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(filter)
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
This successfully requires a header containing an API key, but only for endpoints in /v1/...
I have a new requirement to require certificate for authentication. I followed these guides to get X.509 authentication set-up in my project:
- Baeldung
- DZone
- Codecentric
I am running into a few problems, however:
- Cert is ALWAYS required, not just for
/v1/*
endpoints - API key filter no longer works
Here's my updated application.properties
file:
server.port=8443
server.ssl.enabled=true
server.ssl.key-store-type=PKCS12
server.ssl.key-store=classpath:cert/keyStore.p12
server.ssl.key-store-password=<redacted>
server.ssl.trust-store=classpath:cert/trustStore.jks
server.ssl.trust-store-password=<redacted>
server.ssl.trust-store-type=JKS
server.ssl.client-auth=need
And my updated SecurityJavaConfig
class:
@Component("securityConfig")
@ConfigurationProperties("project.security")
@EnableWebSecurity
@Order(1) //Safety first.
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOG = LoggerFactory.getLogger(SecurityJavaConfig.class);
private static final String API_KEY_HEADER = "x-api-key";
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/ping")
);
private String apiKey;
@Value("#{'${project.security.x509clients}'.split(',')}")
private List<String> x509clients;
@Override
public void configure(final WebSecurity web) {
web.ignoring().requestMatchers(PUBLIC_URLS);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
APIKeyFilter filter = new APIKeyFilter(API_KEY_HEADER);
filter.setAuthenticationManager(authentication -> {
String apiKey = (String) authentication.getPrincipal();
if (this.apiKey != null && !this.apiKey.isEmpty() && this.apiKey.equals(apiKey)) {
authentication.setAuthenticated(true);
return authentication;
} else {
throw new BadCredentialsException("Access Denied.");
}
});
httpSecurity
.antMatcher("/v1/**")
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(filter)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService())
.and()
.csrf()
.disable();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) {
if (x509clients.contains(username)) {
return new User(
username,
"",
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")
);
} else {
throw new UsernameNotFoundException("Access Denied.");
}
}
};
}
}
I have a feeling that there's an issue with the order of my chain in httpSecurity
methods, but I'm not sure what that is. Also, I tried adding the second configure()
method ignoring the PUBLIC_URLS
, but that did not help whatsoever. I also tried changing server.ssl.client-auth
to want
but it allows clients to connect to my /v1/*
APIs with no cert at all.
Example output that should not require a cert:
$ curl -k -X GET https://localhost:8443/ping
curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate
Example output that should require a cert AND an api-key:
$ curl -k -X GET https://localhost:8443/v1/clients
curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate
$ curl -k -X GET https://localhost:8443/v1/clients --cert mycert.crt --key mypk.pem
[{"clientId":1,"clientName":"Sample Client"}]