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.

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.

Using 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.

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:

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:

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.

Login Process

The login process is conducted in the AccountController, and works as follows:

Page that needs authorization is requested.

Redirected to GET AccountControllerLogon 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 AccountControllerLogon action.

The AccountControllers 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:

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.

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.