• Nederlands (Nederland)
    • English (United States)
    • العربية (مصر)
    • Deutsch (Deutschland)
    • Español (España, alfabetización internacional)
    • français (France)
    • हिंदी (भारत)
    • italiano (Italia)
    • 日本語 (日本)
    • 한국어 (대한민국)
    • polski (Polska)
    • русский (Россия)
    • ไทย (ไทย)
    • Türkçe (Türkiye)
    • Tiếng Việt (Việt Nam)
    • 中文(中华人民共和国)
    • 中文(香港特別行政區)
  • Inloggen
  • Registreren
DotNetAge - Mvc & jQuery CMS
Zijbalk verbergen

Integrating OpenID in an ASP.NET MVC Application using DotNetOpenAuth


Via Integrating OpenID in an ASP.NET MVC Application using DotNetOpenAuth – Rick Strahl’s Web Log

The #1 request I got on the CodePaste.net site - which provides a quick and easy way to publish and share code snippets publicly - has been to implement OpenId for authentication rather than the custom username/password login that’s been in use up until now. OpenId is a centralized login/authentication mechanism which handles authentication through one or more centralized OpenId providers. These providers live on the Web and are accessed through a forwarding and callback mechanism – you log in at the provider’s site and are then returned to the original starting Url with an authorized user token that uniquely identifies that user to the original site.

What is OpenId?

OpenId is a single sign-on scheme – the idea is that you keep your login and profile information in one place so that you don’t have to login at every Web location and create new user credentials on each site. The  idea of a single sign on isn’t new of course – lots of these things have been around over the years with the most remembered probably being Microsoft’s Passport/Windows Live ID (not that anybody likes Windows Live Id). OpenId uses a similar concept, but the difference is that there are many providers and  sites that are implementing OpenId Providers so that you can use their log on credentials that you are already using to log into other sites. Chances are that you already have a sign on on one of the OpenId providers rather than just one provider that controls access. You can essentially choose where you log in from.Some of sites that are OpenId providers now are Google, Yahoo, Microsoft Windows Live, AOL, Facebook, Flickr and many many more. Chances are you’re signed up on one of these services and you can use their logons to log in to other sites that support OpenId. The rush to OpenId is fairly fresh so some of these work better than others. The ones I’ve been using are MyOpenId, Google and Yahoo of which MyOpenId works best if you want to share profile information.

To be honest I hadn’t warmed up to OpenId until recently. In fact I had to be convinced by user requests on the CodePaste.net site to take a look at integration. Now that it’s there I think it is a pretty sweet way to go although I think the concept is lost on the average user until OpenId becomes more wide spread. It also didn’t help that the original implementations and providers were pretty crappy and the process on most sites to sign up actually was really slow and clunky. That has changed a bit with better interfaces that are faster and many more sites are using OpenId and many more providers exist.

Another pain point is that supporting standards for retrieving profile information like Simple Registration (SREG) is not widely supported. Providers are returning information in a variety of different formats and most don’t return profile information at all whether you approve or not which is really lame. You presumably own the data on any service you’ve given data to so why the fuss of sharing it if you approve of sharing? Anyway, don’t count on getting full contact information from OpenId for a while. Some providers will give some information, others won’t give any – so your application has to be able to handle both scenarios.

OpenId Development

For development with .NET there are a few libraries out there that handle the communications details for you. While communications are handled using simple URLs for routing and callbacks the data encoded into these URLs must be secure so that they can’t be spoofed and this process is not exactly trivial.

For the site I chose DotNetOpenAuth – after doing a little bit of research it seemed to me that this was the most active development project that is also most feature rich. DotNetOpenAuth does much more than OpenId it also supports oAuth (a non-interactive service based counterpart to OpenId) which is another thing on my list to implement for the API portion of the site. DotNetOpenAuth  is fairly comprehensive and includes support specifically for Web applications – both MVC and Web Forms with controls and helpers provided as well as MVC specific action methods to handle redirection and return routing which is a nice touch.

The tool makes the actual routing and handling of the results fairly easy, but the integration process nevertheless is not completely trivial. The reason for this is that most applications will need to handle both logins as well as attaching an OpenId account to an existing user profile of some sort and this can get tricky because of the HTTP GET based callback interface. The actual OpenId calls and callbacks are quite simple – the actual user interface and hookup routines though ended up taking me a lot longer than I had expected.

Walking through OpenId Authentication

Alright lets take a look and see how this works on the CodePaste.net site. As I mentioned there are two parts – the basic login validation which is very straight forward and simple so I’ll start there.

The login form displays both logins for OpenId and the traditional username and password:

Figure 1 – Logging in with OpenId

I needed to leave the old login in form place so existing users can log in and attach their existing accounts to OpenId. Also some folks may just not want to go the OpenId route. So both are supported on this form.

OpenId validation starts by providing a OpenId Url. The Url depends on the provider and you can pick up that Url from your provider’s OpenId information page – here is a list of a few common ones from the OpenId site. I also provided a couple of icon buttons for the most likely culprits – MyOpenId, Google and Yahoo. Any others need to be typed in. Between Google and Yahoo you probably have an account. There’s also myOpenId which is one of the early providers which is popular, and with it you use a url like http://rickstrahl.myopenid.com to log on. The Yahoo () and Google () Urls are the same for everybody, so those links can actually be auto-fired directly by the click on the icon, while the myOpenId entry requires entering the name first and manually clicking the button.

Once the Login button is clicked (or automatically fired) the process takes you to the provider’s site. For example if you use Google, you’ll see a Google Login screen if you aren’t already signed into Google and you are asked to login. The page will tell you the source site that’s requesting the login and possibly which profile information is requested. Once you log in you are then redirected hence you came – back to the same url where you came from by default. This can be overridden, but in an MVC application you’ll most likely will want to come back to the same URL to redisplay the login (or registration) data with some indication that you are now logged in.

If you are already logged into the OpenId provider (which is usually the case) the login operation is completely transparent – the page is re-routed to the OpenId provider site and immediately returns back to your site at a Url that is specifed. This usually happens pretty fast through a bunch of redirects – from your site to the OpenId provider and fro the OpenId provider back to your site – so other than some extra delay time you don’t even see anything happening. It just looks like you’re going directly to your next page in the app.

When the page returns after a login it includes encoded authentication information on the querystring that becomes available to the request that handles the OpenId login. Specifially DotNetOpenAuth can read the authentication token and any profile information that was returned if any was requested and returned.

Once logged in my profile display then reflects the account association like this:

Loggedin_59e1e5e6-286c-433c-9f1e-2b7d5717aaa9

Figure 2 – Logged in with OpenId and viewing the Profile

The application stores the user’s open id as part of the profile and so it’s easy to display this information. The OpenId relationship can be unlinked and the account can then be assigned to another OpenId or a username and password has to be provided if no OpenId is available. As I mentioned the actual OpenId authentication process is pretty simple but some of the issues surrounding it like attaching and disconnecting accounts from OpenId is a little more complicated – I’ll talk about that below.

If I go to the profile page when the account is not associated with an OpenId the page has an option to login and effectively associate the account with the new OpenId:

Registration_571db2fa-978d-4adc-aa68-1bf427ac2dc7

The same page is also used for new registrations which is useful in order to provide the name and email actually from the OpenId provider (as supported – note that google and yahoo do not provide this data in a format that DotnetOpenAuth extracts automatically).

How to implement OpenId in an MVC Application

Please keep in mind that what I describe is just one way you can do this in this case using DotNetOpenAuth since it provides a host of different ways to implement OpenId. There are high level helpers and for Web Forms there is a server control that you can use. In this example I handle this process manually so you can see the flow of the requests. The process is pretty simple although there’s a fair bit of code (most of it boilerplate) that is involved.

I’ll start with the sign in form because that’s simpler. This relates to figure 1 in the image sequence above. The OpenId login area of the page is wrapped into its own HTML form that kicks off the processing. The layout for this block looks like this:

<% using (Html.BeginForm(new { Controller="Account",Action="OpenIdLogon" }))
       { %>         
         <fieldset >                             
         <div class="containercontent" >
             <div class="labelheaderblock">Log on using OpenId:div>
             <input id="openid_identifier" name="openid_identifier"/>             
             <input id="btnOpenIdLogin" type="submit" value="Login"/>        
             <img id="imgOpenIdLoginProgress" src="<%= ResolveUrl("~/css/images/loading_small.gif") %>" style="display:none" />                 
             
            <div id="divOpenIdIcons">
                 <img src="<%= ResolveUrl("~/images/openid-icon.png") %>" onclick="openIdUrl('openid')" title="myopenid.com" class="hoverbutton" />
                 <img src="<%= ResolveUrl("~/images/google-icon.png") %>" onclick="openIdUrl('google')" title="Google" class="hoverbutton"/>
                 <img src="<%= ResolveUrl("~/images/yahoo-icon.png") %>" onclick="openIdUrl('yahoo')" title="Yahoo" class="hoverbutton" />
             div>     
         div>
         fieldset>
<% } %> 

The image icons fire a small JavaScript routine that creates the OpenId urls that are injected into the text box:

function openIdUrl(site)
{
    var value = "";
    var autoClick = false;
    
    if (site == "openid") {
        value = ".myopenid.com"
    }
    else if (site == "google") {
        value = "https://www.google.com/accounts/o8/id";
        autoClick = true;
    }
    else if (site == "yahoo") {
        value = "http://yahoo.com/"
        autoClick = true;
    }
        
    if (value) {
        var jText = $("#openid_identifier");
       jText.val(value)
            .focus();
       if (autoClick)
           $("#btnOpenIdLogin").trigger("click");
    }       
}

You can add to this with other URLs and icons as you see fit. I stuck with those three because they are the most common that people will have accounts on but that may change. All others OpenId Urls can be typed in by hand (which is not so smooth).

When the Login button is clicked and the request is submitted it fires into an OpenIdLogOn action method of the controller. The controller has to accept both POST and GET operations. POST is from the initial button click, the GET will occur when the OpenId provider fires back the response and provides the data via the query string.

As mentioned I’m using DotNetOpenAuth here which is a single large DLL you can drop into your BIN folder. It handles all the heavy lifting of creating the redirect link and cracking the response query string extracting the authentication token.

The process involves two steps: Making the original request to the OpenId provider (which redirects) and handling the response that comes back. Here’s the code (which is pretty close to the sample code provided in one of the demos):

[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Get), ValidateInput(false)]
public ActionResult OpenIdLogOn(string returnUrl)
{
    var openid = new OpenIdRelyingParty();
    var response = openid.GetResponse();    if (response == null)  // Initial operation
    {
        // Step 1 - Send the request to the OpenId provider server
        Identifier id;
        if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
        {
            try
            {
                var req = openid.CreateRequest(Request.Form["openid_identifier"]);
                return req.RedirectingResponse.AsActionResult();
            }
            catch (ProtocolException ex)
            {
                // display error by showing original LogOn view
                this.ErrorDisplay.ShowError("Unable to authenticate: " + ex.Message);
                return View("Logon",this.ViewModel);
            }
        }
        else
        {
            // display error by showing original LogOn view
            this.ErrorDisplay.ShowError("Invalid identifier");
            return View("LogOn",this.ViewModel);
        }
    }
    else  // OpenId redirection callback
    {
        // Step 2: OpenID Provider sending assertion response
        switch (response.Status)
        {
            case AuthenticationStatus.Authenticated:
                string identifier = response.ClaimedIdentifier;

                // OpenId lookup fails - Id doesn't exist for login - login first
                if (busUser.ValidateUserOpenIdAndLoad(identifier) == null)
                {
                    this.ErrorDisplay.HtmlEncodeMessage = false;
                    this.ErrorDisplay.ShowError(busUser.ErrorMessage +
                            "Please register to create a new account or associate an existing account with your OpenId");

                    return View("LogOn", this.ViewModel);
                }

                // Capture user information for AuthTicket
                // and issue Forms Auth token
                UserState userState = new UserState()
                {
                    Email = busUser.Entity.Email,
                    Name = busUser.Entity.Name,
                    UserId = busUser.Entity.Id,
                    IsAdmin = busUser.Entity.IsAdmin
                };
                this.IssueAuthTicket(userState, true);

                if (!string.IsNullOrEmpty(returnUrl))
                    return Redirect(returnUrl);

                return Redirect("~/new");

            case AuthenticationStatus.Canceled:
                this.ErrorDisplay.ShowMessage("Canceled at provider");
                return View("LogOn", this.ViewModel);
            case AuthenticationStatus.Failed:
                this.ErrorDisplay.ShowError(response.Exception.Message);
                return View("LogOn", this.ViewModel);
        }
    }
    return new EmptyResult();
}

The code uses the OpenIdRelyingParty class to handle the processing of the login. The first step occurs when the button is clicked and there’s no provider response to process. This code:

var req = openid.CreateRequest(Request.Form["openid_identifier"]);
return req.RedirectingResponse.AsActionResult();

fires off the the request. The openid_identifier is the OpenId Url the user entered into the form and it’s passed to DotNetOpenAuth to start formulating a request for authentication at that Url.

The code above starts the redirection sequence going to the provider and back to the current page  - since I didn’t specify another Url to return to by default the request returns to the current page’s url.

As mentioned I’m using MVC so and as you can see DotNetOpenAuth conveniently includes an AsActionResult() method that creates redirect response in a proper MVC style response which is very thoughtful for consistency. You can also retrieve the URL directly and fire it on your own although I can’t see why you would. If you’re using WebForms or a handler there are other methods on RedirectingResponse to fire the redirects for you.

Next the browser is redirected to the OpenId provider where you either are asked to log in or if you already are logged in you are authenticated and immediately returned to the current page. When the request returns it is now a GET operation and we start back out at the top of the same Controller action.

This time around:

var response = openid.GetResponse();

will not be null – the return URL includes a bunch of new encoded querystring values. GetResponse() looks for those and based on that creates the response instance and the code can branch into the greater if block. The request will either be authenticated, canceled or failed. Failure can be any number of things such as invalid login information, or timing out or some sort of tampering with the URL. Both cancel and failure operations are handled by simply redisplaying the current view with an error message.

If authentication succeeded we can retrieve a unique identifier that identifies the user’s provider choice and we’re guaranteed at this point that this user was authenticated through OpenId.  Yay!

Although you now know the user is authenticated ASP.NET knows nothing about this authentication yet, so the next step is to set a FormsAuthentication ticket to authenticate the user to the application. This process is no different than doing a manual login in any other ASP.NET application with Forms Authentication.

In CodePaste.net I store a user record in the ticket so I have some basic user information available to my application on each hit without having to use session storage or retrieving this info on every request from the database. The code does this:

// Capture user information for AuthTicket
// and issue Forms Auth token
UserState userState = new UserState()
{
    Email = busUser.Entity.Email,
    Name = busUser.Entity.Name,
    UserId = busUser.Entity.Id,
    IsAdmin = busUser.Entity.IsAdmin
};
this.IssueAuthTicket(userState, true);where IssueAuthTicket() looks like this doing standard Forms Authentication ticket assignment:

/// 
/// Issues an authentication issue from a userState instance
/// 
/// 
/// 
private void IssueAuthTicket(UserState userState,bool rememberMe)
{
    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, userState.UserId,
                                                         DateTime.Now, DateTime.Now.AddDays(10),
                                                         rememberMe, userState.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);
}

Once the auth ticket’s been issued the application simply redirects to another page in the application (the new snippet page in this case).

BTW just for clarfication, the userState object contains logic to easily persist to and from string so this object is easily retrieved at the beginning of a request and stored on updates. In my base controller I do:

protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
    base.Initialize(requestContext);

    // Grab the user's login information from FormsAuth
    UserState userState = new UserState();
    if (this.User.Identity != null && this.User.Identity is FormsIdentity)
        this.UserState.FromString(((FormsIdentity)this.User.Identity).Ticket.UserData);

    // have to explicitly add this so Master can see untyped value
    this.ViewData["UserState"] = this.UserState;
    this.ViewData["ErrorDisplay"] = this.ErrorDisplay;
}

The process to authenticate using OpenId is pretty straight forward. No surprises here, really. All we need to have is some conditional logic to route for redirect initially and pick up the result when the request comes back and handle the authentication and creation of a new FormsAuthentication ticket. When the request returns and is marked as authenticated you can safely assume the user is valid. Because of the message encryption and timed security tokens the data is safe and not hijackable.

Registration – A little more complex

The log in process is straight forward primarily because the request doesn’t need to track data that is already on the page. You log in and you move on to another page. However in a log in form (Figure 3) there may be additional pieces of information that you need to track. More importantly you typically have some state associated with the registration page – like which user (or a new user) is currently being displayed and that information has to be carried forward when the Open Id request returns from the provider. So the code to handle this is a little longer.

The HTML layout in Figure 3 is nearly identical to the HTML layout on the login form except there’s some conditional logic to display the input box or the checkbox and message that identifies the current OpenId association.

<% using (Html.BeginForm(new { Controller = "Account", Action = "OpenIdRegistrationLogOn" })){ %>         
         <fieldset >                             
         <div class="containercontent" style="padding: 10px 20px;" >
             
             
            <% if( string.IsNullOrEmpty(Model.busUser.Entity.OpenId) ) { %>
                 <label for="openid_identifier" class="labelheaderblock">Enter an OpenId Url:label>
                 <input id="openid_identifier" size="40" name="openid_identifier"/>             
                 <input id="btnOpenIdLogin" type="submit" value="Login"/> 
                 <img id="imgOpenIdLoginProgress" src="<%= ResolveUrl("~/css/images/loading_small.gif") %>" style="display:none" />                 
                 
                 <div id="divOpenIdIcons">
                     <img src="<%= ResolveUrl("~/images/openid-icon.png") %>" onclick="openIdUrl('openid')" title="myopenid.com" class="hoverbutton" />
                     <img src="<%= ResolveUrl("~/images/google-icon.png") %>" onclick="openIdUrl('google')" title="Google" class="hoverbutton"/>
                     <img src="<%= ResolveUrl("~/images/yahoo-icon.png") %>" onclick="openIdUrl('yahoo')" title="Yahoo" class="hoverbutton" />
                 div>
                             
             <% } else { %>    
                 <label for="openid_identifier" class="labelheaderblock">OpenId Associationlabel>
                 <img src="<%= ResolveUrl("~/css/images/greencheck.gif") %>" /> This user account is linked to <b><%= Html.Encode(Model.busUser.Entity.OpenId) %>b>
                 <input id="btnOpenIdUnlink" name="btnOpenIdUnlink" type="submit" value="Unlink" />                     
             <% } %>

             <%= Html.Hidden("Id2", this.Model.busUser.Entity.Id) %>
         div>
         fieldset>
    <% } %>

Notice that here we need to keep track of the active User if any. Since you can attach/detach from an existing profile we do have to know which user we’re dealing with. This sounds easy enough but remember that you can’t use plain POST data for this because the page goes off the OpenId provider and comes back with a GET that just holds the result values. This means to redisplay the page the entire page has to be re-rendered based on the model. To do this we’ll need to keep track of the user id for the request sequence.

This base process is very  similar to the log in process and uses the same two stages of request sending and return. First capture the value from a hidden form variable and then store it in the Session object to pass between the two requests. Here’s what the open id attachment/detachment code looks like:

[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Get), ValidateInput(false)]
 public ActionResult OpenIdRegistrationLogOn(FormCollection formVars)
 {
     return OpenIdRegistrationLogOn(null,true);
 }
 
 private ActionResult OpenIdRegistrationLogOn(IAuthenticationResponse response, bool reserved )
 {
     this.ViewData["IsNew"] = true;

     var openid = new OpenIdRelyingParty();
    
     if (response == null)
         response = openid.GetResponse();
     
     if (response == null)
     {
         string userId = Request.Form["Id2"];  // have to track the user’s id

         // Check for unlink operation
         if (!string.IsNullOrEmpty(Request.Form["btnOpenIdUnlink"]))
         {
             if (busUser.Load(userId) == null)
             {
                 this.ErrorDisplay.ShowError("Couldn't find associated User: " + busUser.ErrorMessage);
                 return RedirectToAction("Register", new { id=userId });
             }
             busUser.Entity.OpenId = "";
             busUser.Save();
             return RedirectToAction("Register", new { id = userId });
         }

         Identifier id;
         string openIdIdentifier = Request.Form["openid_identifier"];
         if (Identifier.TryParse(openIdIdentifier, out id))
         {
             try
             {
                 // We need to know which user we are working with 
                 // and so we pass the id thru session – (is there a better way?) 


                 Session["userId"] = userId;

                 var req = openid.CreateRequest(id);
                 
                 var fields = new ClaimsRequest();                       
                 fields.Email = DemandLevel.Request;
                 fields.FullName = DemandLevel.Request;                        
                 req.AddExtension(fields);

                 return req.RedirectingResponse.AsActionResult();
             }
             catch (ProtocolException ex)
             {
                 this.ErrorDisplay.ShowError("Unable to authenticate: " + ex.Message);
                 this.busUser.NewEntity();
                 return View("Register",this.ViewModel);
             }
         }
         else
         {
             ViewData["Message"] = "Invalid identifier";
             return View("Login");
         }
     }
     else
     {
         // Reestablish the user we’re dealing with

         string userId = Session["userId"] as string;
         if (string.IsNullOrEmpty(userId))
             ViewData["IsNew"] = true;
         else
             ViewData["IsNew"] = false;

         // Stage 3: OpenID Provider sending assertion response
         switch (response.Status)
         {
             case AuthenticationStatus.Authenticated:
                 var claim = response.GetExtension();
                 string email = null, fullname= null;
                 if (claim != null)
                 {
                     email = claim.Email;
                     fullname = claim.FullName;
                 }
                 string identifier = response.ClaimedIdentifier;

                 var user = busUser.Load(userId);
                 if (user == null)
                 {
                     user = busUser.ValidateUserOpenIdAndLoad(identifier);
                     if (user == null)
                         user = busUser.NewEntity();
                 }
                 
                 // associate openid with the user account
                 busUser.Entity.OpenId = identifier;

                 if (!string.IsNullOrEmpty(email) && string.IsNullOrEmpty(busUser.Entity.Email))
                     busUser.Entity.Email = email;

                 if (!string.IsNullOrEmpty(fullname) && string.IsNullOrEmpty(busUser.Entity.Name))
                     busUser.Entity.Name = fullname;

                 if (string.IsNullOrEmpty(busUser.Entity.Name))
                 {
                     string host = StringUtils.ExtractString(response.FriendlyIdentifierForDisplay, ".", ".com");
                     if (!string.IsNullOrEmpty(host))
                         host = " (" + host + ")";
                    
                     busUser.Entity.Name = "Unknown" + host;

                     this.ErrorDisplay.DisplayErrors.Add("Please enter a user name", "Name");
                 }

                 if (!busUser.Validate())
                 {
                     busUser.Entity.OpenId = "";
                     this.ErrorDisplay.ShowError(busUser.ErrorMessage);
                     return View("Register",this.ViewModel);
                 }
                 if (!busUser.Save())
                 {
                     busUser.Entity.OpenId = "";
                     this.ErrorDisplay.ShowError(busUser.ErrorMessage);
                     return View("Register", this.ViewModel);
                 }

                 UserState userState = new UserState()
                 {
                     Name = busUser.Entity.Name,
                     Email = busUser.Entity.Name,
                     UserId = busUser.Entity.Id,
                     IsAdmin = busUser.Entity.IsAdmin
                 };
                 this.IssueAuthTicket(userState, true);

                 // and reload the page with the saved data
                 return this.RedirectToAction("Register", new { id=busUser.Entity.Id } );
             case AuthenticationStatus.Canceled:
                 this.busUser.NewEntity(true);
                 this.ErrorDisplay.ShowMessage("Canceled at provider");
                 return View("Register", this.ViewModel);
             case AuthenticationStatus.Failed:
                 this.busUser.NewEntity(true);
                 this.ErrorDisplay.ShowError(response.Exception.Message);
                 return View("Register", this.ViewModel);
         }
     }
     return new EmptyResult();
 }

There are two methods here – one as an actual endpoint and another that can be called with an existing response passed to it – this is useful to automatically force a login that doesn’t exist directly into the registration form via code. A DotNetAuth response can only be parsed once or else it’s considered invalid and so passing in an existing response was a requirement to handle this sort of ‘manual’ routing.  For the simple callback scenario you can just use a simple Controller endpoint method.

As in the last example the OpenIdRelyingParty is used to handle the actual request semantics of sending the redirect and receiving the data. This part of the process really doesn’t change. However, how you deal with the data received and abort scenarios adds a significant amount of additional code compared to the previous login only example.

If there’s no OpenId response, the code first checks for unlink requests. For this it needs the id to load an instance of the business entity and update it by removing the OpenId from it. The page is then redisplayed again using the ID as the key.

Otherwise the request is sent off to the OpenId provider. The provider then returns and again the OpenId response will now be set. On success the code tries to retrieve the ClaimsRequest values. Claimsrequest is sent with the initial request to ask the server to return values from the user’s profile. For CodePaste.net I’m interested in full name and email as optional results. Note that some OpenId only providers use a common format called SREG to return values. myOpenDns does as do several other of the dedicated services. However, this standards is only sketchily supported by the big providers. Google for example requires that you Demand data and then will only return the email address (which seems ironic given that’s the most private item in the profile). Yahoo doesn’t return anything – according to their OpenId page they only share profile information with certain sites they deem appropriate. SREG however is gaining momentum so it’s likely that all providers will evenutally support SREG.

In DotnetOpenAuth the request for ‘claim data’ is handled like this:

// Request additional Profile 
var req = openid.CreateRequest(Request.Form["openid_identifier"]); information with ClaimsRequest 
var fields = new ClaimsRequest(); 
fields.Email = DemandLevel.Demand; 
fields.FullName = DemandLevel.Demand; 
req.AddExtension(fields);

You can Request or Demand claims. In theory Demand means that if the provider or user refuses to provide the requested keys the request fails. In reality though providers like google ignore Request commands and so Demand is often required to retrieve information. To retrieve the claims data uses GetExtension():

// Retrieve custom profile information if available 
var claim = response.GetExtension(); 
string email = null, fullname= null; 
if (claim != null) 
{ 
  email = claim.Email; 
  fullname = claim.FullName; 
}

Just remember that there’s no guarantee these values are set.

The other issue in this controller deals with state management of the user. When the user comes to the page to hook up we have state on the page for that particular user, but when we re-direct to the OpenId provider site we loose that state. So in this case the user id needs to be tracked and I’m using a Session object for this.  I haven’t had much need for session in MVC applications but this is one case where I don’t see a way around this unfortunately. Before the request is sent the user’s id is stored in a Session object. When the authenticated request returns the session id is retrieved and used to look up the user’s business object which is the updated with the open id identifier and optionally the profile information if any. The rest of the bulky code deals with a few checks on the data returned and isolating which data to update.

OpenId Integration – More complicated than it looks

Phew – this looks like a lot of code you have to write and while it’s a pretty good chunk the largest part of this code has to do with the business logic to save and update the user’s information and setting the Forms Authentication ticket. The actual OpenId related logic is relatively simple once you understand the basic concept of how the flow of an OpenId authentication works.

As any Web based callback mechanism the code is bulky because you have to effectively route the responses inside your code and you end up with some code that couples Web and business logic which is always ugly. I remember fighting similar issues years ago with PayPal integration.  Processes like these are cumbersome because they are so closely tied to the actual Web request – the Controller Action in this case and don’t provide for an easy way to abstract and wrap into business logic. That’s the penalty you pay for a distributed system in many situations and this is no different.

There are also other solution out there for OpenId integration. Rob Conery has been mucking around and trying to convince me to try RPX which uses a man in the middle approach for hosting OpenId authentication. They basically provide the login API via a pop up window and you then retrieve the authentication data via a backend service call. But this requires yet another provider in the middle that you talk to through a proprietary API. It might help isolating the auth logic (service callback vs. a controller action). But it’s not for me – but for some of you this might be interesting to check out especially if you like the way the RPX looks. It'll definitely be a less code approach.

I hope though that this article is useful to some of you. I certainly wish I would have had something to look at when I started looking at OpenId.There’s a lot of high level posturing stuff out there that talks about the benefits and goals of open ID but preciously little code that actually shows the progression of steps in context that explains OpenId flow on a high level along with an implementation.

 


    Average:4
  • Leest
    (6153)
  • Permalink
Om te delen:

Tag cloud

Anything in here will be replaced on browsers that support the canvas element