Lately I’ve been having a lot of fun architecting and developing a Silverlight project at work that uses WCF to communicate with a backend CMS system. One of the challenges however, was coming up with a reusable, maintainable, and extensible means of applying security to the WCF services. My solution just happens to be the topic of this post.

Luckily, I stumbled upon a post by David Betz (Understanding WCF Services in Silverlight 2) in which he walks the reader through a thorough tutorial on the proper way of using WCF with Silverlight. If you haven’t already read Understanding WCF Services in Silverlight 2 I recommend you do so – especially since the remainder of this post will build upon the SecurityOperationBehavior and SecurityOperationInvoker classes David introduces towards the middle of his post. I’d also strongly encourage you to read a prior post by David (Creating Streamlined, Simplified, yet Scalable WCF Connectivity), which serves as the foundation for his Understanding WCF Services in Silverlight 2 post.

Go ahead and read those two posts, I’ll wait for you right here.

Alright, now that you’ve read and understand David’s posts, let’s pick-up where he left off:

Using The SecurityOperationBehavior and SecurityOperationInvoker Classes

With the knowledge you’ve gained by reading David’s posts you should be able to setup a WCF project with operations secured like so:

Service Contract:

 

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.ServiceModel;
  5. using System.Text;
  6.  
  7. namespace Rjygraham.WcfSecurity.Services.ServiceContracts
  8. {
  9.  
  10.     [ServiceContract]
  11.     public interface ITestService
  12.     {
  13.  
  14.         [OperationContract(AsyncPattern = true)]
  15.         IAsyncResult BeginFoo(AsyncCallback callback, object state);
  16.  
  17.         string EndFoo(IAsyncResult result);
  18.  
  19.     }
  20. }

 

Service Implementation (notice the [SecurityOperationBehavior] attribute from David’s post):

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using Rjygraham.WcfSecurity.Services;
  6. using Rjygraham.WcfSecurity.Services.ServiceContracts;
  7.  
  8. namespace Rjygraham.WcfSecurity.ServiceImplementation
  9. {
  10.     public class TestService : ITestService
  11.     {
  12.  
  13.         [SecurityOperationBehavior]
  14.         public IAsyncResult BeginFoo(AsyncCallback callback, object state)
  15.         {
  16.             CompletedAsyncResult<string> result = new CompletedAsyncResult<string>(state, String.Format("Bar - {0}", DateTime.Now.ToString()));
  17.  
  18.             callback.Invoke(result);
  19.             return result;
  20.         }
  21.  
  22.         public string EndFoo(IAsyncResult result)
  23.         {
  24.             return ((CompletedAsyncResult<string>)result).Data;
  25.         }
  26.  
  27.     }
  28. }

 

Silverlight WCF Client (notice how GetFooAsync method adds the UserName and Password headers):

Code Snippet
  1. using System;
  2. using System.Net;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using System.Windows.Documents;
  6. using System.Windows.Ink;
  7. using System.Windows.Input;
  8. using System.Windows.Media;
  9. using System.Windows.Media.Animation;
  10. using System.Windows.Shapes;
  11. using Rjygraham.WcfSecurity.Services.ServiceContracts;
  12. using System.ServiceModel;
  13. using System.ServiceModel.Channels;
  14.  
  15. namespace Rjygraham.WcfSecurity.Services.Silverlight.Clients
  16. {
  17.  
  18.     public class TestServiceClient : ClientBase<ITestService>
  19.     {
  20.  
  21.         #region Constructors
  22.  
  23.         public TestServiceClient() : base() { }
  24.  
  25.         public TestServiceClient(Binding binding, EndpointAddress remoteAddress) : base(binding, remoteAddress) { }
  26.  
  27.         public TestServiceClient(EndpointAddress remoteAddress) : base(new BasicHttpBinding(), remoteAddress) { }
  28.  
  29.         #endregion
  30.  
  31.         #region GetFoo
  32.  
  33.         public void GetFooAsync(Action<string> callback)
  34.         {
  35.             using (OperationContextScope scope = new OperationContextScope((IContextChannel)Channel))
  36.             {
  37.                 MessageHeaders messageHeadersElement = OperationContext.Current.OutgoingMessageHeaders;
  38.                 messageHeadersElement.Add(MessageHeader.CreateHeader("UserName", "", "JohnDoe"));
  39.                 messageHeadersElement.Add(MessageHeader.CreateHeader("Password", "", "MyPassword"));
  40.  
  41.                 Channel.BeginFoo(GetFooCallback, callback);
  42.             }
  43.         }
  44.  
  45.         private void GetFooCallback(IAsyncResult result)
  46.         {
  47.             Action<string> callBack = result.AsyncState as Action<string>;
  48.             if (callBack != null)
  49.             {
  50.                 callBack.Invoke(Channel.EndFoo(result));
  51.             }
  52.         }
  53.  
  54.         #endregion
  55.  
  56.     }
  57.  
  58. }

 

 

 

 

And then from your application code:

Code Snippet
  1.  
  2. private void Button_Click(object sender, RoutedEventArgs e)
  3. {
  4.     TestServiceClient client = new TestServiceClient(new EndpointAddress("http://localhost:54936/TestService.svc"));
  5.     client.GetFooAsync(Callback);
  6. }
  7.  
  8. private void Callback(string value)
  9. {
  10.     Deployment.Current.Dispatcher.BeginInvoke(delegate
  11.     {
  12.         TestLabel.Text = value;
  13.     });
  14. }

 

So far, so good, but as you’ll recall from David’s post, the code that does the actual validation of the username and password is embedded deep within the SecurityOperationInvoker class (this is where we retrieve the UserName and Password headers to do validation):

Code Snippet
  1. public Object Invoke(Object instance, Object[] inputs, out Object[] outputs)
  2. {
  3.     //+ authorization
  4.     MessageHeaders messageHeadersElement = OperationContext.Current.IncomingMessageHeaders;
  5.     Int32 id = messageHeadersElement.FindHeader("UserName", "") + messageHeadersElement.FindHeader("Password", "");
  6.     if (id > -1)
  7.     {
  8.         String username = messageHeadersElement.GetHeader<String>("UserName", "");
  9.         String password = messageHeadersElement.GetHeader<String>("Password", "");
  10.         SecurityValidator.Authenticate(username, password);
  11.         //+
  12.         return InnerOperationInvoker.Invoke(instance, inputs, out outputs);
  13.     }
  14.     //+
  15.     throw new FaultException<InvalidOperationException>(new InvalidOperationException(SecurityValidator.Message.InvalidCredentials), SecurityValidator.Message.InvalidCredentials);
  16. }

 

As you can see, this is not an ideal setup.

DataAnnotations to the Rescue

If you’ve worked with DataAnnotations within Silverlight or RIA Services, you might be familiar with the CustomValidationAttribute which allows you to write custom validation logic for your model classes. You might end up with something like the following:

The Person class (note the CustomValidationAttribute declared on the Person class):

Code Snippet
  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. using System.Net;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6. using System.Windows.Documents;
  7. using System.Windows.Ink;
  8. using System.Windows.Input;
  9. using System.Windows.Media;
  10. using System.Windows.Media.Animation;
  11. using System.Windows.Shapes;
  12.  
  13. namespace Rjygraham.WcfSecurity.SilverlightClient
  14. {
  15.  
  16.     [CustomValidation(typeof(PersonValidator), "ValidateAgeGender")]
  17.     public class Person
  18.     {
  19.  
  20.         [Required]
  21.         public string FirstName { get; set; }
  22.  
  23.         [Required]
  24.         public string LastName { get; set; }
  25.  
  26.         [Range(0d, 100d)]
  27.         public int Age { get; set; }
  28.  
  29.         public bool IsMale { get; set; }
  30.  
  31.     }
  32.  
  33. }

 

The PersonValidator class:

Code Snippet
  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. using System.Net;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6. using System.Windows.Documents;
  7. using System.Windows.Ink;
  8. using System.Windows.Input;
  9. using System.Windows.Media;
  10. using System.Windows.Media.Animation;
  11. using System.Windows.Shapes;
  12.  
  13. namespace Rjygraham.WcfSecurity.SilverlightClient
  14. {
  15.     public class PersonValidator
  16.     {
  17.  
  18.         public static ValidationResult ValidateAgeGender(Person value)
  19.         {
  20.             if (value.IsMale && value.Age < 18)
  21.             {
  22.                 return new ValidationResult("All males must be 18 years of age or older.");
  23.             }
  24.  
  25.             if (!value.IsMale && value.Age < 21)
  26.             {
  27.                 return new ValidationResult("All females must be 21 years of age or older.");
  28.             }
  29.  
  30.             return ValidationResult.Success;
  31.  
  32.         }
  33.  
  34.     }
  35. }

 

Obviously this is a contrived example, but it illustrates the usefulness and the great extensibility the CustomValidationAttribute class provides DataAnnotations. The initial rendition of the SecurityOperationInvoker is practically begging for the same type of extensibility. In order to do so, we’ll need to modify the SecurityOperationBehavior class to accept a type and method name to pass to the SecurityOperationInvoker class. After some quick Reflectoring of the CustomValidationAttribute class we end up with a SecurityOperationBehavior that looks like this:

Modified SecurityOperationBehavior class:

Code Snippet
  1. using System;
  2. using System.Globalization;
  3. using System.Reflection;
  4. using System.ServiceModel;
  5. using System.ServiceModel.Description;
  6. using System.ServiceModel.Dispatcher;
  7.  
  8. namespace Rjygraham.WcfSecurity.Services
  9. {
  10.  
  11.     [AttributeUsage(AttributeTargets.Method)]
  12.     public class SecurityOperationBehavior : Attribute, IOperationBehavior
  13.     {
  14.  
  15.         #region Fields
  16.  
  17.         private string _cachedErrorMessage;
  18.         private bool _verifiedWellFormed;
  19.         private string _method;
  20.         private Type _validatorType;
  21.  
  22.         #endregion
  23.  
  24.         #region Constructors
  25.  
  26.         public SecurityOperationBehavior(Type validatorType, string method)
  27.         {
  28.             _validatorType = validatorType;
  29.             _method = method;
  30.             string errorMessage = null;
  31.             if (!IsAttributeWellFormed(out errorMessage))
  32.             {
  33.                 throw new InvalidOperationException(errorMessage);
  34.             }
  35.         }
  36.  
  37.         #endregion
  38.  
  39.         #region IOperationBehavior Members
  40.  
  41.         public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
  42.         {
  43.             // Intentionally left empty.
  44.         }
  45.  
  46.         public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  47.         {
  48.             // Intentionally left empty.
  49.         }
  50.  
  51.         public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  52.         {
  53.             dispatchOperation.Invoker = new SecurityOperationInvoker(dispatchOperation.Invoker, this._validatorType, this._method);
  54.         }
  55.  
  56.         public void Validate(OperationDescription operationDescription)
  57.         {
  58.             // Intentionally left empty.
  59.         }
  60.  
  61.         #endregion
  62.  
  63.         #region Validation Methods
  64.  
  65.         private bool IsAttributeWellFormed(out string errorMessage)
  66.         {
  67.             if (!_verifiedWellFormed)
  68.             {
  69.                 _verifiedWellFormed = true;
  70.                 _cachedErrorMessage = ValidateTypeParameter();
  71.                 if (_cachedErrorMessage == null)
  72.                 {
  73.                     _cachedErrorMessage = ValidateMethodParameter();
  74.                 }
  75.             }
  76.             errorMessage = _cachedErrorMessage;
  77.             return (errorMessage == null);
  78.         }
  79.  
  80.         private string ValidateTypeParameter()
  81.         {
  82.             if (this._validatorType == null)
  83.             {
  84.                 return @"The SecurityOperationBehavior.ValidatorType was not specified.";
  85.             }
  86.  
  87.             if (!_validatorType.IsVisible)
  88.             {
  89.                 return string.Format(CultureInfo.CurrentCulture, @"The custom validation type '{0}' must be public.", new object[] { _validatorType.Name });
  90.             }
  91.             return null;
  92.         }
  93.  
  94.         private string ValidateMethodParameter()
  95.         {
  96.             if (string.IsNullOrEmpty(this._method))
  97.             {
  98.                 return "The SecurityOperationBehavior.Method was not specified.";
  99.             }
  100.  
  101.             MethodInfo method = _validatorType.GetMethod(_method, BindingFlags.Public | BindingFlags.Static);
  102.             if (method == null)
  103.             {
  104.                 return string.Format(CultureInfo.CurrentCulture, @"The SecurityOperationBehavior method '{0}' does not exist in type '{1}' or is not public and static.", new object[] { _method, _validatorType.Name });
  105.             }
  106.  
  107.             if (method.ReturnType != typeof(bool))
  108.             {
  109.                 return string.Format(CultureInfo.CurrentCulture, @"The SecurityOperationBehavior method '{0}' in type '{1}' must return System.Boolean. Use true to represent success.", new object[] { _method, _validatorType.Name });
  110.             }
  111.  
  112.             ParameterInfo[] parameters = method.GetParameters();
  113.             if ((parameters.Length != 1))
  114.             {
  115.                 return string.Format(CultureInfo.CurrentCulture, @"The SecurityOperationBehavior method '{0}' in type '{1}' must match the expected signature: public static bool {0}(System.ServiceModel.OperationContext context).", new object[] { _method, _validatorType.Name });
  116.             }
  117.  
  118.             if (parameters[0].ParameterType != typeof(OperationContext))
  119.             {
  120.                 return string.Format(CultureInfo.CurrentCulture, @"The SecurityOperationBehavior method '{0}' in type '{1}' must match the expected signature: public static bool {0}(System.ServiceModel.OperationContext context).", new object[] { _method, _validatorType.Name });
  121.             }
  122.  
  123.             return null;
  124.         }
  125.  
  126.         #endregion
  127.         
  128.     }
  129. }

 

And we’ll need to modify the SecurityOperationInvoker like so:

Code Snippet
  1. using System;
  2. using System.Reflection;
  3. using System.ServiceModel;
  4. using System.ServiceModel.Dispatcher;
  5.  
  6. namespace Rjygraham.WcfSecurity.Services
  7. {
  8.  
  9.     public class SecurityOperationInvoker : IOperationInvoker
  10.     {
  11.  
  12.         #region Fields
  13.  
  14.         private MethodInfo _methodInfo;
  15.  
  16.         #endregion
  17.  
  18.         #region Properties
  19.  
  20.         private IOperationInvoker InnerOperationInvoker { get; set; }
  21.  
  22.         public Boolean IsSynchronous
  23.         {
  24.             get { return InnerOperationInvoker.IsSynchronous; }
  25.         }
  26.  
  27.         #endregion
  28.  
  29.         #region Constructors
  30.  
  31.         public SecurityOperationInvoker(IOperationInvoker operationInvoker, Type validatorType, string method)
  32.         {
  33.             this.InnerOperationInvoker = operationInvoker;
  34.             _methodInfo = validatorType.GetMethod(method, BindingFlags.Public | BindingFlags.Static);
  35.         }
  36.  
  37.         #endregion
  38.  
  39.         #region Public Methods
  40.  
  41.         public Object[] AllocateInputs()
  42.         {
  43.             return InnerOperationInvoker.AllocateInputs();
  44.         }
  45.  
  46.         public Object Invoke(Object instance, Object[] inputs, out Object[] outputs)
  47.         {
  48.             if (Validate(OperationContext.Current))
  49.             {
  50.                 return InnerOperationInvoker.Invoke(instance, inputs, out outputs);
  51.             }
  52.             throw new FaultException<InvalidOperationException>(new InvalidOperationException("Unauthorized."), new FaultReason("Unauthorized."));
  53.         }
  54.  
  55.         public IAsyncResult InvokeBegin(Object instance, Object[] inputs, AsyncCallback callback, Object state)
  56.         {
  57.             if (Validate(OperationContext.Current))
  58.             {
  59.                 return InnerOperationInvoker.InvokeBegin(instance, inputs, callback, state);
  60.             }
  61.  
  62.             throw new FaultException<InvalidOperationException>(new InvalidOperationException("Unauthorized."), new FaultReason("Unauthorized."));
  63.         }
  64.  
  65.         public Object InvokeEnd(Object instance, out Object[] outputs, IAsyncResult result)
  66.         {
  67.             return InnerOperationInvoker.InvokeEnd(instance, out outputs, result);
  68.         }
  69.  
  70.         #endregion
  71.  
  72.         #region Private Methods
  73.  
  74.         private bool Validate(OperationContext context)
  75.         {
  76.             bool result = false;
  77.             try
  78.             {
  79.                 MethodInfo info = _methodInfo;
  80.                 result = (bool)info.Invoke(null, new object[] { context });
  81.             }
  82.             catch (TargetInvocationException exception)
  83.             {
  84.                 if (exception.InnerException != null)
  85.                 {
  86.                     throw exception.InnerException;
  87.                 }
  88.                 throw;
  89.             }
  90.             return result;
  91.         }
  92.  
  93.         #endregion
  94.  
  95.     }
  96. }

 

The changes to the SecurityOperationBehavior class are very straight forward, with most changes consisting of validating the validator class and method name. The interesting changes lie in the SecurityOperationInvoker class. In the Invoker class, we grab the MethodInfo of the validator class which we use in the Validate (lines 74-91 in the code snippet above) method called from within the Invoke (lines 46-53) and InvokeBegin (lines 55-62) methods of the Invoker class. You’ll see that the Validate method simply invokes the specified method on the validator class and returns the result. If Validate returns true, we continue to invoke the web service, otherwise we throw a FaultException.

With our modified SecurityOperationBehavior and SecurityOperationInvoker classes we can now create our validator class:

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.ServiceModel;
  6. using System.ServiceModel.Channels;
  7.  
  8. namespace Rjygraham.WcfSecurity.ServiceImplementation
  9. {
  10.     public class TestServiceValidator
  11.     {
  12.         public static bool ValidateUserNameAndPassword(OperationContext context)
  13.         {
  14.  
  15.             MessageHeaders headers = context.IncomingMessageHeaders;
  16.  
  17.             if (headers.FindHeader("UserName", "") > -1 && headers.FindHeader("Password", "") > -1)
  18.             {
  19.                 string userName = headers.GetHeader<string>("UserName", "");
  20.                 string password = headers.GetHeader<string>("Password", "");
  21.  
  22.                 if (userName == "JohnDoe" && password == "MyPassword")
  23.                 {
  24.                     return true;
  25.                 }
  26.             }
  27.  
  28.             return false;
  29.         }
  30.     }
  31. }

 

And modify our service implementation:

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using Rjygraham.WcfSecurity.Services;
  6. using Rjygraham.WcfSecurity.Services.ServiceContracts;
  7.  
  8. namespace Rjygraham.WcfSecurity.ServiceImplementation
  9. {
  10.     public class TestService : ITestService
  11.     {
  12.  
  13.         [SecurityOperationBehavior(typeof(TestServiceValidator), "ValidateUserNameAndPassword")]
  14.         public IAsyncResult BeginFoo(AsyncCallback callback, object state)
  15.         {
  16.             CompletedAsyncResult<string> result = new CompletedAsyncResult<string>(state, String.Format("Bar - {0}", DateTime.Now.ToString()));
  17.  
  18.             callback.Invoke(result);
  19.             return result;
  20.         }
  21.  
  22.         public string EndFoo(IAsyncResult result)
  23.         {
  24.             return ((CompletedAsyncResult<string>)result).Data;
  25.         }
  26.  
  27.     }
  28. }

Summary

By pulling the validation code out of the SecurityOperationInvoker class we’ve made it 100% extensible – just like the CustomValidationAttribute of DataAnnotations. Since every application may use different validation logic having a solution that is extensible is important. Note that we are using Reflection here so there might be some minor performance hits – especially when the service is firing up for the first time. However, since we’re caching the MethodInfo inside the Invoker class, the only performance hit we’ll take when the WCF method is called is the Invoke within the Validate method of the SecurityOperationInvoker.

I hope you find this solution as useful as I did. Feel free to grab the code below and make whatever changes you need.

Rjygraham.WcfSecurity.zip