12 July 2010

Using the WCF Authentication Service without cookies

One of the most straightforward ways of securing web services for a Silverlight client involves leveraging ASP.NET Forms Authentication to provide cookie-based security. It’s a convenient and robust means of adding declarative role-based security to web services that involves exposing the .NET Authentication Service as a set of web methods.

However, what happens when you want to access these services using a console application or WPF client?

The WCF Authentication Service relies on cookies to maintain the link between client and server. An authenticating cookie is sent down to the client when the user logs in and sent back to the server for each subsequent data request.

This is fine if you are using a client that keeps an HTTP context on the go – i.e. an ASP.NET or Silverlight client – but if you use a client without an HTTP context – such as a console application or WPF client – then any attempt to access secured services will give you an “Access Denied” error.

If you use an HTTP debugger such as Charles you can see what’s going on with the requests. A cookie is being returned by the authentication service, but nothing is being sent in the header to the secured data services so the services are unable to authorise the call and allow access to the service.

The solution is to manage the cookie manually, accepting it from the authentication service and sending it back with each web service call.

Given the WCF provides access to pretty much any part of the SOAP message this is pretty straightforward. It all relies on using the OperationContext class to gain access to the headers where you can read and write the cookie.

The code sample below shows how to extract the authentication cookie that is returned by a call to the WCF Authentication Service:

//* Attempt a login
using(AuthService.AuthenticationServiceClient authClient = new AuthService.AuthenticationServiceClient())
{
    using (OperationContextScope scope = new OperationContextScope(authClient.InnerChannel))
    {
        if (authClient.Login("Username", "Password", null, false))
        {
            //* Retrieve the cookie that is returned in the response
            MessageProperties props = OperationContext.Current.IncomingMessageProperties;
            HttpResponseMessageProperty prop = props[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;
            cookies = prop.Headers[HttpResponseHeader.SetCookie];

            //* Adjust the cookie into something we can send back to the services (see function below)
            cookies = FormatCookie(cookies);
        }
    }
}

Note the use of the FormatCookie() function – this is a piece of code that formats the cookie received from the authentication call into something that you can send back to secured methods. It just removes the path and expires arguments.

/// <summary>
/// Formats a request cookie string from the cookies received from the authentication service
/// </summary>
/// <param name="input">The cookie string received from the authentications service</param>
/// <returns>A formatted cookie string to send to data requests</returns>
private static string FormatCookie(string input)
{
    string[] cookies = input.Split(new char[] { ',', ';' });
    StringBuilder buffer = new StringBuilder(input.Length * 10);
    foreach (string entry in cookies)
    {
        if (entry.IndexOf("=") > 0 && !entry.Trim().StartsWith("path") && !entry.Trim().StartsWith("expires"))
        {
            buffer.Append(entry).Append("; ");
        }
    }
    if (buffer.Length > 0)
    {
        buffer.Remove(buffer.Length - 2, 2);
    }
    return buffer.ToString();
}

Finally, you can send the formatted cookie into a call made to a secured data service and your security context will be picked up:

using(DataService.SecuredServicesClient dataClient = new DataService.SecuredServicesClient())
{
    using (OperationContextScope scope = new OperationContextScope(dataClient.InnerChannel))
    {
        //* Add our cookie into the request header
        var prop = new HttpRequestMessageProperty();
        prop.Headers.Add(HttpRequestHeader.Cookie, cookies);
        OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = prop;

        //* Make the data services call - the cookie will be sent and the client authenticated
        DataService.SecuredDataResponse response = dataClient.GetSecuredData();
    }
}

Filed under ASP.NET, C#, UI Development.