Integrating WCF services with forms authentication

A colleague at work wanted to host a json wcf service inside a web application and call it from jquery on an aspx reataining all authentication info. The web app used forms authentication. Luckily jquery passed all authentication info but WCF didn?t use it.

We started by looking at similar MVC solution with authorize attribute and filters, unfortunately it was not a good idea. But then i remembered that before authorize attribute and filters were implemented in MVC I used a PrincipalPermission attribute (more on this in blog post only in polish). PrincipalPermission is a standard System.Security mechanism to limit access to methods. So I implemented my service as follows:

[ServiceContract]
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class TestService
{
	[WebGet(UriTemplate = "/DoWork", ResponseFormat = WebMessageFormat.Json)]
        [OperationContract]
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        public string DoWork()
        {
            return DateTime.Now.ToString();
        }
}

Everything was fine but the service did not work even when the user was logged in. It turns out that PrincipalPermission is used from within CheckAccess but it uses Thread.CurrentPrincipal to check current identity and because we are working within web app identity is stored in HttpContext.Current.User. One solution is to copy identity in service constructor but it?s not quite as elegant, so I came up with a service behavior:

public class UseFormsAutenticationMessageInspector :
	IDispatchMessageInspector
{
        public UseFormsAutenticationMessageInspector()
        {
        }

        public object AfterReceiveRequest(
		ref Message request,
		IClientChannel channel,
		InstanceContext instanceContext)
        {
            Thread.CurrentPrincipal = HttpContext.Current.User;
            return null;
        }

        public void BeforeSendReply(
		ref Message reply,
		object correlationState)
        {
        }
}

That seemed to work fine except it returned http status code 400 (bad request) when the user was not logged in and we wanted to have it return 403 forbidden. The answer was another service behavior which would add ErrorHandler which handles SecurityException. The only problem was that the error handler didn?t get a SecurityException but a FaultException. After some digging I wrote following error handler

    public class SecurityExceptionErrorHandler : IErrorHandler
    {

        public bool HandleError(Exception error)
        {
            return false;
        }

        public void ProvideFault(
            Exception error,
            MessageVersion version,
            ref Message fault)
        {
            FaultException f = error as FaultException;
            if ( f == null || f.Code.SubCode.Name != "FailedAuthentication"  )
                return;

            fault = Message.CreateMessage(version, String.Empty);

            HttpResponseMessageProperty rmp = new HttpResponseMessageProperty();

            rmp.StatusCode = System.Net.HttpStatusCode.Forbidden;
            rmp.StatusDescription = "Forbidden";

            fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);

            OperationContext.Current.RequestContext.Reply(fault);
        }

    }

And a corresponding service behavior:

    public class HandleSecurityExceptionAttribute : Attribute, IServiceBehavior
    {

        public void AddBindingParameters(
            ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase,
            Collection<ServiceEndpoint> endpoints,
            BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyDispatchBehavior(
            ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase)
        {
            foreach (ChannelDispatcherBase chanDisp in serviceHostBase.ChannelDispatchers)
            {
                ChannelDispatcher disp = chanDisp as ChannelDispatcher;
                disp.ErrorHandlers.Add(new SecurityExceptionErrorHandler());
            }
        }

        public void Validate(
            ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase)
        {
        }
    }

The final service looks like this:

    [UseFormsAuthentication]
    [HandleSecurityException]
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class TestService
    {
        [WebGet(UriTemplate = "/DoWork", ResponseFormat = WebMessageFormat.Json)]
        [OperationContract]
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        public string DoWork()
        {
            return DateTime.Now.ToString();
        }
    }
Tagged , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *