Silverlight and ASP.NET compatible WCF over SSL using custom username/password authentication

WCF, as you probably know, can be a hugely flexible, but equally complicated beast. The domain knowledge stretches from the .NET stack to identity management, encryption and transport. “Out of the box”, it’s pretty straight forward, but the demos and examples often presented by Microsoft are often far from what is needed in real life. Bloggers and authors present excellent examples, but these often fail at the last hurdle where Silverlight compatibility or specific authentication is required. Hopefully, this solution will be a “one-stop shop”.

We needed to be able to deploy software that was loosely coupled via web services, with multiple client platforms. Therefore, we needed:

  • Secure communications, even though most our work is intranet based, securing the transport of the data was a must so we can scale up onto the internet as required.
  • Low maintenance, I approach everything pragmatically. If it is over-complicated for us to produce, it will be over-complicated for the user to maintain. Simplicity is king.
  • Ability to work over internet as well as internet, according to application and scalability.
  • ASP.NET and Silverlight compatibility, the latter already being stymied due to lack of support of the altogether simpler wsHttpBinding. (Most of the SSL examples being internet-based using wsHttpBinding)
  • Allow transmission of credentials in each transaction and have those credentials (which are not Windows-based) verified each time.

What seemed like an impossible combination has at last been been achieved (by myself, anyway). After trawling through countless blogs, questions and white-papers, I’m going to share with you how to create a reliable, secure WCF service that may be redeployed across projects, even with the limited capabilities of Silverlight.

Generation of SSL Certificate

If we’re transmitting sensitive data, it should be secure. Sensitive data includes the username and password and WCF reinforces this by requiring you to use security over transport if using username/password authentication.

To create or assign the certificate, you need to use the IIS Server Certificates administration module within the Server tree:

20-01-2011 18-39-07-small

You can then choose to import an existing certificate, or create a self-signed certificate. If you create a self-signed certificate, it is only of use during the development phase and will introduce problems for you (which we can overcome).

20-01-2011 18-39-37-small

I created a self-signed certificate to show you the difficulties and how to get round them.

Creation of the Service

The service code is straight forward. I like to separate the interface from the implementation.

IClientService
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
namespace Services.Api.Wcf
{
     [ServiceContract]
     
public interface IClientService
     {
           [OperationContract]
           List
<int> GetAllClientIDs();
     }
}


The implementation class includes the requirement for ASP.NET Compatibility mode.

ClientService.svc
<%@ ServiceHost Language="C#" Debug="true" Service="Services.Api.Wcf.ClientService" CodeBehind="ClientService.svc.cs" %>


ClientService.svc.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.ServiceModel.Activation;
using System.Web;

namespace Services.Api.Wcf
{
    [AspNetCompatibilityRequirements(RequirementsMode
= AspNetCompatibilityRequirementsMode.Allowed)]
   
public class ClientService : ServiceBase, IClientService
    {
        
#region ~ from IClientService ~
        
public List<int> GetAllClientIDs()
         {
                ClientRepository clientRepository
= ClientRepositoryFactory.CreateForUser(UserName);
               
return new List<int>(clientRepository.Clients.Select(q => q.IndexNo));
          }
         
#endregion 
     }
}


Web.config

The most challenging part of WCF is getting the configuration right.

<system.serviceModel>
    
<behaviors>
        
<serviceBehaviors>
             
<behavior name="sslBehaviour">
                 
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
                 
<serviceDebug includeExceptionDetailInFaults="true" />
                 
<serviceCredentials>
                        
<userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="Core.WcfUserNamePasswordValidator, Core"/>    </serviceCredentials>
            
</behavior>
        
</serviceBehaviors>
   
 </behaviors>
    
<bindings>
        
<customBinding>
              
<binding name="sslCustomBinding">
                   
<security authenticationMode="UserNameOverTransport"/>
                  
 <binaryMessageEncoding />
                  
 <httpsTransport />               </binding>
         
</customBinding>
     
</bindings>
   
 <services>
        
<service name="Services.Api.Wcf.ClientService" behaviorConfiguration="sslBehaviour">
             
<host>
                   
<baseAddresses>
                            
<add baseAddress="https://mymachine.domain.local/"/>
                  
 </baseAddresses>
            
</host>
           
 <endpoint address="" binding="customBinding" bindingConfiguration="sslCustomBinding" contract="Services.Api.Wcf.IClientService"> </endpoint>
           
 <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
         
</service>
     
</services>
      
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="true" />
</system.serviceModel>



The first element, behavior, defines the SSL configuration of the services that may be attached to it. The important point is the serviceCredentials/userNameAuthentication, which defines the class that will perform the authentication.

Silverlight can only support basicHttpbinding, so it is necessary to define a customBinding. This is defined in the customBinding/binding element, and is set to use the UserNameOverTransport authenticationMode (which requires secure transport) and defines the transport as secure by using httpsTransport.

The service itself is defined in the service element, which ties together the bindingConfiguration and behaviorConfiguration. Additionally, the baseAddresses element defines the address that the service will respond to. Note that this is HTTPS. This will obviously need to be changed when deployed. We use the Web.config transformation feature to achieve this.

The authentication class is shown below and may be as simple or as advanced as you need. Obviously, this is where you verify the authentication against your custom provider.

WcfUserNamePasswordValidator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IdentityModel.Selectors;

namespace Core
{
       
public class WcfUserNamePasswordValidator : UserNamePasswordValidator
        {
              
public override void Validate(string userName, string password)
              {
                   
if (userName!="username" && password!="password") throw new FaultException("Invalid username/password");
              }
          }
}


This service may be deployed on the web server and tested using the HTTPS address. As we created our own self-signed certificate, there will be a Certificate Warning, which we’ll ignore for now.

20-01-2011 20-29-01-small

Creation of the Client

First step when working with the client-side code is to point to the service web-site. Clicking on Add Service Reference and enter the HTTPS address. Again, if you are using a self-signed certificate, you will be warned of this.

20-01-2011 20-38-57-small

Click “Yes”, then “OK” to generate the proxy code. Once generated, you’re ready to use it. This adds a configuration such as the section below:

<system.serviceModel>
      
<bindings>
           
<basicHttpBinding>
                
 <customBinding>
                        
<binding name="CustomBinding_IClientService">
                               
<security defaultAlgorithmSuite="Default" authenticationMode="UserNameOverTransport" requireDerivedKeys="true" securityHeaderLayout="Strict" includeTimestamp="true" keyEntropyMode="CombinedEntropy" messageSecurityVersion="WSSecurity11WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
                                       
<localClientSettings cacheCookies="true" detectReplays="false" replayCacheSize="900000" maxClockSkew="00:05:00" maxCookieCachingTime="Infinite" replayWindow="00:05:00" sessionKeyRenewalInterval="10:00:00" sessionKeyRolloverInterval="00:05:00" reconnectTransportOnFailure="true" timestampValidityDuration="00:05:00" cookieRenewalThresholdPercentage="60" />
                                       
<localServiceSettings detectReplays="false" issuedCookieLifetime="10:00:00" maxStatefulNegotiations="128" replayCacheSize="900000" maxClockSkew="00:05:00" negotiationTimeout="00:01:00" replayWindow="00:05:00" inactivityTimeout="00:02:00" sessionKeyRenewalInterval="15:00:00" sessionKeyRolloverInterval="00:05:00" reconnectTransportOnFailure="true" maxPendingSessions="128" maxCachedCookies="1000" timestampValidityDuration="00:05:00" />  
                                        <
secureConversationBootstrap />
                               
 </security>
                                
<binaryMessageEncoding maxReadPoolSize="64" maxWritePoolSize="16" maxSessionSize="2048">
                                       
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                                 </binaryMessageEncoding>
                                
<httpsTransport manualAddressing="false" maxBufferPoolSize="524288" maxReceivedMessageSize="65536" allowCookies="false" authenticationScheme="Anonymous" bypassProxyOnLocal="false" decompressionEnabled="true" hostNameComparisonMode="StrongWildcard" keepAliveEnabled="true" maxBufferSize="65536" proxyAuthenticationScheme="Anonymous" realm="" transferMode="Buffered" unsafeConnectionNtlmAuthentication="false" useDefaultWebProxy="true" requireClientCertificate="false" />
                          
</binding>
                    
</customBinding>
             
 </bindings>
             
<client> 
                      
<endpoint address="https://api1test.mydomain.local/api/wcf/ClientService.svc" binding="customBinding" bindingConfiguration="CustomBinding_IClientService" contract="ClientEndpoints.Clients.IClientService" name="CustomBinding_IClientService" />
             
</client> 
         
</system.serviceModel>
</configuration>


Before we do, I would recommend you use this segment of code originally developed by Damien McGivern, which will aid in the catching of exceptions. As Damien mentions on his site, the WCF stack doesn’t “break” where you’d expect, making diagnosis difficult.

The ServiceHelper class acts as a Factory for generation of proxy objects.

ServiceHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace AspNet
{
      
public static class ServiceHelper
       {
            
/// <summary>             /// WCF proxys do not clean up properly if they throw an exception. This method ensures that the service proxy is handeled correctly.
           
/// Do not call TService.Close() or TService.Abort() within the action lambda. 
            
/// </summary>
           
 /// <typeparam name="TService">The type of the service to use</typeparam>
           
 /// <param name="action">Lambda of the action to performwith the service</param>
           
public static void Using<TService>(Action<TService> action) where TService : ICommunicationObject, IDisposable, new()
            {
                   var service
= new TService();
                  
bool success = false;
                 
try 
                  {
                      action(service);
                      
if (service.State != CommunicationState.Faulted) 
                      {
                          service.Close();
                          success
= true;
                      }
                  }
                  
finally
                  {
                       
if (!success)
                       {
                            service.Abort();
                       } 
                  }
            }
       }
}


So you can create the reference to the client proxy and use the service using code such as:

Default.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
using System.Net;
using System.Net.Security;
using System.Web.Security;

namespace Web.Client
{
      
public partial class _Default : System.Web.UI.Page
      {
           
protected void Page_Load(object sender, EventArgs e)
           {
                  
// this delegate may be used to ignore SSL/TLS errors
                  
// it disables SSL certificate path verification for the AppDomain
                 
// Make sure you remove it before production!
                 
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback( delegate { return true; });
                  ServiceHelper.Using
<ClientServiceClient>(client =>
                        {
                                client.ClientCredentials.UserName.UserName
= "username";
                                client.ClientCredentials.UserName.Password
= "password";
                                
int[] clientIDs=client.GetClientIDs();
                         });
             }
         }
  }


Note the delegate that I have highlighted. Without this delegate, a self-signed certificate will cause an exception, preventing communication with the server. This delegate is called during this verification and overrides the default behaviour. As this essentially skips the verification of certificates, it shouldn’t make it to a production deployment. (See The Joy of Code for the source of this information)

This configuration should now work. One piece of the puzzle remains; how to identify which user is accessing the web methods. This may be achieved using the following code segment, which can be added to the service methods:

string userName=OperationContext.Current.IncomingMessageProperties.Security.ServiceSecurityContext.PrimaryIdentity.Name;


There are a few gotchas and tips I learnt during this work which you might find helpful.

Useful bits

Make sure that the server and client times are within the time span specified in the maxClockSkew attribute. If the time difference between the machines is greater than this time span, verification will fail. Frustratingly, Googling/Binging for the exception returned by WCF returns 99.9% related to this issue – you’ll probably need to look deeper to find out what actually causes your issue if your clocks marry up. Of course, if you’re deploying an intranet application, you lose control here.

WCF rarely provides useful exceptions. Remember to add the following configuration segment to your server configuration and use the WcfTraceViewer tool within the Windows SDK to diagnose issues. It is invaluable!

 

<system.diagnostics>
         
<sources>
                 
<source name="System.ServiceModel" switchValue="Information, ActivityTracing" propagateActivity="true">
                           
<listeners>
                                     
 <add name="traceListener" type="System.Diagnostics.XmlWriterTraceListener" initializeData= "D:\dev\Services\App_Data\Logging\Wcf\log.svclog" />
                          
 </listeners>
                 
</source>
          
 </sources>
</system.diagnostics>



Remember that Silverlight won’t communicate across domains to your web service, so you may need a clientaccesspolicy.xml or crossdomain.xml policy on your web site.

Recommended Reading

I’ve trawled through a number of books to help in cracking this, including:

  • Microsoft Silverlight 4 Data and Services Cookbook, Cleeron and Dockx, Packt Publishing ISBN 9781847199843 (Chapter 7) – invaluable as the examples are inevitably pragmatic and therefore more useful.
  • Programming WCF Services, Löwy, O’Reilly ISBN 9780596521301 – a great book for background reading, if only to find out what I couldn’t or shouldn’t use due to my Silverlight requirement. Essential reading if you want to get deeper into authentication over WCF such as using the certificates as authentication

I’d be interested in improvements you may think of.

Comments

# WCF web service over HTTPS web.config file

Monday, February 21, 2011 6:16 PM by Taiseer Joudeh

Thanks very much for your useful article.

For anyone who is looking for a clean Web.config configuration to call a web service over HTTPS with BasicHTTPBinding while sending large data such as Images, please use the below:

<system.serviceModel>

   <behaviors>

     <serviceBehaviors>

       <behavior name="sslBehaviour">

         <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true" />

         <serviceDebug includeExceptionDetailInFaults="true" />

       </behavior>

     </serviceBehaviors>

   </behaviors>

  <bindings>

     <basicHttpBinding>

       <binding name="basicwithssl" maxReceivedMessageSize="100000000" sendTimeout="00:10:00" receiveTimeout="00:10:00" textEncoding="UTF-8">

         <readerQuotas maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" maxArrayLength="2147483647" maxStringContentLength="2147483647" maxDepth="2147483647"/>

         <security mode="Transport"/>

       </binding>

     </basicHttpBinding>

   </bindings>

   <services>

     <service name="Service_1_0" behaviorConfiguration="sslBehaviour">

       <host>

         <baseAddresses>

           <add baseAddress="www.example.net/.../>

         </baseAddresses>

       </host>

       <endpoint address="" binding="basicHttpBinding" bindingConfiguration="basicwithssl" contract="UI.IService_1_0"> </endpoint>

     </service>

   </services>

   <serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="false" />

 </system.serviceModel>

# re: Silverlight and ASP.NET compatible WCF over SSL using custom username/password authentication

Tuesday, March 15, 2011 2:36 PM by frosty

Hello,

The RemoteCertificateValidationCallback doesn't work for Silverlight applications.  I've got to figure out a way to ignore invalid certs for Development.  I've tried putting code like you have in the default.aspx.cs file.

However, Silverlight doesn't support this and even though defined in the default page code, it isn't used.

# re: Silverlight and ASP.NET compatible WCF over SSL using custom username/password authentication

Tuesday, March 15, 2011 2:53 PM by Nathan Pledger

@frosty: I'll have a play with this tonight. I would say that having the check on Default.aspx.cs would have no effect, as the Silverlight control is essentially independent of the underlying page (it could just be a .html page). It would need to be within the SL control.

Leave a Comment

(required) 
(required) 
(optional)
(required) 
Please add 7 and 8 and type the answer here: