Implementing OpenId Authentication for a ASP.Net MVC2 site using DotNetOpenAuth - 11/26/2010

OpenId is an excellent standard that has dramatically improved the user experience on the web allowing users to obtain personalized access to websites without creating an account on every site they visit. Trusted by some of the most popular websites on the internet such as Stack Overflow, DotNetOpenAuth is a powerful library that takes the pain out of implementing OpenId.

The Dilemma of Authentication

Authentication is a necessary evil; it is the unfortunate price we pay in order to have non-anonymous access to a system.

From a user perspective, you really just want personalized access but the idea of creating a login and password on each site you want personalized access on is a hassle. Aside from the initial friction of creating a login and password, you now have the burden of securely keeping track of yet another login and password.

As a webmaster, you are aware that your users may leave your site and never return as soon as you require them to create an account. You would like to avoid the hassle and cost of creating a secure login process, account registration process and forgotten password process. However you want to build personalized, interactive features and you cannot do that without somehow verifying the user’s identity.

OpenId as a Solution

Luckily there is a solution to this problem that benefits both users and webmasters, the solution is OpenId.

Users benefit as they no longer need to register an account and instead can just authorize your site to re-use an account they already have. This saves them time and results in one less password to remember.

Webmasters save time and money as they no longer need to worry about creating and maintaining the framework that would be needed to manage a database of accounts.

Using DotNetOpenAuth to Implement OpenId Relying Party

Unfortunately while OpenId is a very friendly for the users, it is a tedious and complicated protocol for system creators. There are many steps going on behind the scenes in the OpenId protocol and to develop and test an implementation from scratch would not be a simple task.

Fortunately there is a pre-packaged API that implements OpenId called DotNetOpenAuth that can be leveraged to greatly simplify this task. Using DotNetOpenAuth you can take away the pain of rolling your own OpenId Protocol implementation.

Getting Started with DotNetOpenAuth

To get started with DotNetOpenAuth you go to http://www.dotnetopenauth.net/ and download the current version. The files come as a zip, extract them to the location of your preference. In the zip file you will find a number of example projects inside of a samples folder. The sample we are using today is "OpenIdRelyingPartyMvc". Also note, Rick Strahl has a nice blog on how to setup OpenId using DotNetOpenAuth.

Improving the Samples - Adding ReturnUrl as a Callback Argument

At this point I am assuming you have downloaded DotNetOpenAuth and have reviewed the OpenIdRelyingPartyMvc sample. This sample is good and it works but needs some refinement to be ready for production.

With the MVC Relying Party example that comes with DotNetOpenAuth, ReturnUrl does not seem to be handled correctly. When a user requests a site that requires authentication they are sent to the login view with an optional Url argument for ReturnUrl as is standard with .Net Authentication. However, once on this view when the user posts to the server what their open id identifier is, the ReturnUrl is lost. To resolve this we will store ReturnUrl in the Login.aspx view so that it is posted by the user to the server. We will then add a Callback argument to our OpenId request so that this argument is then echoed back to us when the response is received. Finally, in this step we are cleaning up the usage of Request.Form and instead creating a model that will hold our form variables.

Store ReturnUrl as a hidden field in the Login.aspx View


	<% if (ViewData["Message"] != null) { %>
	
<%= Html.Encode(ViewData["Message"].ToString())%>
<% } %>

You must log in before entering the Members Area:

" method="post">
Create A New Model Called LogOnModel.cs In Your Models Folder
public class LogOnModel
{
    public string ReturnUrl { get; set; }
    public string openid_identifier { get; set; }
}
Modify the Login method in UserController.cs
public ActionResult LogOn(string returnUrl)
{
    //stage 1:  User loads logon view
    var model = new LogOnModel();
    if (!string.IsNullOrEmpty(returnUrl))
    {
        model.ReturnUrl = returnUrl;
    }
    return View();
}
Modify the Authenticate method in UserController.cs. The first change is adding returnUrl as a callback argument to the OpenId Request, we will also replace Request.Form usage and instead use LogonModel
[ValidateInput(false)]
public ActionResult Authenticate(LogOnModel model)
{
    var response = openid.GetResponse();
    if (response == null)
    {
        // Stage 2: user submitting Identifier
        Identifier id;
        if (Identifier.TryParse(model.openid_identifier, out id))
        {
            try
            {
                var request = openid.CreateRequest(Identifier.Parse(model.openid_identifier));
                       
                //add returnURL as a callback argument
                if (!string.IsNullOrEmpty(model.ReturnUrl))
                {
                    request.AddCallbackArguments("ReturnUrl", model.ReturnUrl);
                }
                return request.RedirectingResponse.AsActionResult();

            }
            catch (ProtocolException ex)
            {
                ViewData["Message"] = ex.Message;
                return View("Login");
            }
        }
        else
        {
            ViewData["Message"] = "Invalid identifier";
            return View("Login");
        }
    }
    else
    {
        // Stage 3: OpenID Provider sending assertion response
        switch (response.Status)
        {
            case AuthenticationStatus.Authenticated:
                Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
                FormsAuthentication.SetAuthCookie(response.ClaimedIdentifier, false);
                if (!string.IsNullOrEmpty(model.ReturnUrl))
                {
                    return Redirect(model.ReturnUrl);
                }
                else
                {
                    return RedirectToAction("Index", "Home");
                }
            case AuthenticationStatus.Canceled:
                ViewData["Message"] = "Canceled at provider";
                return View("Login");
            case AuthenticationStatus.Failed:
                ViewData["Message"] = response.Exception.Message;
                return View("Login");
        }
    }
    return new EmptyResult();
}

Getting First Name, Last Name and Email from the Trusted Party with a Fetch Request

With OpenId you have the ability to get additional information from the Trusted Party by doing a Fetch Request. In this case I wanted the first name, last name and email of the user. In Stage 2 below you add a FetchRequest to the OpenId Request. Then in Stage 3 you get the FetchResponse and grab the information.

Authenticate Method with Fetch Request and Fetch Response to Get User's Name and Email
[ValidateInput(false)]
public ActionResult Authenticate(LogOnModel model)
{
    var response = openid.GetResponse();
    if (response == null)
    {
        // Stage 2: user submitting Identifier
        Identifier id;
        if (Identifier.TryParse(model.openid_identifier, out id))
        {
            try
            {
                var request = openid.CreateRequest(Identifier.Parse(model.openid_identifier));

                //add fetch request to the OpenId request
                var fetch = new FetchRequest();
                fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
                fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);
                fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
                request.AddExtension(fetch);

                //add returnURL as a callback argument
                if (!string.IsNullOrEmpty(model.ReturnUrl))
                {
                    request.AddCallbackArguments("ReturnUrl", model.ReturnUrl);
                }
                return request.RedirectingResponse.AsActionResult();

            }
            catch (ProtocolException ex)
            {
                ViewData["Message"] = ex.Message;
                return View("Login");
            }
        }
        else
        {
            ViewData["Message"] = "Invalid identifier";
            return View("Login");
        }
    }
    else
    {
        // Stage 3: OpenID Provider sending assertion response
        switch (response.Status)
        {
            case AuthenticationStatus.Authenticated:
                //get values from fetch request
                var fetch = response.GetExtension();
                var email = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
                var firstName = fetch.GetAttributeValue(WellKnownAttributes.Name.First);
                var lastName = fetch.GetAttributeValue(WellKnownAttributes.Name.Last);

                //do something with name and email here


                Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
                FormsAuthentication.SetAuthCookie(response.ClaimedIdentifier, false);
                if (!string.IsNullOrEmpty(model.ReturnUrl))
                {
                    return Redirect(model.ReturnUrl);
                }
                else
                {
                    return RedirectToAction("Index", "Home");
                }
            case AuthenticationStatus.Canceled:
                ViewData["Message"] = "Canceled at provider";
                return View("Login");
            case AuthenticationStatus.Failed:
                ViewData["Message"] = response.Exception.Message;
                return View("Login");
        }
    }
    return new EmptyResult();
}

Share this article


Other Blogs

Getting Started with the Telerik MVC Extensions - 2/12/2012
Codemash 2012 Recap & Pictures - 1/14/2012
Findlay Area .Net Users Group(FANUG) - Improving Software Quality with Continuous Integration, and An Introduction to FluentMigrator - 9/28/2011
MVC3 Unobtrusive Validation With MVC Contrib - 8/14/2011
Automating SSRS Report Deployment for CI - 7/10/2011