Spring WS - Mutual Authentication Example

Mutual authentication or two-way authentication refers to two parties authenticating each other at the same time. In other words, the client must prove its identity to the server, and the server must prove its identity to the client before any traffic is sent over the client-to-server connection.

This example shows how to configure both client and server so that mutual authentication using certificates is enabled on a web service using Spring-WS, Spring Boot, and Maven.

General Project Setup

Tools used:

Spring-WS 2.4

HttpClient 4.5

Spring Boot 1.5

Maven 3.5

The setup of the project is based on a previous Spring WS HTTPS example in which we configured the server authentication part. We will extend this setup so that the client also authenticates itself towards the server.

Keytool is used to generate the different Java KeyStores (JKS) which contain the key pairs and public certificates for both client and server.

Subsequently execute the following three commands in order to generate the server-keystore.jks and client-truststore.jks needed to configure the server and client.

Note that we are specifying a DNS subject alternative name entry ("-ext san=dns:localhost") matching the 'localhost' hostname on the first keytool command. This way we do not need to override the HostnameVerifier like we did in the HTTPS client example.

Now (if needed) move the created JKS files into src/main/resources. The result should be as shown below:

If you would like to visualize the content of the above-generated artifacts you can use a tool like Portecle which is a Java-based GUI for managing keystores.

Setup the Client Keystore and Truststore

The details on the keystore and trustore are injected in the ClientConfig class using the @Value annotation. The values are defined in the application.yml properties file which is located under src/main/resources.

As the client needs to authenticate itself, a keystore needs to be configured that contains the private/public key pair of the client that was generated in the previous section.

Similar to the trustore setup, we use a loadKeyMaterial() method to load the keystore when building the SSLContext. The JKS file and password in addition to the password of the private key are specified.

packagecom.codenotfound.ws.client;importjavax.net.ssl.SSLContext;importorg.apache.http.client.HttpClient;importorg.apache.http.conn.ssl.NoopHostnameVerifier;importorg.apache.http.conn.ssl.SSLConnectionSocketFactory;importorg.apache.http.impl.client.HttpClientBuilder;importorg.apache.http.ssl.SSLContextBuilder;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.io.Resource;importorg.springframework.oxm.jaxb.Jaxb2Marshaller;importorg.springframework.ws.client.core.WebServiceTemplate;importorg.springframework.ws.transport.http.HttpComponentsMessageSender;importorg.springframework.ws.transport.http.HttpComponentsMessageSender.RemoveSoapHeadersInterceptor;@ConfigurationpublicclassClientConfig{@Value("${client.default-uri}")privateStringdefaultUri;@Value("${client.ssl.trust-store}")privateResourcetrustStore;@Value("${client.ssl.trust-store-password}")privateStringtrustStorePassword;@Value("${client.ssl.key-store}")privateResourcekeyStore;@Value("${client.ssl.key-store-password}")privateStringkeyStorePassword;@Value("${client.ssl.key-password}")privateStringkeyPassword;@BeanJaxb2Marshallerjaxb2Marshaller(){Jaxb2Marshallerjaxb2Marshaller=newJaxb2Marshaller();jaxb2Marshaller.setContextPath("org.example.ticketagent");returnjaxb2Marshaller;}@BeanpublicWebServiceTemplatewebServiceTemplate()throwsException{WebServiceTemplatewebServiceTemplate=newWebServiceTemplate();webServiceTemplate.setMarshaller(jaxb2Marshaller());webServiceTemplate.setUnmarshaller(jaxb2Marshaller());webServiceTemplate.setDefaultUri(defaultUri);webServiceTemplate.setMessageSender(httpComponentsMessageSender());returnwebServiceTemplate;}@BeanpublicHttpComponentsMessageSenderhttpComponentsMessageSender()throwsException{HttpComponentsMessageSenderhttpComponentsMessageSender=newHttpComponentsMessageSender();httpComponentsMessageSender.setHttpClient(httpClient());returnhttpComponentsMessageSender;}publicHttpClienthttpClient()throwsException{returnHttpClientBuilder.create().setSSLSocketFactory(sslConnectionSocketFactory()).addInterceptorFirst(newRemoveSoapHeadersInterceptor()).build();}publicSSLConnectionSocketFactorysslConnectionSocketFactory()throwsException{// NoopHostnameVerifier essentially turns hostname verification off as otherwise following error// is thrown: java.security.cert.CertificateException: No name matching localhost foundreturnnewSSLConnectionSocketFactory(sslContext(),NoopHostnameVerifier.INSTANCE);}publicSSLContextsslContext()throwsException{returnSSLContextBuilder.create().loadKeyMaterial(keyStore.getFile(),keyStorePassword.toCharArray(),keyPassword.toCharArray()).loadTrustMaterial(trustStore.getFile(),trustStorePassword.toCharArray()).build();}}

Setup the Server Keystore and Truststore

In addition to the setup of the server authentication we need to specify some additional Spring Boot web properties in the application properties file in order to trust the client that will connect to the exposed ticketing web service.

The 'client-auth' property specifies whether client authentication is wanted (“want”) or needed (“need”). In this example we set it to 'need' as we want to assure two-way SSL is established. The server’s truststore and the corresponding password are also configured so that the public certificate of the client is trusted.