Introduction
As some of you may know from reading some of my past articles/blogs, I am not really a web man, but I had an idea a while back to create a tool that had to be web based, so I have been busy constructing this ASP.NET MVC web site in my spare time.
It is still ongoing, but one area that the web site needed was login/authentication, which is a pretty usual requirement on web sites. In fact, ASP.NET has long supplied Forms Authentication for this purpose (as well as other options). Thing is, I did not really want to store username/passwords in my database; then I read about this protocol called OpenID, which is a protocol that numerous web sites adhere to already. And if you have an OpenID compliant login, my site could allow its users to use those credentials directly with the OpenID compliant web site validating them. They basically handle the login/authentication process and redirect back to the original calling site (my site).
This may sound quite nuts, but it is quite probable that you yourself are already in possession of an OpenID login, as there are many OpenID compliant web sites out there. So it seemed to make sense to allow users to simply use their existing login credentials rather than to ask them to create even more credentials for my site.
This is all well and good, so what is this article all about? Quite simple really, this article will demonstrate how to use OpenID with Forms Authentication (to store the authentication cookie) within an ASP.NET MVC web site.
I should mention that this demo apps web site is not the prettiest of web sites, as I have literally applied no styling to it what so ever; I intentionally left it without trying to muddy the water so to speak. So please be aware that it is not going to win any beauty contests at all.
A Brief Discussion About OpenID
OpenID is an open standard that describes how users can be authenticated in a decentralized manner, obviating the need for services to provide their own ad hoc systems and allowing users to consolidate their digital identities.
The OpenID protocol does not rely on a central authority to authenticate a user's identity. Moreover, neither services nor the OpenID standard may mandate a specific means by which to authenticate users, allowing for approaches ranging from the common (such as passwords) to the novel (such as smart cards or biometrics).
The term OpenID may also refer to an ID as specified in the OpenID standard; these IDs take the form of a unique URL, and are managed by some 'OpenID provider' that handles authentication.
OpenID authentication is now used and provided by several large websites. Providers include AOL, BBC, Facebook, Google, IBM, MySpace, Orange, PayPal, VeriSign, LiveJournal, Yandex, Ustream, and Yahoo!
Using OpenID
A basic glossary of the terms used with OpenID:
- End-user
The entity that wants to assert a particular identity.
- Identifier or OpenID
The URL or XRI chosen by the end-user to name the end-user's identity.
- Identity provider or OpenID provider
A service that specializes in registering OpenID URLs or XRIs and providing OpenID authentication (and possibly other identity services). Note that the OpenID specifications use the term "OpenID provider" or "OP". See also: List of OpenID providers.
- Relying party
The site that wants to verify the end-user's identifier; other terms include "service provider" or the now obsolete "consumer".
- User-agent
The program (such as a browser) used by the end-user to access an OpenID provider or a relying party.
Logging in
The end-user interacts with a relying party (such as a website) that provides a means by which to specify an OpenID for the purposes of authentication; an end-user typically has previously registered an OpenID (e.g., alice.openid.example.org) with an OpenID provider (e.g., openid.example.org).
The relying party typically transforms the OpenID into a canonical URL form (e.g., http://alice.openid.example.org/).
- With OpenID 1.0, the relying party then requests the HTML resource identified by the URL and reads an HTML link tag to discover the OpenID provider's URL (e.g., http://openid.example.org/openid-auth.php). The relying party also discovers whether to use a delegated identity (see below).
- With OpenID 2.0, the client discovers the OpenID provider URL by requesting the XRDS document (also called the Yadis document) with the content type application/xrds+xml; this document may be available at the target URL, and is always available for a target XRI.
There are two modes in which the relying party may communicate with the OpenID provider:
checkid_immediate
, in which the relying party requests that the OpenID provider not interact with the end-user. All communication is relayed through the end-user's user-agent without explicitly notifying the end-user.checkid_setup
, in which the end-user communicates with the OpenID provider via the same user-agent used to access the relying party.
The checkid_setup
mode is more popular on the Web; also, the checkid_immediate
mode can fall back to the checkid_setup
mode if the operation cannot be automated.
First, the relying party and the OpenID provider (optionally) establish a shared secret, referenced by an associate handle, which the relying party then stores. If using the checkid_setup
mode, the relying party redirects the user's user-agent to the OpenID provider so the end-user can authenticate directly with the OpenID provider.
The method of authentication may vary, but typically, an OpenID provider prompts the end-user for a password or an InfoCard, and then asks whether the end-user trusts the relying party to receive the necessary identity details.
If the end-user declines the OpenID provider's request to trust the relying party, then the user-agent is redirected to the relying party with a message indicating that authentication was rejected; the relying party in turn refuses to authenticate the end-user.
If the end-user accepts the OpenID provider's request to trust the relying party, then the user-agent is redirected to the relying party along with the end-user's credentials. That relying party must then confirm that the credentials really came from the OpenID provider. If the relying party and OpenID provider had previously established a shared secret, then the relying party can validate the identity of the OpenID provider by comparing its copy of the shared secret against the one received along with the end-user's credentials; such a relying party is called stateful because it stores the shared secret between sessions. On the contrary, a stateless or dumb relying party must make one more background request (check_authentication
) to ensure that the data indeed came from the OpenID provider.
After the OpenID has been verified, authentication is considered successful, and the end-user is considered logged in to the relying party under the identity specified by the given OpenID (e.g. alice.openid.example.org). The relying party typically then stores the end-user's OpenID along with the end-user's other session information.
If an OpenID provider uses strong authentication, OpenID can be used for secure transactions such as banking and e-commerce.
http://en.wikipedia.org/wiki/OpenID: Up on date 09/12/2010.
The Demo App
As I said at the beginning, the demo app is an ASP MVC web site that shows how to use OpenID along with Forms Authentication to store the authentication cookie.
This is what is looks like from a structural point of view:
It can be seen that it follows the standard ASP MVC project structure, and the only other thing of real note there is that there are several Views, but only Site.aspx requires authorization to occur before it can be viewed.
What the Demo App is Trying to Achieve
What I wanted for my site was to be able to use OpenID, and I also wanted an OpenID provider to handle the actual logging in part, you know where the username and password are actually typed in. I did not want to have to mess with those at all. OpenID does also allow for you to let users enter their OpenID username and password on your site, and then validate these against an OpenID provider, but that is not what I wanted; rather, I wanted to redirect to an OpenID provider to let the user log in and then be told, yes that user is valid, here is their login token, which I could then store for later.
This may not suit your purposes, in which case you should probably look at alternative solutions to this one.
Showcasing the Demo App
When we launch the demo code, in Index.aspx (the ASP.NET MVC default HomeController.Index action ensures we end up with the Index.aspx view), we will see the following. This page does not require authorization of any sorts.
What it does have is a link to a page (Site.aspx) that does require authorization before it can be viewed; when this link is clicked, we get something like this shown:
There are a couple of things to note there, such as:
- The browser's address bar shows a query string which includes a
ReturnUrl
which is set to ReturnUrl=/Site/Data, which just happens to be the controller/action URL of the page that we tried to load that required authorization before it could be viewed. This ReturnUrl
query string parameter is a standard feature of Forms Authentication, which is what we will eventually use to store just the Authentication cookie. - There are quite a few image buttons that can be clicked. Each of these images represents an OpenID compliant site that you could use to login with. For example, I have a Google account, so I may choose to use my Google credentials. I should point out that I got the bulk of the content for the Logon.aspx page from a blog somewhere, but I can not recall where from, so apologies for not mentioning the source directly in this article.
If I proceed to use my Google account by clicking on the Google image, the current browser session will be navigated to Google, where I can enter my normal login credentials, as shown below:
Once I have entered my usual Google (OpenID compliant) credentials, I will then be returned back to my previous page that I was on before I went off to the OpenID compliant web site to allow me to login, this obviously being Google in my case.
This is shown below after I have logged in using my Google credentials:
You may be able see from this that the URL is not the original page that I wanted to view, not Site/Data, and not the login page. This is what the code in this article is all about; it shows you how to work with logging in to an OpenID compliant web site and to also use Forms Authentication to manage the Authentication cookie. In the example above, we basically did this:
- Loads a page that did not require any authentication (HomeController -> Index action -> Index.aspx).
- From there, we clicked a link to a page that required authentication (SiteController -> Data action -> Site.aspx). This immediately realised we were not authenticated yet, due to the lack of a Forms Authentication cookie, and redirected the browser session to the Login.aspx page (managed by the AccountController).
- From the Login.aspx page, we have a
ReturnUrl
(which is set to ReturnUrl=/Site/Data) to the original page that we attempted to view that needed the user to be authenticated before we could return to it. - Next, we chose an OpenID provider to use; we are then redirected to the OpenID provider web site, where we log in.
- If the login process is successful, we are directed back to the original page that the user wanted to view (that's the
ReturnUrl
which Forms Authentication provides for us), that was not possible until the user was authenticated. This is achieved using a Forms Authentication cookie along with an ASP.NET MVC attribute, which we will discuss in the next section.
So How Does It All Work
Before I start to explain this, let me just say that my code relies on a third party DLL called "DotNetOpenAuth.dll" which is freely available at: http://www.dotnetopenauth.net/, where there are many .NET based demo examples and, of course, the DotNetOpenAuth.dll itself.
Specifying That a Page Needs Authorization Before It Can Be Viewed
This is perhaps the simplest part of the supplied demo code. Luckily for us, ASP.NET MVC comes with the very hand AuthoriseAttribute
, that can literally be applied to your controllers that will restrict access of callers to the controllers actions unless they are authenticated. Which in our case means, we have logged in and have a Forms Authentication cookie stored in the session.
This is what the demo code's controller looks like that requires authorization to occur before the user can view the results of calling any of the actions:
using System.Web.Mvc;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.RelyingParty;
namespace DotNetOpenIdTest.Controllers
{
[HandleError]
[Authorize]
public class SiteController : Controller
{
public ActionResult Data()
{
return View("Site");
}
}
}
Login Page
The login page is where all the OpenID providers are shown, and where the user can click one of them to be redirected to the OpenID provider's web site. As I say, I got the bulk of this page from somewhere, but just can't seem to remember where, so if you think you know where it came from, let me know. Anyway, the login page's View looks like the following:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>OpenId Demo</title>
<link rel="stylesheet" type="text/css"
media="screen" href="../../Content/openid.css" />
<script type="text/javascript" src="../../Scripts/jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="../../Scripts/jquery.openid.js"></script>
<script type="text/javascript">
$(function () { $("form.openid:eq(0)").openid(); });
</script>
</head>
<body>
<div>
<h2>
Logon Using OpenId</h2>
<p>
<%=ViewData["message"]%></p>
<form class="openid" method="post" action="/Account/Logon">
<div>
<ul class="providers">
<li class="openid" title="OpenID">
<img src="../../Content/images/openidW.png" alt="icon" />
<span><strong>http://{your-openid-url}</strong></span></li>
<li class="direct" title="Google">
<img src="../../Content/images/googleW.png" alt="icon" />
<span>https://www.google.com/accounts/o8/id</span></li>
<li class="direct" title="Yahoo">
<img src="../../Content/images/yahooW.png" alt="icon" />
<span>http://yahoo.com/</span></li>
<li class="username" title="AOL screen name">
<img src="../../Content/images/aolW.png" alt="icon" />
<span>http://openid.aol.com/<strong>username</strong></span></li>
<li class="username" title="MyOpenID user name">
<img src="../../Content/images/myopenid.png" alt="icon" />
<span>http://<strong>username</strong>.myopenid.com/</span></li>
<li class="username" title="Flickr user name">
<img src="../../Content/images/flickr.png" alt="icon" />
<span>http://flickr.com/<strong>username</strong>/</span></li>
<li class="username" title="Technorati user name">
<img src="../../Content/images/technorati.png" alt="icon" />
<span>http://technorati.com/people/technorati/
<strong>username</strong>/</span></li>
<li class="username" title="Wordpress blog name">
<img src="../../Content/images/wordpress.png" alt="icon" />
<span>http://<strong>username</strong>.wordpress.com</span></li>
<li class="username" title="Blogger blog name">
<img src="../../Content/images/blogger.png" alt="icon" />
<span>http://<strong>username</strong>.blogspot.com/</span></li>
<li class="username" title="LiveJournal blog name">
<img src="../../Content/images/livejournal.png" alt="icon" />
<span>http://<strong>username</strong>.livejournal.com</span></li>
<li class="username" title="ClaimID user name">
<img src="../../Content/images/claimid.png" alt="icon" />
<span>http://claimid.com/<strong>username</strong></span></li>
<li class="username" title="Vidoop user name">
<img src="../../Content/images/vidoop.png" alt="icon" />
<span>http://<strong>username</strong>.myvidoop.com/</span></li>
<li class="username" title="Verisign user name">
<img src="../../Content/images/verisign.png" alt="icon" />
<span>http://<strong>username</strong>.pip.verisignlabs.com/</span>
</li>
</ul>
</div>
<fieldset>
<label for="openid_username">
Enter your <span>Provider user name</span></label>
<div>
<span></span>
<input type="text" name="openid_username" /><span></span>
<input type="submit" value="Login" /></div>
</fieldset>
<fieldset>
<label for="openid_identifier">
Enter your <a class="openid_logo" href="http://openid.net">OpenID</a></label>
<div>
<input type="text" name="openid_identifier" />
<input type="submit" value="Login" /></div>
</fieldset>
</form>
</div>
</body>
</html>
Where the following jQuery based JavaScript (jquery.openid.js) is wiring up the images to the AccountController
's Logon
action. The only really important thing that happens is that the page is submitted from the following JavaScript; all the real work happens in the AccountController
's Logon
action code.
$.fn.openid = function() {
var $this = $(this);
var $usr = $this.find('input[name=openid_username]');
var $id = $this.find('input[name=openid_identifier]');
var $front = $this.find('div:has(input[name=openid_username])>span:eq(0)');
var $end = $this.find('div:has(input[name=openid_username])>span:eq(1)');
var $usrfs = $this.find('fieldset:has(input[name=openid_username])');
var $idfs = $this.find('fieldset:has(input[name=openid_identifier])');
var submitusr = function() {
if ($usr.val().length < 1) {
$usr.focus();
return false;
}
$id.val($front.text() + $usr.val() + $end.text());
return true;
};
var submitid = function() {
if ($id.val().length < 1) {
$id.focus();
return false;
}
return true;
};
var direct = function() {
var $li = $(this);
$li.parent().find('li').removeClass('highlight');
$li.addClass('highlight');
$usrfs.fadeOut();
$idfs.fadeOut();
$this.unbind('submit').submit(function() {
$id.val($this.find("li.highlight span").text());
});
$this.submit();
return false;
};
var openid = function() {
var $li = $(this);
$li.parent().find('li').removeClass('highlight');
$li.addClass('highlight');
$usrfs.hide();
$idfs.show();
$id.focus();
$this.unbind('submit').submit(submitid);
return false;
};
var username = function() {
var $li = $(this);
$li.parent().find('li').removeClass('highlight');
$li.addClass('highlight');
$idfs.hide();
$usrfs.show();
$this.find('label[for=openid_username] span').text($li.attr("title"));
$front.text($li.find("span").text().split("username")[0]);
$end.text("").text($li.find("span").text().split("username")[1]);
$id.focus();
$this.unbind('submit').submit(submitusr);
return false;
};
$this.find('li.direct').click(direct);
$this.find('li.openid').click(openid);
$this.find('li.username').click(username);
$id.keypress(function(e) {
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
return submitid();
}
});
$usr.keypress(function(e) {
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
return submitusr();
}
});
$this.find('li span').hide();
$this.find('li').css('line-height', 0).css('cursor', 'pointer');
$this.find('li:eq(0)').click();
return this;
};
Login Process
The login process is conducted in the AccountController
, and works as follows:
- Page that needs authorization is requested.
- Redirected to GET
AccountController
Logon
action, if the user is not authenticated. - Logon view is shown, which has all the OpenID provider links on them, where the HTML form is set to POST to the
AccountController
POST Logon
action. - User picks an OpenID provider, and clicks it, which calls the JavaScript, which really just results in the OpenID provider string being stored and a POST request being made to the
AccountController
Logon
action. - The
AccountController
s POST Logon
action does two things:
- It adds on a
ClaimRequest
, where it asks the OpenID provider to include Email/FullName in the data that will be included with a successful response from the OpenID provider. - Then redirects the OpenID provider web site (via magic of DotNetOpenAuth.dll), where the user enters their details.
- If the user enters valid credentials at the OpenID provider's web site, they are redirected to the default
Logon
action on AccountController
(via magic of DotNetOpenAuth.dll), at which point, it will examine the result from the OpenID provider IAuthenticationResponse response
, which is available by calling the OpenIdRelyingParty
type's GetResponse()
method. If the response is found to be AuthenticationStatus.Authenticated
, the user is deemed validated, and then more details about the user can be requested from the OpenID provider's response, which is accomplished using response.GetUntrustedExtension<ClaimsResponse>
/ response.GetExtension<ClaimsResponse>
, where ClaimsResponse
is the response that matches the ClaimsResponse
that we asked the OpenID provider to include in the AccountController
's POST Logon
action in Step 5. We can then use the ClaimsResponse
to obtain the user's Email and FullName from the OpenID provider's ClaimsResponse
, which is stored in a small data construct that I have created, that is called UserData
, which looks like this:
public class UserData
{
public string Email { get; set; }
public string FullName { get; set; }
public override string ToString()
{
return String.Format("{0}-{1}", Email, FullName);
}
}
- The last thing that happens is that there is a
FormsAuthenticationTicket/HttpCookie
created for the OpenID authenticated UserData
object.
Here is all the code for AccountController
:
using System.Web.Mvc;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.RelyingParty;
using System.Web.Security;
using System.Linq;
using System;
using System.Text.RegularExpressions;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using System.Web;
namespace DotNetOpenIdTest.Controllers
{
public class UserData
{
public string Email { get; set; }
public string FullName { get; set; }
public override string ToString()
{
return String.Format("{0}-{1}", Email, FullName);
}
}
public class AccountController : Controller
{
#region Actions
public ActionResult LogOn()
{
ViewData["message"] = "You are not logged in";
OpenIdRelyingParty openid = new OpenIdRelyingParty();
IAuthenticationResponse response = openid.GetResponse();
if (Request.Params["ReturnUrl"] != null)
Session["ReturnUrl"] = Request.Params["ReturnUrl"];
if (response != null &&
response.Status == AuthenticationStatus.Authenticated)
{
var claimUntrusted =
response.GetUntrustedExtension<ClaimsResponse>();
var claim = response.GetExtension<ClaimsResponse>();
UserData userData = null;
if (claim != null)
{
userData = new UserData();
userData.Email = claim.Email;
userData.FullName = claim.FullName;
}
if (claimUntrusted != null && userData == null)
{
userData = new UserData();
userData.Email = claimUntrusted.Email;
userData.FullName = claimUntrusted.FullName;
}
IssueAuthTicket(userData, true);
Session["ClaimedIdentifierMessage"] = response.ClaimedIdentifier;
if (Session["ReturnUrl"] != null)
{
string url = Session["ReturnUrl"].ToString();
return new RedirectResult(url);
}
else
throw new InvalidOperationException("There is no ReturnUrl");
}
return View("LogOn");
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string openid_identifier)
{
var openid = new OpenIdRelyingParty();
IAuthenticationRequest request =
openid.CreateRequest(Identifier.Parse(openid_identifier));
var fields = new ClaimsRequest();
fields.Email = DemandLevel.Require;
fields.FullName = DemandLevel.Require;
request.AddExtension(fields);
return request.RedirectingResponse.AsActionResult();
}
public ActionResult LogOff()
{
Session.RemoveAll();
Session.Clear();
Session.Abandon();
Response.Cookies.Remove("AUTHCOOKIE");
FormsAuthentication.SignOut();
return View("LogOff");
}
#endregion
#region Private Methods
private void IssueAuthTicket(UserData userData, bool rememberMe)
{
FormsAuthenticationTicket ticket =
new FormsAuthenticationTicket(1, userData.Email,
DateTime.Now, DateTime.Now.AddDays(10),
rememberMe, userData.ToString());
string ticketString = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie =
new HttpCookie(FormsAuthentication.FormsCookieName, ticketString);
if (rememberMe)
cookie.Expires = DateTime.Now.AddDays(10);
HttpContext.Response.Cookies.Add(cookie);
}
#endregion
}
}
Web.Config
I found that I had to add a dotNetOpenAuth
config section in Web.Config for the OpenId.Dll to work correctly with some providers (Google). Oh, I am also showing that Forms Authentication is enabled too.
="1.0"
<configuration>
<configSections>
<section name="dotNetOpenAuth"
type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection"
requirePermission="false"
allowLocation="true"/>
</configSections>
.......
.......
.......
.......
.......
<dotNetOpenAuth>
<openid>
<relyingParty>
<behaviors>
<add type="DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform,
DotNetOpenAuth" />
</behaviors>
</relyingParty>
</openid>
</dotNetOpenAuth>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880"/>
</authentication>
.......
.......
.......
.......
</configuration>
That's It
Anyway, that is all I really wanted to say. I know web development is not my normal arena, so I have more than likely made some school boy errors. If that is the case, please tell me as I am just about to put this into my private out of hours ASP MVC project. So any of you advanced/seasoned ASP MVC devs reading this, see anything wrong, please let me know.
Likewise, if you liked the article, it would be nice to hear about that, by way of a comment/vote.