Creating an OpenId Relying Party and Provider In .NET

Preamble/Motivation

I'm working on an MVC web app that will soon be moving to Azure. The big roadblock to moving the application is that it uses LDAP to communicate accross our local intranet to our Active Directory domain controller to authenticate users. The de facto method of authenticating users in a web app in Azure against an in-house Active Directory is to use Federated Services. Federated Services requires significant setup by both the developer (to integrate) and IT, to configure on and expose the domain controller.

In order to avoid Federated Services (for the time being), I thought that I could create a middleman application that would remain inside the intranet that the cloud application would redirect users to. The middleman app would then authenticate the users against the Active Directory (with Windows Authentication or Forms Authentication+LDAP(s)), and return then to my application. This, however, sounds an awful lot like OpenId.

An important sidenote: OAuth != OpenId. I was using them incorrectly and sometimes interchangibly, and it's a pretty good idea when getting into either to know their uses and differences.

So what I decided to do was support OpenId authentication in my web app, which is nice, because it opens up the ability to allow users to authenticate with Google, Yahoo, StackExchange, Blogger, Wordpress and more, just by enabling them (more on "enabling" other providers later). Then, in order to support the OpenId authentication against our Active Directory, I'd create a middleman application that serves as an OpenId provider, doing the authentication not against a user database like providers usually do, but against the Active Directory.

Architecture

Taking inspiration from this old example, but with modification to eliminate an optional step (Bob, in that link), and to update with my architecture:

OpenId Flow

There are actually a couple steps between steps 7 and 8 regarding the authentication between Alice and Bob, but those can differ, and are essentially just a login page and submission, that I wanted to omit to simplify the diagram. In my implementation, between 7 and 8, Bob redirects Alice to another Action (url) that is an [Authorized] action. Because this application is configured with Windows Authentication, this causes IIS to send an HTTP 401 Unauthorized, which challenges the user to provide credentials (as it happens, if the user has this domain set up as an intranet site, it can detect their windows credentials and do this completely invisible to the user).

Implementation

There's basically just one implementation of OpenId (and OAuth) in .NET to which I could find any references online. DotNetOpenAuth even shiped with an MVC template from Microsoft at one point, for creating a web app template to authenticate against Google/Yahoo/etc. I didn't use this template, because I already had a web application that's pretty complete. I also want to support multiple modes of authentication via configuration, so I need to have actions in my login controller for forms authentication in addition to the new OpenId actions. Note that in my application, I use the mode in which the Relying Party doesn't care about the user's Identity Url, but just sends the user to a generic endpoint on the Provider which allows any user to authenticate. This StackOverflow Question encapsulates that difference pretty well.

OpenId Relying Party (Cloud Application)

On my relying party application, I installed the appropriate Nuget packages for DotNetOpenAuth (core, relying party, and their dependencies).
With OpenId, a user can theoretically provide the Url of any OpenId provider's endpoint. I didn't want to enable this for my app, but could, at any time. Instead, I provided just a few standard options, as buttons. If the user clicks the button for Google, Yahoo, or Active Directory, it kicks off the OpenId process for the appopriate provider, by posting the appropriate Url to an action (Unauthorized) on my Login Controller.

This takes us through steps 1-3 in the diagram above. Steve presents a login page when Alice visits, she decides to use Bob as her OpenId provider, and tells Steve.

This is the action to initiate the connection. Note that my controller has a static OpenIdRelyingParty action, which will be used when the Provider responds.

So this takes us through steps 4-7 in the diagram. Steve checks that bob is a provider and Bob responds with proof (an XRDS doucument). That happens in line 18: openId.CreateRequest(<Bob's Endpoint Url>). Then, Steve says, "I need some info from Bob when he proves it's you, in this case, just your email. Now go prove to Bob who you are."

Now I'm going to skip over what happens between Bob and Alice, which will be covered below, and assume that Bob identified Alice in the affirmative and sent her back to Steve. This is the action where Alice will be redirected back to by Bob, to prove to Steve who she is. There's a whole bunch of information in the URL, which is parsed out by openId.GetResponse().

Steve makes sure the response is valid, checks it's status, and responds appropriately. If the authentication worked, Steve attempts to extract an email address from the response. Then, Steve uses the email (or not) that he got, to call LoginUserWithEmail which is a completely app-specific function. In my case, it makes a call out to my database's Users table to verify that I have an active user with the returned email, and if so, Create a FormsAuthentication cookie and redirect the user to the homepage. If it fails, it tells the user that we don't have an account for them. If you wanted to, you could prompt the user to create an account at that point. This takes us through the end of the diagram's steps!

OpenId Provider (Middleman to Active Directory)

On the middleman, I also installed Nuget packages for DotNetOpenAuth (core, provider, and dependencies). Using examples from the (long out of date) DotNetOpenAuth/Samples/OpenIdProviderMVC project, I created an XRDS view that can be shared for the Server Endpoint discovery and Signon document.

Then I created my endpoint, which returns either the Server document or Signon document depending on the accept headers. So when the RelyingParty calls openId.CreateRequest(), DotNetOpenAuth creates a request with proper headers and sends it to my Id action, then detects the returned XRDS document and makes sure there's a valid endpoint there.

Then, I created my Provider endpoint and Authorizing functions. The URL to the Provider action is returned in the XRDS document above, so that's the URL that Bob gives to Steve to tell Alice to go to. When Alice goes to that URL, Bob detects that it's a real OpenId request coming in, then processes the request. What happens in my app is that we call an action called TriggerWindowsAuthorization which is just an Action that is Authorized, causing IIS to return a 401 challenege.

Gotchas

I had a number of hangups that were pretty important lessons, once I figured them out.

The first is something I still don't fully understand, but apparently, "IIS blocks ASP.NET web apps from performing HTTP GETs on themselves." This is a problem because my apps are both running on the local IIS server. Two things seemed to help: 1. Instead of addressing the Provider and Relying Party both as Localhost, I used one on Localhost, and the other at my PC's name ..com, and 2. Use one on IIS Express so it runs on a different port. The effect of this was that my Relying Party was unable to detect the Provider's endpoint, because the request wasn't actually getting through.

The second lesson is to make sure all appropriate endpoints are properly authorized or not. This means that the two actions above (in my Relying Party's Login controller) and the Endpoint and Provider actions (in my Provider) are all unauthorized, and the action that checks the user's identity (in the Provider) is authorized, to trigger the 401 challenge. For the provider, I achieved this by making the whole controller Authorized, and putting <Location/> elements in the web.config that allow unauthorized users to get to those paths (actions).