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
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.ServiceModel;
- using System.Text;
-
- namespace Rjygraham.WcfSecurity.Services.ServiceContracts
- {
-
- [ServiceContract]
- public interface ITestService
- {
-
- [OperationContract(AsyncPattern = true)]
- IAsyncResult BeginFoo(AsyncCallback callback, object state);
-
- string EndFoo(IAsyncResult result);
-
- }
- }
Service Implementation (notice the [SecurityOperationBehavior] attribute from David’s post):
Code Snippet
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Web;
- using Rjygraham.WcfSecurity.Services;
- using Rjygraham.WcfSecurity.Services.ServiceContracts;
-
- namespace Rjygraham.WcfSecurity.ServiceImplementation
- {
- public class TestService : ITestService
- {
-
- [SecurityOperationBehavior]
- public IAsyncResult BeginFoo(AsyncCallback callback, object state)
- {
- CompletedAsyncResult<string> result = new CompletedAsyncResult<string>(state, String.Format("Bar - {0}", DateTime.Now.ToString()));
-
- callback.Invoke(result);
- return result;
- }
-
- public string EndFoo(IAsyncResult result)
- {
- return ((CompletedAsyncResult<string>)result).Data;
- }
-
- }
- }
Silverlight WCF Client (notice how GetFooAsync method adds the UserName and Password headers):
Code Snippet
- using System;
- using System.Net;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Documents;
- using System.Windows.Ink;
- using System.Windows.Input;
- using System.Windows.Media;
- using System.Windows.Media.Animation;
- using System.Windows.Shapes;
- using Rjygraham.WcfSecurity.Services.ServiceContracts;
- using System.ServiceModel;
- using System.ServiceModel.Channels;
-
- namespace Rjygraham.WcfSecurity.Services.Silverlight.Clients
- {
-
- public class TestServiceClient : ClientBase<ITestService>
- {
-
- #region Constructors
-
- public TestServiceClient() : base() { }
-
- public TestServiceClient(Binding binding, EndpointAddress remoteAddress) : base(binding, remoteAddress) { }
-
- public TestServiceClient(EndpointAddress remoteAddress) : base(new BasicHttpBinding(), remoteAddress) { }
-
- #endregion
-
- #region GetFoo
-
- public void GetFooAsync(Action<string> callback)
- {
- using (OperationContextScope scope = new OperationContextScope((IContextChannel)Channel))
- {
- MessageHeaders messageHeadersElement = OperationContext.Current.OutgoingMessageHeaders;
- messageHeadersElement.Add(MessageHeader.CreateHeader("UserName", "", "JohnDoe"));
- messageHeadersElement.Add(MessageHeader.CreateHeader("Password", "", "MyPassword"));
-
- Channel.BeginFoo(GetFooCallback, callback);
- }
- }
-
- private void GetFooCallback(IAsyncResult result)
- {
- Action<string> callBack = result.AsyncState as Action<string>;
- if (callBack != null)
- {
- callBack.Invoke(Channel.EndFoo(result));
- }
- }
-
- #endregion
-
- }
-
- }
And then from your application code:
Code Snippet
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- TestServiceClient client = new TestServiceClient(new EndpointAddress("http://localhost:54936/TestService.svc"));
- client.GetFooAsync(Callback);
- }
-
- private void Callback(string value)
- {
- Deployment.Current.Dispatcher.BeginInvoke(delegate
- {
- TestLabel.Text = value;
- });
- }
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
- public Object Invoke(Object instance, Object[] inputs, out Object[] outputs)
- {
- //+ authorization
- MessageHeaders messageHeadersElement = OperationContext.Current.IncomingMessageHeaders;
- Int32 id = messageHeadersElement.FindHeader("UserName", "") + messageHeadersElement.FindHeader("Password", "");
- if (id > -1)
- {
- String username = messageHeadersElement.GetHeader<String>("UserName", "");
- String password = messageHeadersElement.GetHeader<String>("Password", "");
- SecurityValidator.Authenticate(username, password);
- //+
- return InnerOperationInvoker.Invoke(instance, inputs, out outputs);
- }
- //+
- throw new FaultException<InvalidOperationException>(new InvalidOperationException(SecurityValidator.Message.InvalidCredentials), SecurityValidator.Message.InvalidCredentials);
- }
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
- using System;
- using System.ComponentModel.DataAnnotations;
- using System.Net;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Documents;
- using System.Windows.Ink;
- using System.Windows.Input;
- using System.Windows.Media;
- using System.Windows.Media.Animation;
- using System.Windows.Shapes;
-
- namespace Rjygraham.WcfSecurity.SilverlightClient
- {
-
- [CustomValidation(typeof(PersonValidator), "ValidateAgeGender")]
- public class Person
- {
-
- [Required]
- public string FirstName { get; set; }
-
- [Required]
- public string LastName { get; set; }
-
- [Range(0d, 100d)]
- public int Age { get; set; }
-
- public bool IsMale { get; set; }
-
- }
-
- }
The PersonValidator class:
Code Snippet
- using System;
- using System.ComponentModel.DataAnnotations;
- using System.Net;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Documents;
- using System.Windows.Ink;
- using System.Windows.Input;
- using System.Windows.Media;
- using System.Windows.Media.Animation;
- using System.Windows.Shapes;
-
- namespace Rjygraham.WcfSecurity.SilverlightClient
- {
- public class PersonValidator
- {
-
- public static ValidationResult ValidateAgeGender(Person value)
- {
- if (value.IsMale && value.Age < 18)
- {
- return new ValidationResult("All males must be 18 years of age or older.");
- }
-
- if (!value.IsMale && value.Age < 21)
- {
- return new ValidationResult("All females must be 21 years of age or older.");
- }
-
- return ValidationResult.Success;
-
- }
-
- }
- }
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
- using System;
- using System.Globalization;
- using System.Reflection;
- using System.ServiceModel;
- using System.ServiceModel.Description;
- using System.ServiceModel.Dispatcher;
-
- namespace Rjygraham.WcfSecurity.Services
- {
-
- [AttributeUsage(AttributeTargets.Method)]
- public class SecurityOperationBehavior : Attribute, IOperationBehavior
- {
-
- #region Fields
-
- private string _cachedErrorMessage;
- private bool _verifiedWellFormed;
- private string _method;
- private Type _validatorType;
-
- #endregion
-
- #region Constructors
-
- public SecurityOperationBehavior(Type validatorType, string method)
- {
- _validatorType = validatorType;
- _method = method;
- string errorMessage = null;
- if (!IsAttributeWellFormed(out errorMessage))
- {
- throw new InvalidOperationException(errorMessage);
- }
- }
-
- #endregion
-
- #region IOperationBehavior Members
-
- public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
- {
- // Intentionally left empty.
- }
-
- public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
- {
- // Intentionally left empty.
- }
-
- public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
- {
- dispatchOperation.Invoker = new SecurityOperationInvoker(dispatchOperation.Invoker, this._validatorType, this._method);
- }
-
- public void Validate(OperationDescription operationDescription)
- {
- // Intentionally left empty.
- }
-
- #endregion
-
- #region Validation Methods
-
- private bool IsAttributeWellFormed(out string errorMessage)
- {
- if (!_verifiedWellFormed)
- {
- _verifiedWellFormed = true;
- _cachedErrorMessage = ValidateTypeParameter();
- if (_cachedErrorMessage == null)
- {
- _cachedErrorMessage = ValidateMethodParameter();
- }
- }
- errorMessage = _cachedErrorMessage;
- return (errorMessage == null);
- }
-
- private string ValidateTypeParameter()
- {
- if (this._validatorType == null)
- {
- return @"The SecurityOperationBehavior.ValidatorType was not specified.";
- }
-
- if (!_validatorType.IsVisible)
- {
- return string.Format(CultureInfo.CurrentCulture, @"The custom validation type '{0}' must be public.", new object[] { _validatorType.Name });
- }
- return null;
- }
-
- private string ValidateMethodParameter()
- {
- if (string.IsNullOrEmpty(this._method))
- {
- return "The SecurityOperationBehavior.Method was not specified.";
- }
-
- MethodInfo method = _validatorType.GetMethod(_method, BindingFlags.Public | BindingFlags.Static);
- if (method == null)
- {
- 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 });
- }
-
- if (method.ReturnType != typeof(bool))
- {
- 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 });
- }
-
- ParameterInfo[] parameters = method.GetParameters();
- if ((parameters.Length != 1))
- {
- 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 });
- }
-
- if (parameters[0].ParameterType != typeof(OperationContext))
- {
- 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 });
- }
-
- return null;
- }
-
- #endregion
-
- }
- }
And we’ll need to modify the SecurityOperationInvoker like so:
Code Snippet
- using System;
- using System.Reflection;
- using System.ServiceModel;
- using System.ServiceModel.Dispatcher;
-
- namespace Rjygraham.WcfSecurity.Services
- {
-
- public class SecurityOperationInvoker : IOperationInvoker
- {
-
- #region Fields
-
- private MethodInfo _methodInfo;
-
- #endregion
-
- #region Properties
-
- private IOperationInvoker InnerOperationInvoker { get; set; }
-
- public Boolean IsSynchronous
- {
- get { return InnerOperationInvoker.IsSynchronous; }
- }
-
- #endregion
-
- #region Constructors
-
- public SecurityOperationInvoker(IOperationInvoker operationInvoker, Type validatorType, string method)
- {
- this.InnerOperationInvoker = operationInvoker;
- _methodInfo = validatorType.GetMethod(method, BindingFlags.Public | BindingFlags.Static);
- }
-
- #endregion
-
- #region Public Methods
-
- public Object[] AllocateInputs()
- {
- return InnerOperationInvoker.AllocateInputs();
- }
-
- public Object Invoke(Object instance, Object[] inputs, out Object[] outputs)
- {
- if (Validate(OperationContext.Current))
- {
- return InnerOperationInvoker.Invoke(instance, inputs, out outputs);
- }
- throw new FaultException<InvalidOperationException>(new InvalidOperationException("Unauthorized."), new FaultReason("Unauthorized."));
- }
-
- public IAsyncResult InvokeBegin(Object instance, Object[] inputs, AsyncCallback callback, Object state)
- {
- if (Validate(OperationContext.Current))
- {
- return InnerOperationInvoker.InvokeBegin(instance, inputs, callback, state);
- }
-
- throw new FaultException<InvalidOperationException>(new InvalidOperationException("Unauthorized."), new FaultReason("Unauthorized."));
- }
-
- public Object InvokeEnd(Object instance, out Object[] outputs, IAsyncResult result)
- {
- return InnerOperationInvoker.InvokeEnd(instance, out outputs, result);
- }
-
- #endregion
-
- #region Private Methods
-
- private bool Validate(OperationContext context)
- {
- bool result = false;
- try
- {
- MethodInfo info = _methodInfo;
- result = (bool)info.Invoke(null, new object[] { context });
- }
- catch (TargetInvocationException exception)
- {
- if (exception.InnerException != null)
- {
- throw exception.InnerException;
- }
- throw;
- }
- return result;
- }
-
- #endregion
-
- }
- }
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
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Web;
- using System.ServiceModel;
- using System.ServiceModel.Channels;
-
- namespace Rjygraham.WcfSecurity.ServiceImplementation
- {
- public class TestServiceValidator
- {
- public static bool ValidateUserNameAndPassword(OperationContext context)
- {
-
- MessageHeaders headers = context.IncomingMessageHeaders;
-
- if (headers.FindHeader("UserName", "") > -1 && headers.FindHeader("Password", "") > -1)
- {
- string userName = headers.GetHeader<string>("UserName", "");
- string password = headers.GetHeader<string>("Password", "");
-
- if (userName == "JohnDoe" && password == "MyPassword")
- {
- return true;
- }
- }
-
- return false;
- }
- }
- }
And modify our service implementation:
Code Snippet
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Web;
- using Rjygraham.WcfSecurity.Services;
- using Rjygraham.WcfSecurity.Services.ServiceContracts;
-
- namespace Rjygraham.WcfSecurity.ServiceImplementation
- {
- public class TestService : ITestService
- {
-
- [SecurityOperationBehavior(typeof(TestServiceValidator), "ValidateUserNameAndPassword")]
- public IAsyncResult BeginFoo(AsyncCallback callback, object state)
- {
- CompletedAsyncResult<string> result = new CompletedAsyncResult<string>(state, String.Format("Bar - {0}", DateTime.Now.ToString()));
-
- callback.Invoke(result);
- return result;
- }
-
- public string EndFoo(IAsyncResult result)
- {
- return ((CompletedAsyncResult<string>)result).Data;
- }
-
- }
- }
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