Securing WCF with Forms Authentication

In this post I will describe how to secure a WCF RESTful service with Forms Authentication.  I blogged on WCF many a times, but usually skipped right over security aspects of the service.  I will go into sufficient (hopefully) level of details now.

The idea of having an un-secured service on the internet is not an appealing one.  This means that anyone can connect to it and consume the data exposed by the service.  Yes, granted, that person would have to discover your service somehow, but still the aspects of security cannot be ignored.  As a result, we must authenticate and authorize every consumer of the service.  For authentication I will use forms authentication.  One of primary reasons why I want to do that is because I do not have to litter my API with user Id and password for every method.  Instead I will rely on built-in functionality in ASP.NET to do a bulk of heavy lifting for me.  Once ASP.NET established the user, I will generate an authorization cookie, and that cookie will be consumed by the client, and then re-submitted with requests.

So, here is my solution at a high level.

  • Create authentication WCF Service
  • Create Data WCF RESTful service, which has actual API I am exposing.
  • Secure the site with forms authentication.
  • Client will first call authentication service, get a cookie, then submit it with requests to RESTful service.

Let’s start by creating a RESTful service.  Just use built-in template in Visual Studio 2010

image

Now because I want a real service, I am going to use Entity Framework Code First to create some basic functionality to perform CRUD operations on a Person object.

 

namespace CustomWcfRestService
{
    public class Person
    {
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set;}
    }
}

I am also going to have a users table I am going to use for authentication.

namespace CustomWcfRestService
{
    public class User
    {
        public int UserID { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
    }
}

Here is sample code for my operational RESTful service.  I am using json format for everything.

using System.Collections.Generic;
using
System.Data;
using
System.Linq;
using
System.ServiceModel;
using
System.ServiceModel.Activation;
using
System.ServiceModel.Web;
using
System.Web.Script.Services;
 

namespace
CustomWcfRestService
{
    [
ServiceContract
]
    [
AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode
.Required)]
    [
ServiceBehavior(InstanceContextMode = InstanceContextMode
.PerCall)]
   
public class CustomService

    {
        [
WebGet(UriTemplate = "/GetPeople", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
       
public List<Person
> GetPeople()
        {
           
using(var ctx = new Context
())
            {
                ctx.Configuration.LazyLoadingEnabled =
false
;
                ctx.Configuration.ProxyCreationEnabled =
false
;
               
return
ctx.People.OrderBy(one => one.LastName).ThenBy(two => two.FirstName).ToList();
            }
        }
 
        [
WebInvoke(UriTemplate = "/Create", Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat
.Json)]
       
public Person Create(Person
person)
        {
           
using (var ctx = new Context
())
            {
                ctx.Entry(person).State =
EntityState
.Added;
                ctx.SaveChanges();
               
return
person;
            }
        }
 
        [
WebGet(UriTemplate = "/GetPerson?id={id}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat
.Json)]
       
public Person Get(int
id)
        {
           
using (var ctx = new Context
())
            {
                ctx.Configuration.LazyLoadingEnabled =
false
;
                ctx.Configuration.ProxyCreationEnabled =
false
;
               
return
ctx.People.Find(id);
            }
        }
 
        [
WebInvoke(UriTemplate = "/UpdatePerson", Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat
.Json)]
       
public Person Update(Person
person)
        {
           
using (var ctx = new Context
())
            {
                ctx.Entry(person).State =
EntityState
.Modified;
                ctx.SaveChanges();
               
return
person;
            }
        }
 
        [
WebInvoke(UriTemplate = "/GetPerson?id={id}", Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat
.Json)]
       
public void Delete(int
id)
        {
           
using (var ctx = new Context
())
            {
               
var person = new Person
{ID = id};
                ctx.Entry(person).State =
EntityState
.Deleted;
                ctx.SaveChanges();
            }
        }
 
    }
}

 

I am now going to add a new service, called LoginService that I will use for authentication.  It will use the same Users table to validate user name and password.

using System;
using
System.Linq;
using
System.ServiceModel;
using
System.ServiceModel.Activation;
using
System.Web.Security;
using
System.Web;
 

namespace
CustomWcfRestService
{
    [
AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode
.Required)]
    [
ServiceBehavior(InstanceContextMode = InstanceContextMode
.PerCall)]
   
public class LoginService : ILoginService

    {
 
       
public bool Login(string userName, string password)
        {
           
bool returnValue = false
;
           
User
user;
           
using (var ctx = new Context
())
            {
                user = ctx.Users.Where(one => one.UserName == userName).FirstOrDefault();
               
if (user != null
)
                {
                    returnValue = (user.Password == password);
                }
            }
           
if
(returnValue)
            {
               
var ticket = new FormsAuthenticationTicket
(
                        1,
                        userName,
                       
DateTime
.Now,
                       
DateTime
.Now.AddDays(1),
                       
true
,
                        user.UserID.ToString()
                    );
               
string encryptedTicket = FormsAuthentication
.Encrypt(ticket);
               
var cookie = new HttpCookie(FormsAuthentication
.FormsCookieName, encryptedTicket);
               
HttpContext
.Current.Response.Cookies.Add(cookie);
 
            }
           
return
returnValue;
        }
    }
}

 

As you can see, I validate credentials against database, then I am creating custom cookie, encrypting it and sending back to the client.  I have to use ASP.NET compatibility mode to enable HttpContext and related functionality.

Now, I need to enable actual security.  I am doing this entirely in Web.Config by enabling forms authentication, denying requests from un-authenticated users, then adding a Location exception just for my login service.

<?xml version="1.0"?>
<configuration>
    <connectionStrings>
        <add name="SecuredServiceDemo"
           connectionString="Server=.;Integrated Security=SSPI;Database=SecuredServiceDemo
"
           providerName="System.Data.SqlClient" />

    </connectionStrings>
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
        <authentication mode="Forms">
        </authentication>
        <authorization>
            <deny users="?"/>
        </authorization>
    </system.web>
 
    <location path="LoginService.svc">
        <system.web>
            <authorization>
                <allow users="?"/>
            </authorization>
        </system.web>
    </location>

 

 

And that is it.  Now just create a test client as in following:

using System.Net;
using
System.ServiceModel;
using
System.ServiceModel.Channels;
using
TestServiceApp.LoginService;
using
System.IO;
 

namespace
TestServiceApp
{
   
class Program

    {
       
static void Main(string[] args)
        {
           
var sharedCookie = string
.Empty;
           
bool
isValid;
           
string data = string
.Empty;
 
           
var authClient = new LoginServiceClient
();
           
using (new OperationContextScope
(authClient.InnerChannel))
            {
                isValid = authClient.Login(
"me@you.com", "pw"
);
               
if
(isValid)
                {
                   
var response = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty
.Name];
                    sharedCookie = response.Headers[
"Set-Cookie"
];
                }
            }
 
           
if
(isValid)
            {
 
               
var request = (HttpWebRequest)WebRequest.Create("http://localhost:48090/CustomService/GetPeople"
);
                request.Headers[
"Cookie"
] = sharedCookie;
               
var
responce = request.GetResponse();
               
using (var
stream = responce.GetResponseStream())
                {
                   
using (var reader = new StreamReader
(stream))
                    {
                        data = reader.ReadToEnd();
                    }
                }
            }
 
        }
    }
}

 

Just to confirm that everything is working, I can just comment out the part that authenticates and gets a cookie, and I get 404 as expected.

You can download the complete solution here and try yourself.