Implementing OpenId Authentication for a ASP.Net MVC2 site using DotNetOpenAuth
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<asp:Content ID="Content4" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">
<% if (ViewData["Message"] != null) { %>
<div style="border: solid 1px red">
<%= Html.Encode(ViewData["Message"].ToString())%>
</div>
<% } %>
<p>You must log in before entering the Members Area: </p>
<form action="Authenticate?ReturnUrl=<%=HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"]) %>" method="post">
<label for="openid_identifier">OpenID: </label>
<input id="openid_identifier" name="openid_identifier" size="40" />
<!--add the return url variable to be stored in the form as a hidden field-->
<input type="hidden" name="ReturnUrl" value="<%=Model.ReturnUrl %>" />
<input type="submit" value="Login" />
</form>
<script type="text/javascript">
document.getElementById("openid_identifier").focus();
</script>
</asp:Content>
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<FetchResponse>();
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();
}
More writing
AI Model Regulation (Fable, GPT-5.6, OSS)
We used to celebrate new AI models, but lately releases are being delayed, restricted, or slow-rolled over national security concerns. Should AI be regulated, and if so, by whom?
Clustered Indexes and Performance
One of the most overlooked aspects of database design that I see from most application developers is the clustered index. Let us take a moment to discuss why this detail is so important, and to walk through some examples.
I'm BACK!
After a brief 8-year break, I decided to start blogging again! Here is what motivated me to start.
Getting Started with the Telerik MVC Extensions
A review of my experience using the Telerik MVC Extensions on several real world projects and instructions for integrating the Telerik MVC Extensions into your ASP.Net MVC project.
Codemash 2012 Recap & Pictures
Codemash Recap and Event Photos from Codemash 2012 including misc event photos, pictures of humorous Tweets on the large screen, a few presentations and the Pecha Kucha talks