Saturday, March 5, 2011

ASP.NET MVC: using DotNetOpenAuth to create a simple OpenID log in

I recently started dabbling in ASP.NET MVC in an effort to make my own competative crowdsourcing web site. The nature of crowdsourcing is such that it requires multiple people to come together on the internet and work on the same project, but I wouldn't necessarily know who those people are, what are their intentions and much less have a reasonable way to ensure that they're all going to "play good." Developing the web site requires that the contributors have access to some parts of the project which should not be publicly available: e.g. the Web.config file (which contains the connection string of the database) and subsequently access to the database, since both the Web.config and the database will have to be modified as the web site grows. Of course this poses a serious problem for the privacy of the users: their personal information, such as e-mail address and password, may be accessible to members of the community as long as those members are also contributing to the web site.

There are probably some very good ways to prevent the personal information of the users from leaking out, perhaps by providing various credential levels for the contributors and less trusted users will have a restricted access to the database, and/or coming up with a complex scheme to protect private information. In any case, the security solutions only seem to mask the problem and the root of the problem is that I had to store the passwords and verify the user log in against the database. Naturally, the best way to secure such private information is not to store it at all, so I turned to OpenID! I only allow users to log in to the web site with an open ID and that model seems to work very well for other successful web sites, such as StackOverflow.

I got the latest DotNetOpenAuth library and since I'm as a total "noob" I cracked open the sample projects I opened up the ASP.NET MVC sample only to find out that the sample is missing the M from the MVC, which is a pretty important letter! A few days later I stumbled across a question on StackOverflow that gave me an opportunity to share my newly gained knowledge with the world! The OP was very satisfied with my answer and suggested that I write it in my blog, alas here I am writing about it!

So what would a very simple OpenID authentication in the MVC framework look like? Well, have a look for yourself:

1. We need a Model:


public class User
{
[DisplayName("User ID")]
public int UserID{ get; set; }

[Required]
[DisplayName("OpenID")]
public string OpenID { get; set; }
}

public class FormsAuthenticationService : IFormsAuthenticationService
{
public void SignIn(string openID, bool createPersistentCookie)
{
if (String.IsNullOrEmpty(openID)) throw new ArgumentException("OpenID cannot be null or empty.", "OpenID");

FormsAuthentication.SetAuthCookie(openID, createPersistentCookie);
}

public void SignOut()
{
FormsAuthentication.SignOut();
}
}



2. We need a View:


<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>


Log in - YourWebSiteName



<%--- If you have a domain, then you should sign up for an affiliate id with MyOpenID or something like that ---%>
Please log in with your OpenID or create an
OpenID with myOpenID
if you don't have one.


<%
string returnURL = HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"]);
if (returnURL == null)
{
returnURL = string.Empty;
}

using (Html.BeginForm("LogIn", "User", returnURL))
{%>
<%= Html.LabelFor(m => m.OpenID)%>:
<%= Html.TextBoxFor(m => m.OpenID)%>

<%
} %>

<%--- Display Errors ---%>
<%= Html.ValidationSummary()%>



3. And finally, we need a Controller:

[HandleError]
public class UserController : Controller
{
private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
public IFormsAuthenticationService FormsService { get; set; }

protected override void Initialize(RequestContext requestContext)
{
if (FormsService == null)
{
FormsService = new FormsAuthenticationService();
}

base.Initialize(requestContext);
}

// **************************************
// URL: /User/LogIn
// **************************************
public ActionResult LogIn()
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Profile", "User");
}

Identifier openID;
if (Identifier.TryParse(Request.QueryString["dnoa.userSuppliedIdentifier"], out openID))
{
return LogIn(new User { OpenID = openID }, Request.QueryString["ReturnUrl"]);
}
else
{
return View();
}
}

[HttpPost]
public ActionResult LogIn(User model, string returnUrl)
{
string openID = ModelState.IsValid?model.OpenID:Request.Form["openid_identifier"];

if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Profile", "User");
}
else if (!string.IsNullOrEmpty(openID))
{
return Authenticate(openID, returnUrl);
}
else if(ModelState.IsValid)
{
ModelState.AddModelError("error", "The OpenID field is required.");
}

// If we got this far, something failed, redisplay form
return View(model);
}

// **************************************
// URL: /User/LogOut
// **************************************
public ActionResult LogOut()
{
if (User.Identity.IsAuthenticated)
{
FormsService.SignOut();
}

return RedirectToAction("Index", "Home");
}

// **************************************
// URL: /User/Profile
// **************************************
[Authorize]
public ActionResult Profile(User model)
{
if (User.Identity.IsAuthenticated)
{
// ------- YOU CAN SKIP THIS SECTION ----------------
model = /*some code to get the user from the repository*/;

// If the user wasn't located in the database
// then add the user to our database of users
if (model == null)
{
model = RegisterNewUser(User.Identity.Name);
}
// --------------------------------------------------

return View(model);
}
else
{
return RedirectToAction("LogIn");
}
}

private User RegisterNewUser(string openID)
{
User user = new User{OpenID = openID};

// Create a new user model

// Submit the user to the database repository

// Update the user model in order to get the UserID,
// which is automatically generated from the DB.
// (you can use LINQ-to-SQL to map your model to the DB)

return user;
}

[ValidateInput(false)]
private ActionResult Authenticate(string openID, string returnUrl)
{
var response = openid.GetResponse();
if (response == null)
{
// Stage 2: user submitting Identifier
Identifier id;
if (Identifier.TryParse(openID, out id))
{
try
{
return openid.CreateRequest(openID).RedirectingResponse.AsActionResult();
}
catch (ProtocolException ex)
{
ModelState.AddModelError("error", "Invalid OpenID.");

ModelState.AddModelError("error", ex.Message);
return View("LogIn");
}
}
else
{
ModelState.AddModelError("error", "Invalid OpenID.");
return View("LogIn");
}
}
else
{
// Stage 3: OpenID Provider sending assertion response
switch (response.Status)
{
case AuthenticationStatus.Authenticated:
Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
FormsAuthentication.SetAuthCookie(response.FriendlyIdentifierForDisplay, true);
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Profile", "User");
}
case AuthenticationStatus.Canceled:
ModelState.AddModelError("error", "Authentication canceled at provider.");
return View("LogIn");
case AuthenticationStatus.Failed:
ModelState.AddModelError("error", "Authentication failed: " + response.Exception.Message);
return View("LogIn");
}
}
return new EmptyResult();
}
}


Wanna see it all in action? Go to my web site and log in with your OpenID: http://www.mydevarmy.com/User/LogIn

Voila! You have a simple log in with OpenID!

2 comments:

  1. @Kiril

    Thanks for this great peace of work explaining how to incorporate DotNetOpenAuth openID into an MVC application. I searched for days trying to find out how to accomplish this task, and my hat goes off to you! If your using MVC 3 it takes a little tweaking but it is doable.

    The only big problem I faced with your example was the IFormsAuthenticationService interface that you used. After some searching I found the answer that I would like to share:

    public interface IFormsAuthenticationService
    {
    void SignIn(string userName, bool createPersistentCookie);
    void SignOut();
    }

    ReplyDelete