Wednesday, October 8, 2008

WCF Service: support transaction and IIS in an intranet environment

Currently I am working on a web project whose data access layer is separated as a standalone WCF service. Both the web server and WCF service are in the same intranet environment and we use Windows integrated authentication to verify users in a domain, so the security issue is not that critical. An additional requirement is that WCF service needs to know the client credential which logs in the web application and store that information into database for later audit. So the web server should pass over the client credential to the WCF service.

I use IIS to host my WCF service and web application. The WCF service and web application could be on different computers. In IIS I set my web application to use Windows integrated authentication, so users have to provide their domain user name and password to access the web application.

basicHttpBinding with “TransportOnly” security setting

In this situation, I simply use basicHttpBinding with the security setting “TransportOnly” and it works well. Here is the relative server configuration code:
<basicHttpBinding>
<binding name="basicBinding">
<security mode = "TransportCredentialOnly">
<transport clientCredentialType="Windows"/>
</security>
</binding>
</basicHttpBinding>

I use the following code to get the user name in the WCF service:
string user = System.ServiceModel.OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name;

The client configuration code is the following:
<basicHttpBinding>
<binding name="BasicHttpBinding_IMyService" >
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" realm="" />
</security>
</binding>
</basicHttpBinding>

The code works well getting the user name in a Windows domain from a client.

Drawbacks of basicHttpBinding

Despite the obvious disadvantage in security, we still can make do with basicHttpBinding since all our applications are in an internal Windows domain. Another fatal weakness, however, forced me to transfer from basicHttpBinding to wsHttpBinding. basicHttpBinding doesn’t support Transaction! I have a business mode that I need to call different WCF functions several times and all the calls should be within one transaction.

In .NET framework, we usually use TranscationScope to automatically manage transactions, especially in a distributed environment. WCF supports transaction, and provides many relative classes and attributes. However, all of them are not working with basicHttpBinding.

wsHttpBinding with transaction support

wsHttpBinding can have different security setting. There are many good blogs we can find in the internet. If we use Transport security with wsHttpBinding, we actually use SSL/TLS over HTTP, that is, HTTPS. We cannot use HTTP prefix in this situation. Transport is good for point-to-point scenarios, but in end-to-end scenarios where we have intermediaries existing, we should go for Message security.

The following is what I changed to my basicHttpBinding source code.

1. Install a certificate in WCF for transport security to support HTTPS. Transport security needs SSL, so we have to install a server certificate first.

2. In server side, the configuration is like this:
<wsHttpBinding>
<binding name="wsBinding" transactionFlow="true" >
<security mode = "Transport">
<transport clientCredentialType="Windows"/>
</security>
</binding>
</wsHttpBinding>

<serviceBehaviors>
<behavior name="DBService.Service1Behavior">
<serviceMetadata httpGetEnabled="false" httpsGetEnabled="true"/>
<serviceCredentials>
<serviceCertificate storeName="My" findValue="CN=localhost"/>
</serviceCredentials>
</behavior>
</serviceBehaviors>

3. In WCF service source code, add attributes in several places:

In the service interface:
[ServiceContract()]
public interface IMyService
{
[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
bool UpdateData(AInfo info);
}

In the implementation class:
[ServiceBehavior(TransactionAutoCompleteOnSessionClose = true,
ReleaseServiceInstanceOnTransactionComplete = true,
TransactionIsolationLevel = System.Transactions.IsolationLevel.Serializable,
TransactionTimeout = "01:00:00",
InstanceContextMode = InstanceContextMode.PerSession)]
public class MyService : IMyService
{
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)]
public int UpdateData(AInfo info)
{
return 1;
}
}

4. The client configuration code is like this:
<wsHttpBinding>
<binding name="WSHttpBinding_IAssayService" closeTimeout="00:01:00"
openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00"
bypassProxyOnLocal="false" transactionFlow="true" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="1000000"
messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
allowCookies="false">
<security mode="Transport">
<transport clientCredentialType="Windows" realm="" />
<message clientCredentialType="UserName" negotiateServiceCredential="false"
algorithmSuite="Default" />
</security>
</binding>
</wsHttpBinding>

5. The client source code looks like this:
TransactionOptions options = new TransactionOptions();
options.Timeout = new TimeSpan(1, 0, 0);
MyServiceClient client = new MyServiceClient();
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, options))
{
client. UpdateData (source1);
client. UpdateData (source2);
client. UpdateData (source3);
}

Now I can call my service function any times. All the operations will be rolled back if any of them fails.