I'm trying to configure a reactive WebClient to use 2-way TLS. I used this answer as a reference. (The one using a WebClientCustomizer, not the one using an InsecureTrustManager).
I double-checked the keystores and truststores on both client and server side, but the server sends back an error saying that the client is not presenting any certificate:
@Bean
WebClientCustomizer configureWebclient(@Value("${server.ssl.trust-store}") String trustStorePath, @Value("${server.ssl.trust-store-password}") String trustStorePass,
@Value("${server.ssl.key-store}") String keyStorePath, @Value("${server.ssl.key-store-password}") String keyStorePass, @Value("${server.ssl.key-alias}") String keyAlias) {
return new WebClientCustomizer() {
@Override
public void customize(Builder webClientBuilder) {
SslContext sslContext;
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
List<Certificate> certificateCollcetion = Collections.list(trustStore.aliases()).stream().filter(t -> {
try {
return trustStore.isCertificateEntry(t);
} catch (KeyStoreException e1) {
throw new RuntimeException("Error reading truststore", e1);
}
}).map(t -> {
try {
return trustStore.getCertificate(t);
} catch (KeyStoreException e2) {
throw new RuntimeException("Error reading truststore", e2);
}
}).collect(Collectors.toList());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
sslContext = SslContextBuilder.forClient()
.keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))
.trustManager((X509Certificate[]) certificateCollcetion.toArray(new X509Certificate[certificateCollcetion.size()]))
.build();
} catch (Exception e) {
log.error("Error creating web client", e);
throw new RuntimeException(e);
}
ClientHttpConnector connector = new ReactorClientHttpConnector((opt) -> {
opt.sslContext(sslContext);
});
webClientBuilder.clientConnector(connector);
}
};
}
Can somebody please share insight on how to correctly configure a reactive WebClient to use 2-way TLS?
For some reason the server would not accept the client certificate when the ssl context was built like this:
sslContext = SslContextBuilder.forClient()
.keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))
.trustManager((X509Certificate[]) certificateCollcetion.toArray(new X509Certificate[certificateCollcetion.size()]))
.build();
To fix this, I had to initialize a KeyManagerFactory:
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePass.toCharArray());
Then I initialized the ssl context with the factory:
SslContext sslContext = SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.trustManager((X509Certificate[]) certificateCollection.toArray(new X509Certificate[certificateCollection.size()]))
.build();
After that, the server accepted the certificate and I could connect.
In summary, I used this cleaner solution that utilizes factories for both the key-store and the trust-store:
@Value("${server.ssl.trust-store}")
String trustStorePath;
@Value("${server.ssl.trust-store-password}")
String trustStorePass;
@Value("${server.ssl.key-store}")
String keyStorePath;
@Value("${server.ssl.key-store-password}")
String keyStorePass;
@Bean
public WebClient create2WayTLSWebClient() {
ClientHttpConnector connector = new ReactorClientHttpConnector(
options -> {
options.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
options.sslContext(get2WaySSLContext());
}
);
return WebClient.builder()
.clientConnector(connector)
.build();
}
private SslContext get2WaySSLContext() {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePass.toCharArray());
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(trustStore);
return SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.trustManager(trustManagerFactory)
.build();
} catch (Exception e) {
logger.error("Error creating 2-Way TLS WebClient. Check key-store and trust-store.");
e.printStackTrace();
}
return null;
}
Just a note, if you are using Spring 5.1 or newer, this specific implementation will not work as you can no longer pass HttpClientOptions to a ReactorClientHttpConnector. Use this link as a guide for that configuration. However the meat of the code in this answer should still be applicable to that sort of configuration.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With