SOAP over HTTPS with client certificate authentication

· April 30, 2016

Recently I had to consume a SOAP web service over HTTPS using client certificate authentication. I thought I will write a blog post about it describing my findings. For the example I will build a simple service which exposes team information about the UEFA EURO 2016 football championship. The service will be secured with client certificate authentication and accessible only over HTTPS.

Producer

First we define the web service domain with XML Schema, which Spring-WS will expose automatically as a WSDL. The schema defines that for a given country code we return information about the team like nick name, coach, which country they represent.

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.uefa.com/uefaeuro/season=2016/teams"
           targetNamespace="http://www.uefa.com/uefaeuro/season=2016/teams" elementFormDefault="qualified">

    <xs:element name="getTeamRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="countryCode" type="xs:string"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="getTeamResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="team" type="tns:team"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:complexType name="team">
        <xs:sequence>
            <xs:element name="countryCode" type="xs:string"/>
            <xs:element name="country" type="xs:string"/>
            <xs:element name="nickName" type="xs:string"/>
            <xs:element name="coach" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>

</xs:schema>

We generate domain classes from XSD file during build time using the maven-jaxb2-plugin plugin.

<plugin>
    <groupId>org.jvnet.jaxb2.maven2</groupId>
    <artifactId>maven-jaxb2-plugin</artifactId>
    <version>0.12.3</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <generatePackage>com.uefa.euro.season</generatePackage>
        <schemas>
            <schema>
                <url>${project.basedir}/src/main/resources/teams.xsd</url>
            </schema>
        </schemas>
    </configuration>
</plugin>

With Spring-WS we create endpoints (using the @Endpoint annotation) which handle incoming XML requests. The @PayloadRoot tells Spring-WS that the getTeam method knows how to handle XML messages that have getTeamRequest as local part with the http://www.uefa.com/uefaeuro/season=2016/teams namespace. The @ResponsePayload indicates that the return value of the method will be the payload of the response message.

@Endpoint
public class TeamEndpoint {

    private static final String NAMESPACE_URI = "http://www.uefa.com/uefaeuro/season=2016/teams";

    private TeamRepository teamRepository;

    public TeamEndpoint(TeamRepository teamRepository) {
        this.teamRepository = teamRepository;
    }

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getTeamRequest")
    @ResponsePayload
    public GetTeamResponse getTeam(@RequestPayload GetTeamRequest request) throws TeamNotFoundException, EmptyCountryCodeException {
        if (StringUtils.isEmpty(request.getCountryCode())) {
            throw new EmptyCountryCodeException("country code cannot be empty");
        }

        GetTeamResponse response = new GetTeamResponse();
        Team team = teamRepository.findByCountryCode(request.getCountryCode());

        if (team == null) {
            throw new TeamNotFoundException("invalid country code or country did not qualify");
        }

        response.setTeam(team);
        return response;
    }
}

To enable support for the Spring-WS annotations we use Java config, by declaring the @EnableWs annotation. The WSDL file is exposed with the help of DefaultWsdl11Definition bean definition, where the bean name determines under which name the generated WSDL file is available.

@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {

    @Bean
    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "/uefaeuro2016/*");
    }

    @Bean(name = "teams")
    public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema teamSchema) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("TeamsPort");
        wsdl11Definition.setLocationUri("/uefaeuro2016");
        wsdl11Definition.setTargetNamespace("http://www.uefa.com/uefaeuro/season=2016/teams");
        wsdl11Definition.setSchema(teamSchema);
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema countriesSchema() {
        return new SimpleXsdSchema(new ClassPathResource("teams.xsd"));
    }
}

After starting the uefa service the WSDL will be available at http://localhost:8080/uefaeuro2016/teams.wsdl. It can be easily tested with SoapUI after importing the WSDL file

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:team="http://www.uefa.com/uefaeuro/season=2016/teams">
   <soapenv:Header/>
   <soapenv:Body>
      <team:getTeamRequest>
      	<team:countryCode>HU</team:countryCode>
      </team:getTeamRequest>
   </soapenv:Body>
</soapenv:Envelope>

the response:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns2:getTeamResponse xmlns:ns2="http://www.uefa.com/uefaeuro/season=2016/teams">
         <ns2:team>
            <ns2:countryCode>HU</ns2:countryCode>
            <ns2:country>Hungary</ns2:country>
            <ns2:nickName>Mighty Magyars</ns2:nickName>
            <ns2:coach>Bernd Storck</ns2:coach>
         </ns2:team>
      </ns2:getTeamResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Enable HTTPS

So far so good, but we would like to secure the service with client certificate and making it only available over HTTPS.

First we need to get an SSL certificate (self-signed or get one from a certificate authority). Let’s generate a self-signed certificate with the keytool utility which comes bundled in JRE.

keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360

This will generate a keystore called keystore.jks with a newly generated certificate in it with certificate alias selfsigned, which you can check with the following command:

keytool -list -keystore keystore.jks -storepass password

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 1 entry

selfsigned, Apr 27, 2016, PrivateKeyEntry,
Certificate fingerprint (SHA1): 01:83:FE:F0:44:7A:87:63:66:4B:F0:2F:20:B7:D0:14:87:6D:A8:16

Then we use this certificate in our uefa service by declaring the followings in the default application.properties:

server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=password
server.ssl.key-alias=selfsigned

Here we included the keystore.jks into the project which is of course not recommended but for this simple example is ok.

After restarting the uefa service our WSDL file will be available at https://localhost:8443/uefaeuro2016/teams.wsdl. If you access it in Chrome browser for example, the browser will complain that it is using a self-signed certificate. In SoapUI we are no longer able to send SOAP messages to http://localhost:8080/uefaeuro2016 instead we need to use https://localhost:8443/uefaeuro2016 target url.

Authentication with client certificate

However any client is able to call the service. Let’s create separate certificates for two clients one for SoapUI and one for a java client.


keytool -genkey -keyalg RSA -alias soapui -keystore soapui.jks -storepass password -validity 360
keytool -genkey -keyalg RSA -alias javaclient -keystore javaclient.jks -storepass password -validity 360

// extract the certificate from the keystores
keytool -export -alias soapui -file soapui.crt -keystore soapui.jks -storepass password
keytool -export -alias javaclient -file javaclient.crt -keystore javaclient.jks -storepass password

Then for the uefa service we need to configure the truststore, which determines the remote authentication credentials which should be trusted, in contrast with keystore which determines the authentication credentials to send to the remote host. First we import the soapui and javaclient certificates into the truststore keystore. After the first import the truststore keystore will be automatically created.

keytool -import -alias soapui -file soapui.crt -keystore truststore.jks -storepass password
keytool -import -alias javaclient -file javaclient.crt -keystore truststore.jks -storepass password

The truststore should have both soapui and javaclient certificates:

keytool -list -keystore truststore.jks -storepass password

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 2 entries

javaclient, Apr 27, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 7A:44:89:59:21:C7:8D:24:C8:78:30:8C:B7:C1:C9:ED:B7:DD:19:ED
soapui, Apr 27, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 37:0F:1A:AF:03:BA:44:DC:BC:0A:9B:77:4B:60:5F:D5:B7:FC:63:6E

After this we configure the truststore of the uefa service.

server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=password
server.ssl.key-alias=selfsigned

server.ssl.trust-store=classpath:truststore.jks
server.ssl.trust-store-password=password
server.ssl.client-auth=need

Is important to set the server.ssl.client-auth to need in order to make the client authentication mandatory. Now SoapUI is not able to call our uefa service only just with a trusted certificate, otherwise it returns javax.net.ssl.SSLHandshakeException After configuring the client soapui certificate in the SoapUI Preferences -> SSL Settings form with KeyStore and KeyStore Password fields we can successfully send SOAP requests.

As an exercise you can create a dummy certificate (not included in the truststore of the service) and use it in SoapUI and verify that the connection is not established.

Java client

Now let’s connect to the uefa service from a java client. We are using the maven-jaxb2-plugin again to generate java classes from the exposed WSDL by our uefa service.

<plugin>
    <groupId>org.jvnet.jaxb2.maven2</groupId>
    <artifactId>maven-jaxb2-plugin</artifactId>
    <version>0.13.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <schemaLanguage>WSDL</schemaLanguage>
        <generatePackage>com.eufa.euro</generatePackage>
        <schemas>
            <schema>
                <url>${project.basedir}/src/main/resources/uefaeuro.wsdl</url>
            </schema>
        </schemas>
    </configuration>
</plugin>

We leverage the WebServiceGatewaySupport class from Spring-WS which provides a reference to an already configured WebServiceTemplate instance.

public class TeamClient extends WebServiceGatewaySupport {

    public GetTeamResponse getTeamByCountryCode(String countryCode) {
        GetTeamRequest request = new GetTeamRequest();
        request.setCountryCode(countryCode);

        GetTeamResponse response = (GetTeamResponse) getWebServiceTemplate().marshalSendAndReceive(request);

        return response;
    }
}

Next we need to configure the web service components. The Jaxb2Marshaller is responsible to serialize and deserialize XML requests. By default the WebServiceTemplate is using HttpUrlConnectionMessageSender to send messages which is not good for us since it does not come with HTTPS certificate support. Instead we use HttpsUrlConnectionMessageSender. We use the previously created javaclient (trusted by the uefa service) when setting the keystore of the java client. But we need to set also the truststore of the java client with the selfsigned certificate of uefa service in order have a successful SSL handshake.

@Configuration
public class WebServiceClientConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebServiceClientConfig.class);

    @Value("${uefa.ws.endpoint-url}")
    private String url;

    @Value("${uefa.ws.key-store}")
    private Resource keyStore;

    @Value("${uefa.ws.key-store-password}")
    private String keyStorePassword;

    @Value("${uefa.ws.trust-store}")
    private Resource trustStore;

    @Value("${uefa.ws.trust-store-password}")
    private String trustStorePassword;

    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("com.eufa.euro");
        return marshaller;
    }

    @Bean
    public TeamClient teamClient(Jaxb2Marshaller marshaller) throws Exception {
        TeamClient client = new TeamClient();
        client.setDefaultUri(this.url);
        client.setMarshaller(marshaller);
        client.setUnmarshaller(marshaller);

        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(keyStore.getInputStream(), keyStorePassword.toCharArray());

        LOGGER.info("Loaded keystore: " + keyStore.getURI().toString());
        try {
            keyStore.getInputStream().close();
        } catch (IOException e) {
        }
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(ks, keyStorePassword.toCharArray());

        KeyStore ts = KeyStore.getInstance("JKS");
        ts.load(trustStore.getInputStream(), trustStorePassword.toCharArray());
        LOGGER.info("Loaded trustStore: " + trustStore.getURI().toString());
        try {
            trustStore.getInputStream().close();
        } catch(IOException e) {
        }
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(ts);

        HttpsUrlConnectionMessageSender messageSender = new HttpsUrlConnectionMessageSender();
        messageSender.setKeyManagers(keyManagerFactory.getKeyManagers());
        messageSender.setTrustManagers(trustManagerFactory.getTrustManagers());

        // otherwise: java.security.cert.CertificateException: No name matching localhost found
        messageSender.setHostnameVerifier((hostname, sslSession) -> {
            if (hostname.equals("localhost")) {
                return true;
            }
            return false;
        });

        client.setMessageSender(messageSender);
        return client;
    }
}

Summary

In this post we saw how can we expose a simple SOAP web service over HTTPS and have two clients (soapUI, java client) connect to it using client certificates. If you would like to try out this have a look at this https://github.com/altfatterz/spring-ws-with-keystore repository.

Twitter