Access SOAP service with certificate and authentication

Today I had a task to consume a SOAP endpoint from an external source. The documentation says that base authentication is required but also a client certificate has to be used to establish the SSL connection. I was trying for several hours to accomplish both, but it seemed that it was not that simple.

I was using Visual Studio build the client for the WSDL files. Afterwards I was adding the certificate like in the code below:

var binding = new BasicHttpsBinding(); var endpointAddress = new EndpointAddress("https://..."); var client = new MyPortTypeClient(binding, endpointAddress); // Certificate client.ClientCredentials.ClientCertificate.Certificate = new X509Certificate2("C:\\certificate.pfx", "{certificate_password}"); // Basic authentication client.ClientCredentials.UserName.UserName = "{username}"; client.ClientCredentials.UserName.Password = "{password}"; var response = await client.TestAsync();

But unfortunately I always received the following error message:

System.ServiceModel.CommunicationException: An error occurred while sending the request. ---> 
System.Net.Http.HttpRequestException: An error occurred while sending the request. ---> 
System.IO.IOException: The decryption operation failed, see inner exception. ---> 
System.ComponentModel.Win32Exception: The specified data could not be decrypted.

After some research I found that I have to add the following important line:

binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;

This will make sure, that the certificate is actually used on the transport level and the message can be decrypted.

The problem with the decryption was fixed, but now the error message looked like this:

System.ServiceModel.Security.MessageSecurityException: The HTTP request is unauthorized with client authentication scheme 'Anonymous'. 
The authentication header received from the server was 'Basic realm="..."'.

After some further investigation I realized that this meant, that the Authorization header probably was not added properly. I also read about how the certificate and the authentication header cannot be combined by default, since they are different options on HttpClientCredentialType.

So I implemented a custom EndpointBehaviour, that will add the Authorization header for basic authentication to the request:

public class AddBasicAuthenticationClientMessageInspector : IClientMessageInspector { private readonly string username; private readonly string password; public AddBasicAuthenticationClientMessageInspector(string username, string password) { this.username = username; this.password = password; } public object BeforeSendRequest(ref Message request, IClientChannel channel) { var httpRequestProperty = new HttpRequestMessageProperty(); httpRequestProperty.Headers[HttpRequestHeader.Authorization] = $"Basic {Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}"))}"; request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestProperty); return null!; } public void AfterReceiveReply(ref Message reply, object correlationState) { } } public class AddBasicAuthenticationClientMessageInspectorEndpointBehavior : IEndpointBehavior { private readonly string username; private readonly string password; public AddBasicAuthenticationClientMessageInspectorEndpointBehavior(string username, string password) { this.username = username; this.password = password; } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.ClientMessageInspectors.Add(new AddBasicAuthenticationClientMessageInspector(username, password)); } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } }

An now I was finally able to access the SOAP endpoint:

var binding = new BasicHttpsBinding(); var endpointAddress = new EndpointAddress("https://..."); var client = new MyPortTypeClient(binding, endpointAddress); // Certificate client.ClientCredentials.ClientCertificate.Certificate = new X509Certificate2("C:\\certificate.pfx", "{certificate_password}"); // Basic authentication client.Endpoint.EndpointBehaviors.Add(new AddBasicAuthenticationClientMessageInspectorEndpointBehavior("{username}", "{password}")); var response = await client.TestAsync();

I hope with this article I can save someone else a lot of time, since it took my several hours to figure this out!