DevelopMENTAL Madness

Thursday, July 02, 2009

Building a Single Sign On Provider Using ASP.NET and WCF: Part 2

Using Forms Authentication with WCF

This is the second article in a four part series on building a single sign on (SSO) provider using the ASP.NET platform. If you just want to know about forms authentication and WCF and aren’t interested in implementing SSO read ahead. Otherwise, make sure to check out part 1 (part 2, part 3, part 4 and the source are now available).

Introduction

An important distinction to make here before I get started is that I am not using FormsAuthentication to secure a WCF service. I am using FormsAuthentication to manage user identity. FormsAuthentication comes with handy Encrypt/Decrypt methods which allow us to pass a token around between our service, the client and the web application using our service as a trusted identity/authentication provider. But the methods only operate on a FormsAuthenticationTicket instance.

This is really not a problem in this scenario, it gives us just enough of what we need – we can encrypt the data objects we’re passing around and we can also use serialization to include any complex objects in the FormsAuthenticationTicket.UserData property.

Setup

As long as you are hosting WCF from ASP.NET you can take advantage of built-in ASP.NET features, including FormsAuthentication. There are many ways to secure an WCF service, but since we’re trying to build single sign on capabilities and we don’t want our users to be required to obtain additional credentials it makes sense to use FormsAuthentication for our service as well. MSDN contains documentation for setting up FormsAuthentication using System.Web.ApplicationService.AuthenticationService. AuthenticationService is designed to allow you to use ASP.NET Membership from any SOAP client regardless of wither or not it uses the .NET Framework. But if you want to use FormsAuthentication and keep it simple just do this:

  1. Configure ASP.NET FormsAuthentication in the web application where your WCF service is located:
    <system.web>
        ...
        <authentication mode="Forms">
            <forms cookieless="UseCookies" path="/SSO" name="SSOService"/>
        </authentication>
        ...
    </system.web>

    This step isn’t required, however it is recommended at least during development. Setting up your dev machine to make your browser think you are communicating with multiple different sites. You need to edit your local “hosts” file, then add host headers to your IIS configuration and then setup Visual Studio to startup each application with the correct url. Then if you have multiple members on your team it further complicates things. If you do it this way, your SSO Service and each application you’re debugging can have a separate cookie path and everything will behave as if you were communicating across domains.

  2. Add AspNetCompatibility to the system.ServiceModel configuration section:
    <system.serviceModel>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
        ...
    </system.serviceModel>

    Step 2 gives you access to the ASP.NET intrinsic objects (Context, Server, Session, Request and Response) so you can read/write cookies.

  3. Add the AspNetCompatibilityRequirements attribute to your service implementation class:
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class SSOService : ISSOService, ISSOPartnerService
    {
        //... concrete interface implementation
    }

    If you don’t complete step 3, WCF will complain when it sees reads your config settings from step 2 and it doesn’t see the AspNetCompatibilityRequirements attribute on your service implementation.

Using FormsAuthentication

If the client can supply valid credentials then we want to be able to determine the identity of the client for subsequent requests. So after validating the credentials we need to create a ticket and then send it back to the client:

public SSOToken Login(string username, string password)
{
    // default response
    SSOToken token = new SSOToken
    {
        Token = string.Empty,
        Status = "DENIED"
    };
 
    // authenticate user
    if (string.CompareOrdinal("foo", username) == 0
        && string.CompareOrdinal("bar", password) == 0)
    {
        // mock data to simulate passing around additional data
        Guid temp = Guid.NewGuid();
 
        // manage cookie lifetime
        DateTime issueDate = DateTime.Now;
        DateTime expireDate = issueDate.AddMonths(1);
 
        // create the ticket and protect it
        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, username, issueDate, expireDate, true, temp.ToString());
        string protectedTicket = FormsAuthentication.Encrypt(ticket);
 
        // save the protected ticket with a cookie
        HttpCookie authorizationCookie = new HttpCookie(FormsAuthentication.FormsCookieName, protectedTicket);
        authorizationCookie.Expires = expireDate;
 
        // protect the cookie from session hijacking
        authorizationCookie.HttpOnly = true;
 
        // write the cookie to the response stream
        HttpContext.Current.Response.Cookies.Add(authorizationCookie);
 
        // update the response to indicate success
        token.Status = "SUCCESS";
        token.Token = protectedTicket;
    }
 
    return token;
}
 

Next, when our client needs to assert its identity to a web application using our SSO service we need to provide something which can’t be tampered with and the web application can use to verify with our service to ensure no tampering occurred.

public SSOToken RequestToken()
{
    // default response
    SSOToken token = new SSOToken
    {
        Token = string.Empty,
        Status = "DENIED"
    };
 
    // verify we've already authenticated the client
    if (HttpContext.Current.Request.IsAuthenticated)
    {
        // get the current identity
        FormsIdentity identity = (FormsIdentity)HttpContext.Current.User.Identity;
 
        // we'll send the client its own FormsAuthenticationTicket, but 
        // we'll encrypt it so only we can read it when the partner app
        // needs to validate who the client is through our service
        token.Token = FormsAuthentication.Encrypt(identity.Ticket);
        token.Status = "SUCCESS";
    }
 
    return token;
}
 

We’ll use the FormsAuthenticationTicket we created and give the client an encrypted copy. Neither the client nor the web application can read it. The intent is that we give it to the client, the client will forward it to the web application and then the web application will be required ask our service if we can read it and if it is valid. If everything succeeds, we’ll confirm to the web application that the user is indeed who it says it is and can accept our assertion of the client’s identity.

Security

I mentioned this in the last post, but I’ll say it again here. We don’t want to allow session hijacking here and so make sure you set your cookies to HttpOnly = true.

Conclusion

Basically, as long as you configure your service correctly for AspNetCompatibility you can still use forms authentication. By using FormsAuthentication you get everything you need for identity management pre-built for you.

If you’re following the SSO series, the next installment will focus on configuring WCF to support JSONP.

Labels: , ,