WCF and Intermediate Devices
Using an intermediate device with a WCF client/service application can pose a few issues, we will discuss some of them in this blog.
I have split up these into two categories: Issues related to Security and those not related to security.
Scenario 1: Issues related to security when an Intermediate device is involved.
WCF supports three types of security modes - Transport Security, Message Security and Mixed-Mode Security:
· Transport security mode uses a transport-level protocol, such as HTTPS, to achieve transfer security. Transport mode has the advantage of being widely adopted, available on many platforms, and less computationally complex. However, it has the disadvantage of securing messages only from point-to-point.
· Message security mode, on the other hand, uses WS-Security (and other specifications) to implement transfer security. Because the message security is applied directly to the SOAP messages and is contained inside the SOAP envelopes, together with the application data, it has the advantage of being transport protocol-independent, more extensible, and ensuring end-to-end security (versus point-to-point); it has the disadvantage of being several times slower than transport security mode because it has to deal with the XML nature of the SOAP messages.
· TransportWithMessageCredential/Mixed-Mode Security is a hybrid between Transport and Message Security. In this mode, message security is used to authenticate the client and transport security is used to authenticate the server and provide message confidentiality and integrity.
One of the most common intermediary device that affects a WCF security scenario is a load balancer which also offloads/terminates SSL.
When SSL offloading is enabled, all communication from the load balancer to the webservers is done in clear text(HTTP), even for HTTPS requests from clients to the load balancer. In other words client talks to the device over HTTPS and the device then talks to the service using HTTP.
A WCF client/service solution that relies on transport security (HTTPS) may fail to run with such a device because now the webserver doesn’t have SSL enabled and the SSL is terminated at the device.
Let’s look at a few scenarios based on the type of client authentication that is used.
1) The security scenario being used is Transport Security with Anonymous Client.
SOAP messages are encrypted and signed using HTTPS. Service is authenticated using an SSL Certificate. No client credentials are needed.
The following configuration can be used to work with the device in between.
Client: (Client configuration remains unchanged since client is unaware of the device).
<basicHttpBinding>
<binding name="TransportSecurityNoClientCreds">
<security mode="Transport"/>
</binding>
</basicHttpBinding>
<client>
<endpoint address="https://<DeviceAddress>/WcfService/Service.svc"
binding="basicHttpBinding" bindingConfiguration="TransportSecurityNoClientCreds"
contract="ServiceReference1.IService" name="BasicHttpBinding_IService" />
</client>
Service: (Here service is configured for NO security since the security is handled by the device)
<basicHttpBinding>
<binding name="NoSecurityNoClientCreds">
<security mode="None"/>
</binding>
</basicHttpBinding>
<service name="Service" behaviorConfiguration="ServiceBehavior">
<endpoint address="" binding="basicHttpBinding" bindingConfiguration="NoSecurityNoClientCreds" contract="IService"/>
</service>
NOTE: https://msdn.microsoft.com/en-us/library/ms730128.aspx talks about how to turn off Keep-Alive header by using a customBinding to replace basicHttpBinding if needed.
Also, if you use wsHttpBinding, then you may need to turn off security context establishment by setting EstablishSecurityContext=”false” on the WSHttpBinding. The other alternative to this would be to use sticky sessions on the load balancer so that the request sticks to one particular server on the farm.
2) The security scenario being used is Transport Security with client authentication.
SOAP messages are encrypted and signed using HTTPS. The intermediate device is authenticated using an SSL certificate. The client is authenticated using a username and password(basic authentication) at the transport layer. You can use windows credentials as well.
The following configuration can be used.
Client:
<basicHttpBinding>
<binding name="TransportSecurityWithBasicCred">
<security mode="Transport">
<transport clientCredentialType="Basic"/> //For windows client credentials,you will need to change this to “Windows”
</security>
</binding>
</basicHttpBinding>
<client>
<endpoint address="https://<DeviceAddress>/WcfService/Service.svc"
binding="basicHttpBinding" bindingConfiguration="TransportSecurityWithBasicCred"
contract="ServiceReference1.IService" name="BasicHttpBinding_IService" />
</client>
Service: (Here service is still listening on HTTP so we need to use an HTTP-based client authentication and the transfer security is being provided by the intermediate device).
<basicHttpBinding>
<binding name="TransportSecurityWithBasicCred">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Basic"/>
</security>
</binding>
</basicHttpBinding>
<service name="Service" behaviorConfiguration="ServiceBehavior">
<endpoint address="" binding="basicHttpBinding" bindingConfiguration="TransportSecurityWithBasicCred" contract="IService">
</service>
As you see above, on the service side we are using TransportCredentialOnly mode which doesn’t need transport security.
Note: With the TransportCredentialOnly mode, you cannot use Certificate-based client authentication:
Certificate-based client authentication is not supported in TransportCredentialOnly security mode. Select the Transport security mode.
3) There can also be a scenario where the intermediate device needs client credentials at the transport layer to let the client through(e.g. ISA) and the service needs client credentials in the soap message.
https://blogs.msdn.com/sajay/archive/2006/12/12/passing-a-username-as-a-supporting-token.aspx talks about a possible solution for this.
4) The security scenario being used is mixed mode security.
This mode provides integrity, confidentiality, and server authentication using HTTPS. The service must be configured with a certificate. Client authentication is provided by SOAP message security.
This mode is applicable when the user is authenticating with a UserName or Certificate credential and there is an existing HTTPS deployment for securing message transfer.
Using a standard binding with a security mode of TransportWithMessageCredentials, doesn’t work in this case because the service is not hosted over SSL. You may get the following error:
The 'BasicHttpBinding'.'https://tempuri.org/' binding for the 'IService'.'https://tempuri.org/' contract is configured with an authentication mode that requires transport level integrity and confidentiality. However the transport cannot provide integrity and confidentiality.
The reason for this is that WCF will check to see if the hosting environment actually supports SSL before it will allow that binding to be used. However, if the SSL is handled by a hardware intermediary (like an SSL Load Balancer/Off Loader) the hosting environment will not be supporting SSL.
Solution:
There is a hotfix(https://support.microsoft.com/kb/971831) available for .net framework 3.5 SP1 that adds an AllowInsecureTransport property in the SecurityBindingElement class that allows mixed-mode secured messages to be sent over an unsecured transport such as HTTP.
After installing this hotfix on the server, you can use a custom binding like the following:
<customBinding>
<binding name="AllowInsecureTransportBinding">
<textMessageEncoding/>
<security authenticationMode="SecureConversation" allowInsecureTransport="true"
requireSecurityContextCancellation="true">
<secureConversationBootstrap
authenticationMode="UserNameOverTransport"
requireDerivedKeys="false" />
</security>
<!--If you dont need to establish a secure converstaion, you can use the following
<security authenticationMode="UserNameOverTransport" allowInsecureTransport="true" />-->
<httpTransport/>
</binding>
</customBinding>
<services>
<service name="Service" behaviorConfiguration="ServiceBehavior">
<!-- Service Endpoints -->
<endpoint address="" binding="customBinding" bindingConfiguration="AllowInsecureTransportBinding" contract="IService"/>
</service>
</services>
There is however one aspect that doesn’t work using the approach above and that is the wsdl support. Browsing to the service with wsdl enabled throws the following exception.
NOTE: This binding works fine at runtime, clients can invoke it and it works fine, the only problem is with generating the wsdl for the service.
An ExceptionDetail, likely created by IncludeExceptionDetailInFaults=true, whose value is:
System.InvalidOperationException: An exception was thrown in a call to a policy export extension.
Extension: System.ServiceModel.Channels.TransportSecurityBindingElement
Error: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider. Policy export for such a binding is not supported. Make sure the transport binding element in the binding implements the ITransportTokenAssertionProvider interface. ----> System.InvalidOperationException: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider. Policy export for such a binding is not supported. Make sure the transport binding element in the binding implements the ITransportTokenAssertionProvider interface.
at System.ServiceModel.Channels.TransportSecurityBindingElement.System.ServiceModel.Description.IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext policyContext)
at System.ServiceModel.Description.MetadataExporter.ExportPolicy(ServiceEndpoint endpoint)
--- End of inner ExceptionDetail stack trace ---
at System.ServiceModel.Description.MetadataExporter.ExportPolicy(ServiceEndpoint endpoint)
at System.ServiceModel.Description.WsdlExporter.ExportEndpoint(ServiceEndpoint endpoint, XmlQualifiedName wsdlServiceQName)
at System.ServiceModel.Description.WsdlExporter.ExportEndpoints(IEnumerable`1 endpoints, XmlQualifiedName wsdlServiceQName)
at System.ServiceModel.Description.ServiceMetadataBehavior.MetadataExtensionInitializer.GenerateMetadata()
at System.ServiceModel.Description.ServiceMetadataExtension.EnsureInitialized()
at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.InitializationData.InitializeFrom(ServiceMetadataExtension extension)
at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.GetInitData()
at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.TryHandleDocumentationRequest(Message httpGetRequest, String[] queries, Message& replyMessage)
at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.ProcessHttpRequest(Message httpGetRequest)
at System.ServiceModel.Description.ServiceMetadataExtension.HttpGetImpl.Get(Message message)
at SyncInvokeGet(Object , Object[] , Object[] )
at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
This is because the security binding element requires the transport element to implement the ITransportTokenAssertionProvider interface. HTTPS binding element implements this but not the HTTP element.
Currently there is not a good solution for this. There are a couple of workarounds that can be used.
1) Make wsdl available off-band. Browse to the original wsdl hosted on an SSL enabled site, save this wsdl file and provide it to your clients off-band.
2) Another option is to create a custom transport element that implements ITransportTokenAssertionProvider and IPolicyExportExtension.
Here is a sample code to do this.
Step 1: Create a Class Library Project with the following two classes in it.
HttpTransportBindingElementWithWSDL.cs
using System;
using System.Collections.Generic;
using System.Web;
using System.ServiceModel.Channels;
using System.Xml;
using System.ServiceModel.Description;
/// <summary>
/// Summary description for HttpTransportBindingElementWithWSDL
/// </summary>
public class HttpTransportBindingElementWithWSDL : HttpTransportBindingElement, ITransportTokenAssertionProvider, IPolicyExportExtension
{
public HttpTransportBindingElementWithWSDL() : base() { }
public override BindingElement Clone()
{
HttpTransportBindingElementWithWSDL retval = new HttpTransportBindingElementWithWSDL();
return retval;
}
#region ITransportTokenAssertionProvider Members
public XmlElement GetTransportTokenAssertion()
{
string secpolNS0 = "https://schemas.xmlsoap.org/ws/2005/07/securitypolicy";
XmlDocument xmldoc = new XmlDocument();
XmlElement TransTokAssert = xmldoc.CreateElement("sp", "HttpToken", secpolNS0);
return TransTokAssert;
}
#endregion
#region IPolicyExportExtension Members
void IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext context)
{
HttpsTransportBindingElement httpsTBE = new HttpsTransportBindingElement();
((IPolicyExportExtension)httpsTBE).ExportPolicy(exporter, context);
}
#endregion
}
HttpTransportElementWithWSDL.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security;
using System.ServiceModel.Configuration;
using System.Configuration;
/// <summary>
/// Summary description for HttpTransportElementWithWSDL
/// </summary>
public class HttpTransportElementWithWSDL : HttpTransportElement
{
public override Type BindingElementType
{
get
{
return typeof(HttpTransportBindingElementWithWSDL);
}
}
protected override TransportBindingElement CreateDefaultBindingElement()
{
HttpTransportBindingElementWithWSDL retval =
new HttpTransportBindingElementWithWSDL();
return retval;
}
protected override BindingElement CreateBindingElement()
{
return new HttpTransportBindingElementWithWSDL();
}
}
Step 2: Compile the above class library project. I named it as HttpTransportWithWSDL so its compiled to a dll called HttpTransportWithWSDL.dll
Step 3: Add a reference of this project to your WCF service.
Step 4: Add a bindingElementExtension in the web.config within the <system.serviceModel> section
<extensions>
<bindingElementExtensions>
<add name="HttpTransportElementWithWSDL" type="HttpTransportElementWithWSDL, HttpTransportWithWSDL"/>
</bindingElementExtensions>
</extensions>
Step 5: Replace the <httpTransport/> binding element in the previous customBinding with the new <HttpTransportElementWithWSDL/> binding element we created, so the updated customBinding looks like the following:
<customBinding>
<binding name="AllowInsecureTransportBinding">
<textMessageEncoding/>
<security authenticationMode="SecureConversation" allowInsecureTransport="true"
requireSecurityContextCancellation="true">
<secureConversationBootstrap
authenticationMode="UserNameOverTransport"
requireDerivedKeys="false" />
</security>
<!--If you dont need to establish a secure converstaion, you can use the following
<security authenticationMode="UserNameOverTransport" allowInsecureTransport="true" />-->
<HttpTransportElementWithWSDL/>
</binding>
</customBinding>
You should now be able to browse to the service and see the wsdl as well.
One problem with this approach is you will notice that the address shown in the service page is http instead of https.
To test this service, you will need to create a client and use it to call the service. You can do this using the svcutil.exe tool from the command line with the following syntax:
svcutil.exe https://<DeviceAddress>/WcfService/Service.svc?wsdl
Clients will need to replace this to https to create a proxy from this wsdl.
Scenario #2: Issues NOT related to security when an Intermediate device is involved.
1) When an intermediary device is used, the clients would be accessing the service using the URL of the device and not of the webserver. But the generated wsdl for the service might still have the name of the physical web server node serving the WCF service instead of the load balancer name.
There is a hotfix available to address this. https://support.microsoft.com/kb/971842
To enable the hotfix after you installed it, you have to configure the WCF service to use the following service behavior:
<serviceBehaviors>
<behavior name="<name>">
<useRequestHeadersForMetadataAddress>
<defaultPorts>
<add scheme="http" port="81" />
<add scheme="https" port="444" />
</defaultPorts>
</useRequestHeadersForMetadataAddress>
</behavior>
</serviceBehaviors>
Note <name> is a placeholder that you should replace with the behavior name in your WCF service.
The hotfix causes WCF to generate the correct URI by using the "Host" HTTP header of the incoming metadata request. In this case, the "Host" header contains the load balancer address instead of the internal node address.
If a URI inside the WSDL document has a different scheme than the scheme of the "Host" header URI, for example, if a request for metadata comes over HTTPS but the metadata contains HTTP URIs, the hotfix will need the port number for that different scheme. The port number can be specified per scheme in the <defaultPorts> section.
2) You may run into the following address filter exception if the To address of the message does not match the address the service is listening on, because the To address now contains the device’s address.
Unhandled Exception: System.ServiceModel.EndpointNotFoundException: The message with To 'https://<DeviceAddress>/Service.svc' cannot be processed at the receiver, due to an AddressFilter mismatch at the EndpointDispatcher. Check that the sender and receiver's EndpointAddresses agree.
You can work around this by setting the addressfilter mode to Any for your service.
[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)]
public class Service : IService
More details : https://msdn.microsoft.com/en-us/library/system.servicemodel.addressfiltermode.aspx
3) Connection related errors when the device tears down the connection.
Sometimes the intermediate device may terminate a connection either due to the connection being idle or for some unexpected reasons.
Consider the scenario where you have a long running service method call and you have increased the timeouts appropriately on both client and server side. This works fine but when you add an intermediate device that has a lower timeout value, you will get a timeout from this device thereby causing a socket exception similar to the following:
Client:
<ExceptionString>System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
at System.Net.Sockets.Socket.Receive(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags)
at System.ServiceModel.Channels.SocketConnection.ReadCore(Byte[] buffer, Int32 offset, Int32 size, TimeSpan timeout, Boolean closing)</ExceptionString>
<NativeErrorCode>2746</NativeErrorCode>
Server:
<ExceptionString>System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
at System.Net.Sockets.Socket.Send(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags)
at System.ServiceModel.Channels.SocketConnection.Write(Byte[] buffer, Int32 offset, Int32 size, Boolean immediate, TimeSpan timeout)</ExceptionString>
<NativeErrorCode>2746</NativeErrorCode>
One option available(and this may not work for all devices) is to do the following from your client application to keep sending a keepAlive request
System.Net.ServicePointManager.SetTcpKeepAlive(true, 3000, 3000);
4) Routing messages through an intermediate device.
In some scenarios, the intermediate device may be acting as a router, e.g. TcpTrace for , some other router to handle common tasks like security, logging etc.
https://msdn.microsoft.com/en-us/magazine/cc163412.aspx#S5 talks about this in detail.
You can configure a listenUri for the service endpoint to get this working:
<endpoint address=”https://<Device or Router Address>/WcfService/Service.svc”
listenUri=”https://<PhysicalServerName>/WcfService/Service.svc”
binding=”wsHttpBinding”
contract=”ISimpleMath”/>
address: This is the logical address that SOAP messages target and the address that the WCF dispatcher uses to map the message to an endpoint and ultimately to a method call.
This is also the address that shows up in the WSDL, so clients will send messages to this address.
ListenUri: This is the physical or actual transport-specific network address that WCF listens for messages to arrive. The device would be configured to forward the messages to this address.
5) Device modifying the conent of the SOAP envelope.
In a particular scenario a SSL-Offloading device was also modifying the content of the soap envelope. In this case all "https://" text was being replaced with "https://". So the XML namespace is changed from xmlns:s="https:// to xmlns:s="https://.
WCF does a string compare on the namespace to check the SOAP version(SOAP1.1 is identified by the https://schemas.xmlsoap.org/soap/envelope/ namespace while SOAP 1.2 is identified by the https://www.w3.org/2002/12/soap-envelope
namespace ), because of the extra “s”, WCF couldn’t determine the version of the message and hence throws System.ServiceModel.CommunicationException:Unrecognized message version.
Comments
Anonymous
May 03, 2011
thanks this post helped me solve the problems i was facingAnonymous
July 17, 2014
Excellent post. Thanks for sharing this useful information.