I asked a question in the forums a week or so ago about how one could go about getting username credentials to be passed through HTTP. The problem is that, out of the box, WCF will not allow you to specify message credentials without using a secure transport. This is probably a really smart move for Microsoft because it will prevent people from exposing their credentials. While it certainly causes headaches with development since you need to configure certificates on all your dev boxes, that’s probably still not a good enough reason for Microsoft to allow message level credentials go over HTTP (you just know the average developer will never switch to HTTPS when they deploy and will end up leaking customer credentials all over the internet). However, there is a very real world scenario where one might need clear/text credentials which is in the case where SSL is handled by a load balancing machine such as those provided by F5 networks.
First, let me explain what this SSL passthrough is all about. Basically the load balancer, in addition to it’s normal duties, offloads the SSL work from the underlying server farm. There are two benefits to this:
- It has hardware acceleration for SSL, so you don’t burn CPU cycles on your web servers.
- You can configure the SSL certificate in one place only.
What ends up happening is that an incoming SSL request gets decrypted and routed as a plain HTTP request to one of the underlying servers. Since the server might need to know whether or not the original request was indeed SSL, you can also optionally inject a new HTTP header to indicate that it was. Responses from the server are also plain old HTTP back through the load balancer where it is SSL encrypted before being sent back to the user. Simple, right?
Ok, so… now that we know why we might need to achieve this in WCF, let’s talk about how it can be done. While the solution is included in the post I linked to earlier, I wanted to blog about it here in more detail. All the credit for the solution has to go to Pedro Felix who saw my post in the forums, figured out a way to make it work and contacted me offline with some sample code. On to the solution…
We can’t use HttpsTransportBindingElement because it forces the SSL handling to be done by the service itself. At the same time we can’t use a security mode of TransportWithMessageCredential over the plain old HttpTransportBindingElement. So what do we do? We create our own HTTP transport which does allow credentials to be passed. Fear not, this is actually really easy:
- Create a new class called HttpsViaProxyTransportBindingElement
- Subclass HttpBindingTransportElement
- Override Clone to return a new instance of HttpsViaProxyTransportBindingElement
- Override GetProperty<T> where you will return base.GetProperty<T> for all requests except for that of ISecurityCapabilities where you will return a custom implementation described in the next step.
- Create a new class called SslViaProxySecurityCapabilities and implement the ISecurityCapabilities interface with the following rules:
- SupportRequestProtectionLevel – EncryptAndSign
- SupportResponeProtectionLevel – EncryptAndSign
- SupportsClientAuthentication – false
- SupportsClientWindowsIdentity – false
- SupportsServerAuthentication – true
Now you have yourself a custom transport which is capable of speaking plain old HTTP thanks to the base implementation provided by WCF, but by overriding GetProperty<T> and returning custom ISecurityCapabilities when the security settings of the transport are checked for the SecurityMode.TransportWithMessageCredential requirements it will allow this mode of operation.
It’s at this point that I want to take this opportunity to warn everyone that this could lead to a major security risk if you were to use this approach and didn’t have SSL provided by the front end. You’re basically LYING about your capabilities (i.e. that you provide encryption) to allow for the credentials to be passed in clear-text. DO NOT ABUSE THIS APPROACH JUST TO WORK AROUND HAVING TO USE SSL FOR USERNAME CREDENTIALS.
Ok, so now that we have our custom transport implementation, we need to actually configure a binding to use it. This requires that we use a CustomBinding because the built in bindings revolve around the WCF native transports. While you could pretty easily put the CustomBinding together programatically, that’s no fun. We want to do it with a config file, so we need to write a custom configuration element. Specifically, in WCF terms, we need to write a BindingElementExtensionElement. Just like we did with the custom transport implementation we can simply inherit the majority of the behavior from the existing HttpTransportElement class. Let’s look at how we’d do that:
- Create a new class called HttpsViaProxyTransportElement
- Subclass HttpTransportElement
- Override BindingElementType to return our HttpsViaProxyTransportBindingElement
- Override CreateDefaultBindingElement to return a new instance of HttpsViaProxyTransportBindingElement
We now have a custom binding element extension element which can use to configure a custom binding. Now let’s see what a configuration file might look like that uses this:
<!-- first we need to register our binding element extension -->
<add name="httpsViaProxyTransport" type="YourNamespace.HttpsViaProxyTransportElement, YourAssembly"/>
<!-- now we need to create a custom binding that uses our transport -->
<!-- require user name to be passed over the transport -->
<security authenticationMode="UserNameOverTransport" />
<!-- use our custom transport which has no proprietary settings, but
exposes all thesame settings as the plain old http transport -->
<httpsViaProxyTransport authenticationScheme="Anonymous" />
<!-- here we would use the testBinding in our service endpoint -->
That’s all there is to it. Now when the message comes into the front-end as SSL it is decrypted and sent as a plain HTTP request to on of the servers where the web service is running. With the changes we’ve made we are now allowed to received the user name credentials in clear text and process them at the service. When the service responds, the message is re-encrypted by the front-end and sent back to the client. The only limitation, which is discussed in the post, is that there no support for auto-WSDL generation. We would need to provide a custom implementation of ITransportTokenAssertionProvider and IPolicyExportExtension. For now I leave up to the reader though it may be the subject of a future post.