Monday, May 24, 2010

How to programmatically configure SSL for WCF Services

One of the issues with WCF is that is heavily depends on declarative configuration. In case of the SSL protocol, a basic ASP.NET WebService supports it “out-of-the-box” if the web server supports it. But a WCF service configured for noSSL calls will not accept SSL calls without an extra configuration. This article describes the issue and the ability to configure the service programmatically to lower the risk of misconfiguration.

Static configuration of WCF endpoints

Let’s start from a basic configuration of a service configured for noSSL calls:

<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="basicHttpBinding1">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="SilverlightApplication13.Web.Service2Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="SilverlightApplication13.Web.Service2Behavior"
name="SilverlightApplication13.Web.Service2">
<endpoint address="" binding="basicHttpBinding" bindingConfiguration="basicHttpBinding1" contract="SilverlightApplication13.Web.Service2">
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>

As you can see I have a single endpoint with security mode set to None which corresponds to noSSL mode. With such static configuration the SSL is not a problem. You just define an extra endpoint with proper settings (mode set to Transport):
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="basicHttpBinding1">
<security mode="None" />
</binding>
<binding name="basicHttpBinding2">
<security mode="Transport" />
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="SilverlightApplication13.Web.Service2Behavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="SilverlightApplication13.Web.Service2Behavior"
name="SilverlightApplication13.Web.Service2">
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration="basicHttpBinding1"
contract="SilverlightApplication13.Web.Service2">
</endpoint>
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration="basicHttpBinding2"
contract="SilverlightApplication13.Web.Service2">
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>

However, there are two issues here.

First, note that both endpoints has empty value of address attribute. This won’t work. The second endpoint’s address has to be manually configured. For example:

<services>
<service behaviorConfiguration="SilverlightApplication13.Web.Service2Behavior"
name="SilverlightApplication13.Web.Service2">
<endpoint address="" binding="basicHttpBinding"
bindingConfiguration="basicHttpBinding1"
contract="SilverlightApplication13.Web.Service2">
</endpoint>
<endpoint address="https://myhost.com/service1.svc" binding="basicHttpBinding"
bindingConfiguration="basicHttpBinding2"
contract="SilverlightApplication13.Web.Service2">
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>

It means that everytime you expose your service under different address, you have to reconfigure it in web.config.

But having two endpoints with different bindings stops your service from working with the Visual Studio Integrated Web Server since it does not support the SSL protocol (you’ll get the exception from the integrated webserver saying that SSL is not supported).

The simplest solution for both issues is to have a two sets of configuration files. One used with the development environment would have a single endpoint for noSSL calls. The other one used in a production environment. However, having few or few dozens of services means that there’s a risk of the misconfiguration, revealed only in the runtime.

Creating endpoints dynamically

The other option however would be to create endpoints dynamically depending on the environment. This way no static configuration is required and the service would expose a single noSSL endpoint when working with the Integrated WebServer and two endpoints (SSL/noSSL) when deployed onto the IIS.

I have found few articles on the dynamic configuration of self-hosted WCF services, however it has been much harder to find a way to do it with the IIS.

To have your service configured dynamically, you start with the *.svc file to change the way the IIS initializes your service:

<%@ ServiceHost Language="C#" Debug="true" 
Service="SilverlightApplication13.Web.Service2" CodeBehind="Service2.svc.cs" %>

and you change this to point to a factory class responsible for creating instances of your service:

<%@ ServiceHost Language="C#" Debug="true" 
Factory="SilverlightApplication13.Web.Service1Factory" CodeBehind="Service1.svc.cs" %>

and you provide the implementation of the factory:

public class Service1Factory : ServiceHostFactory
{
public override ServiceHostBase CreateServiceHost( string constructorString, Uri[] baseAddresses )
{
ServiceHost serviceHost = new ServiceHost( typeof( Service1 ), baseAddresses );

// create the noSSL endpoint
serviceHost.AddServiceEndpoint( typeof( Service1 ), new BasicHttpBinding(), "" );

// create SSL endpoint
if ( ExposeAsSSL )
serviceHost.AddServiceEndpoint(
typeof( Service1 ),
new BasicHttpBinding( BasicHttpSecurityMode.Transport ) ,
baseAddresses[0].ToString().Replace( "http", "https" ) );

// Enable HttpGet for the service
ServiceMetadataBehavior metadata = new ServiceMetadataBehavior();
metadata.HttpGetEnabled = true;
serviceHost.Description.Behaviors.Add( metadata );

return serviceHost;
}
}

[ServiceContract( Namespace="http://www.test.com/test" )]
[ServiceBehavior(
InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple )] //please refer to the docs for more info on these values
public class Service1
{
[OperationContract]
public string DoWork( string Param )
{
return Param;
}
}
Note that there’s just a single ExposeAsSSL variable which has to be configured somehow but the risk of misconfiguration is much lower.

4 comments:

Anonymous said...

I bet this would explain why my service reference is valid and works over http but fails with an object undefined error over https. I've been fighting this for a few hours...thanks for the post!

rjha94 said...

How do you set this ExposeAsSSL variable? Would it be possible to set this variable one time depending on whether or not IIS is configured for SSL traffic?

Wiktor Zychla said...

I tend to put it in appSettings so that it can be easily configured by administrators who deploy our applications. They KNOW whether or not SSL is configured for the website.

Jeff said...

Wiktor...excellent post...I've been looking for ways to programmatically configure WCF bindings without using a Web.Config file, and your example of using the ServiceHostfactory class was a great help. Thanks!