SOAP over HTTPS with client certificate authentication

The tutorial, SOAP over HTTPS with client certificate authentication, will show you how we can use client certificate to handshake with server along with basic authentication for consuming the service. We have also seen how to authenticate by sending authentication information over http headers in SOAP web service but here we will use client certificate (jks file) as a security mechanism. Even you can use header authentication along with client certificate to make more secure.

I will show here both server side code or service and client side code so that server expects client to establish communication through certificate authentication. Here to consume the service you will be given client certificate (extention might be .crt or .der or .p12 or anything else), password for this certificate and username/password for basic authentication (in case if you need also header authentication).

You may also like to read Spring SOAP web service producers and Spring SOAP web service consumers

Prerequisites

Knowledge of Java, SOAP
Softwares
Eclipse
JDK 1.8
Gradle 4.x
Spring Boot

Here actually we will create soap web service producer and soap web service consumer to finish the example about soap over https with client certificate authentication.

Creating and setting up Gradle project in Eclipse

Create gradle project called spring-boot-soap-https-authentication using the following gradle dependencies. In the below build script we have defined jaxb configurations in order to generate jaxb classes from xsd files.

You may also refer to the similar example Spring SOAP Web Service Producers

Currently we do not have any jaxb plugin available in Gradle. That’s why we have written one task called jaxb to generate the jaxb classes from xsd files. We have specified the folder src/generated-sources/java where the jxb classes should be generated under the package com.jeejava.jaxb.

buildscript {
	ext {
		springBootVersion = '1.5.9.RELEASE';
	}
                
    repositories {
        mavenLocal()
		mavenCentral()       
    }
    
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

allprojects {
    apply plugin: 'eclipse'
	apply plugin: 'idea'
	apply plugin: 'org.springframework.boot'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

sourceSets.main.java.srcDirs "src/generated-sources/java"
sourceSets.main.resources.excludes = ['temperature.xsd']

configurations {
    jaxb
}

repositories {
    mavenLocal()
	mavenCentral()
}
    
dependencies {
	compile("org.springframework.boot:spring-boot-starter-web-services:${springBootVersion}")
	compile("wsdl4j:wsdl4j:1.6.2")
    jaxb (
        'com.sun.xml.bind:jaxb-xjc:2.2.7',
        'com.sun.xml.bind:jaxb-impl:2.2.7'
    )
}

task jaxb {
    System.setProperty('javax.xml.accessExternalSchema', 'all')
    def jaxbTargetDir = file("src/generated-sources/java")
    doLast {
        jaxbTargetDir.mkdirs()
        ant.taskdef(
                name: 'xjc',
            classname: 'com.sun.tools.xjc.XJCTask',
            classpath: configurations.jaxb.asPath
        )
        
        ant.jaxbTargetDir = jaxbTargetDir
        ant.xjc(
            destdir: '${jaxbTargetDir}',
            package: 'com.jeejava.jaxb',
            schema: 'src/main/resources/xsd/temperature.xsd'
        )
    }
}

compileJava.dependsOn jaxb

You should be able to build the blank project. Please ensure that the overall state is “BUILD SUCCESS” before continuing.

If you get any exception like “Unable to find main class”, then create a main class Application under package com.jeejava.main.

Note: You won’t be able to import Spring Boot dependencies in main class until your project downloads all dependencies. So first create main class with empty main method and later when your project is successfully built then you can import required dependencies.

Creating XSD

Create an XSD (schema definition file) under src/main/resources/xsd/temperature.xsd.

As you see in the below xsd file we have two input requests and two output responses because we want to convert temperature from Celsius to Fahrenheit.

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
	xmlns:tns="http://www.example.com/TemperatureService" targetNamespace="http://www.example.com/TemperatureService"
	elementFormDefault="qualified">
	<xs:element name="GetCelsiusRequest">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="celcius" type="xs:double" />
			</xs:sequence>
		</xs:complexType>
	</xs:element>
	<xs:element name="GetCelsiusResponse">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="fahrenheit" type="xs:double" />
			</xs:sequence>
		</xs:complexType>
	</xs:element>
</xs:schema>

SOAP web service configuration

Spring WS uses a different servlet type for handling SOAP messages: MessageDispatcherServlet. It is important to inject and set ApplicationContext to MessageDispatcherServlet. Without this, Spring WS will not detect Spring beans automatically.

By naming this bean messageDispatcherServlet, it does not replace Spring Boot’s default DispatcherServlet bean.

DefaultMethodEndpointAdapter configures annotation driven Spring WS programming model. This makes it possible to use the various annotations like @Endpoint mentioned earlier.

DefaultWsdl11Definition exposes a standard WSDL 1.1 using XsdSchema.

It’s important to notice that we need to specify bean names for MessageDispatcherServlet and DefaultWsdl11Definition. Bean names determine the URL under which web service and the generated WSDL file is available.

In this case, the WSDL will be available under http://<host>:<port>/ws/temp.wsdl.

package com.jeejava.config;

import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;

@EnableWs
@Configuration
public class SoapWebServiceConfig extends WsConfigurerAdapter {
	@Bean
	public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
		MessageDispatcherServlet servlet = new MessageDispatcherServlet();
		servlet.setApplicationContext(applicationContext);
		servlet.setTransformWsdlLocations(true);
		return new ServletRegistrationBean(servlet, "/ws/*");
	}

	@Bean(name = "temp")
	public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) {
		DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
		wsdl11Definition.setPortTypeName("TempPort");
		wsdl11Definition.setLocationUri("/ws");
		wsdl11Definition.setTargetNamespace("http://www.example.com/TemperatureService");
		wsdl11Definition.setSchema(countriesSchema);
		return wsdl11Definition;
	}

	@Bean
	public XsdSchema helloSchema() {
		return new SimpleXsdSchema(new ClassPathResource("xsd/temperature.xsd"));
	}

}

Create the service layer

Create service layer class TemperatureService for converting temperature from fahrenheit to celcius and celcius to fahrenheit.

This class has one method – convertCelsiusToFahrenheit converts celsius temperature to fahrenheit temperature.

package com.jeejava.service;

import org.springframework.stereotype.Service;

@Service
public class TemperatureService {
	public double convertCelsiusToFahrenheit(final double celsius) {
		double fahrenheit = (9.0 / 5.0) * celsius + 32;
		return fahrenheit;
	}
}

Create user service endpoint

To create a service endpoint, we only need a POJO with a few Spring WS annotations to handle the incoming SOAP requests.

@Endpoint registers the class with Spring WS as a potential candidate for processing incoming SOAP messages.

@PayloadRoot is then used by Spring WS to pick the handler method based on the message’s namespace and localPart.

@RequestPayload indicates that the incoming message will be mapped to the method’s request parameter.

The @ResponsePayload annotation makes Spring WS map the returned value to the response payload.

package com.jeejava.soap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

import com.jeejava.jaxb.GetCelsiusRequest;
import com.jeejava.jaxb.GetCelsiusResponse;
import com.jeejava.service.TemperatureService;

@Endpoint
public class TemperatureServiceEndpoint {
	final String NAMESPACE = "http://www.example.com/TemperatureService";

	@Autowired
	TemperatureService temperatureService;

	@ResponsePayload
	@PayloadRoot(namespace = NAMESPACE, localPart = "GetCelsiusRequest")
	public GetCelsiusResponse getFahrenheit(@RequestPayload final GetCelsiusRequest input) {
		GetCelsiusResponse response = new GetCelsiusResponse();
		response.setFahrenheit(temperatureService.convertCelsiusToFahrenheit(input.getCelcius()));
		return response;
	}
}

Change default port

We don’t want tomcat server in spring boot application to be started on random port, so set the server port in src/main/resources/application.properties

server.port=9999

Test the application

Open SOAPUI and use the WSDL URL as http://localhost:9999/ws/temp.wsdl and Endpoint as http://localhost:9999/ws

Enabling HTTPS in SOAP web service

As we need to secure the service with client certificate and making it only available over HTTPS.

First we need to get an SSL certificate. Either you have to get it self-signed or from certificate authority.

We can easily generate self-signed certificate using Java’s built-in keytool utility.

Let’s generate a self-signed certificate using the following command in cmd prompt:

if you have already setup environment variable for Java then you may be able to generate from any path location or you may navigate to the jdk bin directory from cmd prompt and execute the following command. Please note that password must be at least six characters.

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

When you type the above command you will be asked few questions and you may answer them similar to as shown in the below image

soap over https with client certificate authentication

So you have successfully generated a keystore called certificate.jks with a newly generated certificate in it with certificate alias selfsigned and password changeit and validity for this certificate is 360 days.

Now check the generated certificate or keystore using the following command from command prompt.

keytool -list -keystore certificate.jks -storepass changeit

You will see the below information in the cmd prompt as shown in the below image

soap over https with client certificate authentication

Then we use this certificate in our temperature service by declaring the followings in the default application.properties in src/main/resources directory.

Now you see we have updated port from 9999 to 8443 to use https instead of http protocol.

Please make sure you have put the certificate.jks file under classpath src/main/resources directory.

server.port=8443

server.ssl.key-store=classpath:certificate.jks
server.ssl.key-store-password=changeit
server.ssl.key-alias=selfsigned

Now run the application and you would be able to see the WSDL file at https://localhost:8443/ws/temp.wsdl and Endpoint is https://localhost:8443/ws.

So when you access it in browser, the browser will complain that it is using a self-signed certificate. This secure service is now accessible by any client. Therefore we need to generate client certificate as well so that only particular that client will be able to access the service.

Authenticate using client certificate

Now let’s create separate certificate for client. Here we will access the service from Java code, so we will create client certificate for Java client.

If you access the service from other clients as well then create certificate for each client you are accessing from.

Use the following command in cmd prompt in order to generate client certificate for Java client:

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

So when prompt for several questions then give the same answers you had give while generating the server certificate.

Now we need to extract the certificate from truststore for Java client because we need to import this certificate for remote authentication also.

keytool -export -alias javaclient -file javaclient.crt -keystore javaclient.jks -storepass changeit

So the certificate file javaclient.crt gets generated.

Now we have to add the above generated certificate to keystore in order to establish the handshake between Java client and soap server.

Use below command in order to do it

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

Once prompted for Trust this certificate? [no]: . Type yes.

soap over https with client certificate authentication

Now check the truststore should have javaclient certificate using below command:

soap over https with client certificate authentication

Now we need to configure also javaclient truststore at server side so that server knows who is trying to establish connection among themselves. So the whole application.properties file looks similar to below.

server.port=8443

server.ssl.key-store=classpath:certificate.jks
server.ssl.key-store-password=changeit
server.ssl.key-alias=selfsigned

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

It is mandatory to set the server.ssl.client-auth=need in order to make the client authentication mandatory.

So now you neither be able to view wsdl nor be able to connect to service from anywhere except Java client.

The final project structure for server side or soap service producer for soap over https with client certificate authentication is given below

soap over https with client certificate authentication

So we are done with the server side code for soap over https with client certificate authentication. We have also generated required certificates for server and client. Now we will create soap web service consumer for consume the above service.

Creating and setting up the gradle project

Now create and setup the project using below build script. We have the temperature wsdl file from the above service.

For more information on generating stub from wsdl in gradle you may read here and here.

buildscript {
    repositories {
        jcenter()
        mavenCentral()
    }

    dependencies {
        classpath 'no.nils:wsdl2java:0.10'
    }
}

apply plugin: 'java'
apply plugin: 'no.nils.wsdl2java'

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

wsdl2javaExt {
	cxfVersion = "3.1.10"
}

wsdl2java{
	generatedWsdlDir = file("${projectDir}/src/main/service")
	wsdlDir=file("${projectDir}/src/main/resources/wsdl/")
	wsdlsToGenerate = [
		[file("${projectDir}/src/main/resources/wsdl/temperature.wsdl")]
	]
}

compileJava.dependsOn wsdl2java

The temperature.wsdl file content is given below

<?xml version="1.0" encoding="UTF-8" standalone="no"?><wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:sch="http://www.example.com/TemperatureService" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.example.com/TemperatureService" targetNamespace="http://www.example.com/TemperatureService">
  <wsdl:types>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.example.com/TemperatureService">
	<xs:element name="GetCelsiusRequest">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="celcius" type="xs:double"/>
			</xs:sequence>
		</xs:complexType>
	</xs:element>
	<xs:element name="GetCelsiusResponse">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="fahrenheit" type="xs:double"/>
			</xs:sequence>
		</xs:complexType>
	</xs:element>
</xs:schema>
  </wsdl:types>
  <wsdl:message name="GetCelsiusResponse">
    <wsdl:part element="tns:GetCelsiusResponse" name="GetCelsiusResponse">
    </wsdl:part>
  </wsdl:message>
  <wsdl:message name="GetCelsiusRequest">
    <wsdl:part element="tns:GetCelsiusRequest" name="GetCelsiusRequest">
    </wsdl:part>
  </wsdl:message>
  <wsdl:portType name="TempPort">
    <wsdl:operation name="GetCelsius">
      <wsdl:input message="tns:GetCelsiusRequest" name="GetCelsiusRequest">
    </wsdl:input>
      <wsdl:output message="tns:GetCelsiusResponse" name="GetCelsiusResponse">
    </wsdl:output>
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="TempPortSoap11" type="tns:TempPort">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="GetCelsius">
      <soap:operation soapAction=""/>
      <wsdl:input name="GetCelsiusRequest">
        <soap:body use="literal"/>
      </wsdl:input>
      <wsdl:output name="GetCelsiusResponse">
        <soap:body use="literal"/>
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="TempPortService">
    <wsdl:port binding="tns:TempPortSoap11" name="TempPortSoap11">
      <soap:address location="https://localhost:8443/ws"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

Now build the project using gradle command – gradle clean build. Wait for downloading all the required files or jar APIs from the maven repository and finally you should see BUILD SUCCESSFUL message.

Next put the generated javaclient.jks (remember you generated this file during generating truststore) file under classpath directory src/main/resources. Put also the certificate.jks file (generated at the server side code) under classpath directory src/main/resources directory.

Now we need to create class for retrieving TrustManagerFactory and KeyManagerFactory and authenticate the client using certificate.

Create below class for retrieving TrustManagerFactory and KeyManagerFactory:

package com.jeejava.soap.config;

import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;

public class SoapClientConfig {

	public KeyManagerFactory getKeyManagerFactory() {
		InputStream inputStream = null;
		KeyStore ts = null;
		KeyManagerFactory keyManagerFactory = null;
		try {
			ts = KeyStore.getInstance("JKS");
			inputStream = this.getClass().getClassLoader().getResourceAsStream("javaclient.jks");
			ts.load(inputStream, "changeit".toCharArray());
			keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
			keyManagerFactory.init(ts, "changeit".toCharArray());
		} catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException
				| UnrecoverableKeyException e) {
			e.printStackTrace();
		} finally {
			try {
				inputStream.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return keyManagerFactory;
	}

	public TrustManagerFactory getTrustManagerFactory() {
		InputStream inputStream = null;
		KeyStore ts = null;
		TrustManagerFactory trustManagerFactory = null;
		try {
			ts = KeyStore.getInstance("JKS");
			inputStream = this.getClass().getClassLoader().getResourceAsStream("certificate.jks");
			ts.load(inputStream, "changeit".toCharArray());
			trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
			trustManagerFactory.init(ts);
		} catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException e) {
			e.printStackTrace();
		} finally {
			try {
				inputStream.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return trustManagerFactory;
	}
}

Now create below client class to authenticate the client and convert the celsius temperature into fahrenheit temperature.

package com.jeejava.soap.client;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;

import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;

import com.example.temperatureservice.GetCelsiusRequest;
import com.example.temperatureservice.GetCelsiusResponse;
import com.example.temperatureservice.TempPort;
import com.example.temperatureservice.TempPortService;
import com.jeejava.soap.config.SoapClientConfig;

public class SoapClient {

	public static void main(String[] args) {
		SoapClient soapClient = new SoapClient();
		TempPortService tempPortService = new TempPortService();
		TempPort tempPort = tempPortService.getTempPortSoap11();
		soapClient.authenticateClient(tempPort);
		GetCelsiusRequest celsiusRequest = new GetCelsiusRequest();
		celsiusRequest.setCelcius(45);
		GetCelsiusResponse response = tempPort.getCelsius(celsiusRequest);
		System.out.println("The fahrenheit temperature " + response.getFahrenheit());
	}

	private void authenticateClient(TempPort tempPort) {
		Client client = ClientProxy.getClient(tempPort);
		HTTPConduit httpConduit = (HTTPConduit) client.getConduit();
		SoapClientConfig soapClientConfig = new SoapClientConfig();
		KeyManagerFactory keyManagerFactory = soapClientConfig.getKeyManagerFactory();
		TrustManagerFactory trustManagerFactory = soapClientConfig.getTrustManagerFactory();
		TLSClientParameters tslClientParameters = httpConduit.getTlsClientParameters();
		if (tslClientParameters == null) {
			tslClientParameters = new TLSClientParameters();
		}
		tslClientParameters.setTrustManagers(trustManagerFactory.getTrustManagers());
		tslClientParameters.setKeyManagers(keyManagerFactory.getKeyManagers());
		tslClientParameters.setDisableCNCheck(true);
		httpConduit.setTlsClientParameters(tslClientParameters);
	}

}

We have used apache cxf client API to authenticate using client certificate. Notice how we have set TrustManager and KeyManager.

We have also disable the CN check from localhost and ideally we should disable it. If you don’t check that the certificate’s CN doesn’t match the domain name then they can simply create their own certificate (and have it signed by a trusted CA so it looks valid), use it in place of yours, and perform a man in the middle attack. Also, you need to be checking that the certificate comes from a trusted CA. If you skip either of these checks then you are at risk of a MITM (man-in-the-middle) attack.

When you run the above class you should see the below output in the console:

The fahrenheit temperature 113.0

Note: we have used here apache cxf client to authenticate the client. Therefore you need to add below two dependencies into the client project’s build script.

dependencies {
	compile("org.apache.cxf:cxf-rt-frontend-jaxws:2.7.7")
	compile("org.apache.cxf:cxf-rt-transports-http:2.7.7")
}

Congratulations! You have successfully completed the example on SOAP over https with client certificate authentication.

Thanks for reading.

Soumitra Roy Sarkar

I am a professional Web developer, Enterprise Application developer, Software Engineer and Blogger. Connect me on Roy Tutorials Twitter Facebook  Google Plus Linkedin Or Email Me

Leave a Reply

Your email address will not be published. Required fields are marked *