Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Implementing OpenID in ASP.NET MVC

Sunday, 5 December, 2010 @ 10:36 AM < Adam Boddington
Tags: Architecture, ASP.NET MVC, Building Neno, DotNetOpenAuth, OpenID

This is post #12 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.

There's a good example of implementing OpenID relying party with DotNetOpenAuth included in the DotNetOpenAuth download. However, it was Rick Strahl's real world implementation that helped me to grok it. Check them both out -- if the included sample isn't doing it for you, Rick's detailed examination just might.

Registration Process

After reading Rick, I've decided on a two stage registration process.

  1. The first stage is logging in. After a user logs in with their OpenID, a forms authentication cookie is set containing their OpenID identifier. The user is considered authenticated at this stage.

  2. The second stage is registration. The user will be asked to review the display name and email address sent by their OpenID provider, or to enter them if they were withheld. After the information is entered, the user is added to the database and is then considered registered. The application will know if someone is registered if the OpenID identifier in the forms authentication cookie matches a user in the database.

The reason I'm after a display name and email address, especially the email address, is because of a recent kerfuffle over some OpenID providers changing the identifiers of their users. Rob Conery had a hell of a time trying to reassociate users with their accounts. If something similar ever happens to me, or if users simply forget their OpenID, an email address will hopefully eliminate a lot of the hassle. I don't intend to use the email address for any purpose other than account recovery (and possibly a Gravatar) -- a fact I will have to make very clear on the registration page.

I'm not completely sure about this setup. Some people may not feel comfortable entrusting a small site with their email address. I may change the email address requirement later on -- but that won't change my registration process as I'll still require a display name.

Forms Authentication

I'll be using forms authentication, so I'll need to turn a few things on in my Web.config.

<configuration>
    <system.web>
        <authentication mode="Forms">
            <forms timeout="2880" />
        </authentication>
    </system.web>
</configuration>

I don't need to specify a login URL (you'll see why later on), but I'll definitely use the timeout value when setting my cookie. 2,880 minutes is 48 hours.

Cookie Helper

One of the things I liked about Rick's solution is the cookie helper he made to help him get and set the forms authentication cookie. Normally the System.Web.Security.FormsAuthentication class will do this automagically, but Rick had a few extra things he wanted to store in his cookie. I'll be doing something similar with display name and email address. I plan to move from the login screen to the registration screen if the OpenID identifier doesn't match a user in my database. If the OpenID provider gave me a display name and email address (which I'll request, not demand), I want to pass that information off to the registration screen, through the cookie, for the user to review.

public class AuthCookie
{
    private static readonly string DELIMITER = Environment.NewLine;

    public AuthCookie(string identifier)
    {
        Identifier = identifier;
    }

    public string Identifier { get; private set; }
    public string DisplayName { get; set; }
    public string Email { get; set; }

    public static AuthCookie Get()
    {
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
            return null;

        var authCookie = new AuthCookie(HttpContext.Current.User.Identity.Name);

        if (!(HttpContext.Current.User.Identity is FormsIdentity))
            return authCookie;

        var identity = (FormsIdentity)HttpContext.Current.User.Identity;

        if (string.IsNullOrWhiteSpace(identity.Ticket.UserData))
            return authCookie;

        string[] data = identity.Ticket.UserData.Split(new[] { DELIMITER }, StringSplitOptions.None);
        authCookie.DisplayName = data[0];

        if (data.Length > 1)
            authCookie.Email = data[1];

        return authCookie;
    }

    public void Set()
    {
        DateTime now = DateTime.Now;
        string data = DisplayName + DELIMITER + Email;
        var ticket = new FormsAuthenticationTicket(1, Identifier, now, now + FormsAuthentication.Timeout, true, data);
        string encryptedTicket = FormsAuthentication.Encrypt(ticket);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) { Expires = now + FormsAuthentication.Timeout };
        HttpContext.Current.Response.Cookies.Add(cookie);
    }
}

Service Backfill

If you didn't follow that, the OpenID identifier is going in the forms authentication cookie, a.k.a. HttpContext.Current.User.Identity.Name. Which gives me a way to finally implement IUserService.GetCurrentUser. The implementation will depend on System.Web, so I'll make a new project for it, StackingCode.Neno.Services.Web.

public class UserService : Services.UserService
{
    public override User GetCurrentUser()
    {
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
            return null;

        return GetUserByOpenIdIdentifier(HttpContext.Current.User.Identity.Name);
    }
}

And now I can configure Spring with a user service.

<!-- Services -->
<object name="UserService" type="StackingCode.Neno.Services.Web.UserService, StackingCode.Neno.Services.Web" scope="request">
    <property name="Context" ref="Context" />
    <property name="UserRepository" ref="UserRepository" />
</object>
<!-- Repositories -->
<object name="UserRepository" type="StackingCode.Neno.Repositories.NHibernate.UserRepository, StackingCode.Neno.Repositories.NHibernate" scope="request">
    <property name="Context" ref="Context" />
    <property name="Session" ref="Session" />
</object>

Action Filter Attributes

The standard AuthorizeAttribute isn't going to cut it in my registration process. Since I'm authenticating on the OpenID login, but requiring registration before anything can really be done, I'm going to need additional attributes to enforce that. And I might just replace AuthorizeAttribute anyway. It authenticates at the very least, and authorises if you pass it more information (like roles). But I'm only interested in authentication, so it's poorly named for my needs.

public class AuthenticatedAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);

        if (filterContext.Result != null)
            return;

        if (filterContext.HttpContext.User.Identity.IsAuthenticated)
            return;

        filterContext.Controller.TempData.Messages().Add(MessageType.Warning, "Please use an OpenID to login before continuing.");
        filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "Controller", "user" }, { "Action", "login" }, { "ReturnUrl", filterContext.HttpContext.Request.RawUrl } });
    }
}

You can see why I don't need to specify a login URL in my Web.config -- I'm hardcoding it instead -- which smells, but this isn't in Moja yet so I'm cutting myself a break. To make it smell less I could pass in a login URL as an attribute parameter, or use FormsAuthentication.LoginUrl.

Authentication is only half the story, I need registration as well. RegisteredAttribute inherits from AuthenticatedAttribute to enforce the login before the registration.

public class RegisteredAttribute : AuthenticatedAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);

        if (filterContext.Result != null)
            return;

        if (filterContext.HttpContext.User.Identity.IsRegistered())
            return;

        filterContext.Controller.TempData.Messages().Add(MessageType.Warning, "Please provide some more details about yourself before continuing.");
        filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "Controller", "user" }, { "Action", "register" }, { "ReturnUrl", filterContext.HttpContext.Request.RawUrl } });
    }
}

Where did IIdentity.IsRegistered come from? I, uh, just made it up. Let me implement that.

public static class IIdentityExtensions
{
    public static bool IsRegistered(this IIdentity identity)
    {
        return User.Current != null;
    }
}

Login

The UserController class is where the OpenID magic happens. This is almost the same as the DotNetOpenAuth MVC sample for relying party, with just a few changes.

public class UserController : Controller
{
    public ActionResult Login(string returnUrl)
    {
        // Stage 1, show the login form.
        return View();
    }

    [ValidateInput(false)]
    public ActionResult Authenticate(string returnUrl)
    {
        try
        {
            var openIdRelyingParty = new OpenIdRelyingParty();
            IAuthenticationResponse response = openIdRelyingParty.GetResponse();

            // Stage 2, user submitting identifier, redirecting to the provider.
            if (response == null)
            {
                Identifier identifier;

                if (!Identifier.TryParse(Request.Form["Identifier"], out identifier))
                    throw new Exception("Invalid identifier.");

                IAuthenticationRequest request = openIdRelyingParty.CreateRequest(identifier);
                var claims = new ClaimsRequest { FullName = DemandLevel.Request, Email = DemandLevel.Request };
                request.AddExtension(claims);

                return request.RedirectingResponse.AsActionResult();
            }

            // Stage 3, the provider redirecting back with an assertion response.
            switch (response.Status)
            {
                case AuthenticationStatus.Authenticated:
                {
                    var authCookie = new AuthCookie(response.ClaimedIdentifier);
                    var claims = response.GetExtension<ClaimsResponse>();

                    if (claims != null)
                    {
                        authCookie.DisplayName = claims.FullName;
                        authCookie.Email = claims.Email;
                    }

                    authCookie.Set();
                    Messages.Add("Login completed successfully.");

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

                    return RedirectToAction("index", "home");
                }
                case AuthenticationStatus.Canceled:
                    throw new Exception("Authentication cancelled at the provider.");
                default: // AuthenticationStatus.Failed
                    throw new Exception("Authentication failed.", response.Exception);
            }
        }
        catch (Exception ex)
        {
            Messages.Add(ex);

            return View("login");
        }
    }

    // ...
}

The authenticate action gets hit twice -- once by the user, and then again after authentication by a redirect from the OpenID provider.

One difference from the DotNetOpenAuth sample is the request for a display name and email address from the OpenID provider with the ClaimsRequest class. I know a request won't get me far with Google -- they only honour demand -- but that's fine, Google users will just have to type their details in. Another difference is I'm catching exceptions and sending the user back to the login screen with a message. This lets the user try again, or use another OpenID provider. Here's an example of that with a bad identifier.

Invalid Identifier

Register

The UserController class also handles registration.

public class UserController : Controller
{
    // ...

    [Unregistered]
    public ActionResult Register(string returnUrl)
    {
        AuthCookie authCookie = AuthCookie.Get();
        var user = new User { DisplayName = authCookie.DisplayName, Email = authCookie.Email };
        user.OpenIds.Add(new OpenId(user) { Identifier = authCookie.Identifier });

        return View(user);
    }

    [HttpPost]
    [Unregistered]
    public ActionResult Register(string returnUrl, FormCollection collection)
    {
        AuthCookie authCookie = AuthCookie.Get();
        var user = new User { DisplayName = authCookie.DisplayName, Email = authCookie.Email };
        user.OpenIds.Add(new OpenId(user) { Identifier = authCookie.Identifier });

        try
        {
            TryUpdateModel(user, new[] { "DisplayName", "Email", "Website" });

            if (!ModelState.IsValid)
                return View(user);

            Container.Get<IUserService>().CreateUser(user);
            Messages.Add(string.Format("Registration completed successfully. Welcome to Stacking Code, {0}.", user.DisplayName));

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

            return RedirectToAction("index", "home");
        }
        catch (Exception ex)
        {
            Messages.Add(ex);

            return View(user);
        }
    }

    // ...
}

This is a pretty standard create action for MVC. An object is created, some properties are updated from the form, UI validation takes place, and the object is saved. If there are any errors or validation concerns, the user gets the view again with a message.

Logout

Finally there is logout. Not much to it.

public class UserController : Controller
{
    // ...

    public ActionResult Logout()
    {
        if (User.Identity.IsAuthenticated)
        {
            FormsAuthentication.SignOut();
            Messages.Add("Logout completed successfully.");
        }
        else
            Messages.Add(MessageType.Warning, "You are already logged out.");

        return RedirectToAction("index", "home");
    }
}

XRDS

This handles the callback that OpenID providers will make to my site. The home page needs a header to let the OpenID provider know where it can find the XRDS document.

public class HomeController : Controller
{
    public ActionResult Index()
    {
        // Getting a full URL can be done by specifying the protocol.
        Response.AppendHeader("X-XRDS-Location", Url.Action("xrds", "home", null, Request.Url.Scheme));

        // ...
    }

    public ActionResult Xrds()
    {
        return View();
    }
}

The XRDS document looks like this. If there's more than one action accepting OpenIDs, they need to be listed here.

<%@ Page Language="C#" ContentType="application/xrds+xml" Inherits="System.Web.Mvc.ViewPage<dynamic>" %><?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS
    xmlns="xri://$xrd*($v*2.0)"
    xmlns:openid="http://openid.net/xmlns/1.0"
    xmlns:xrds="xri://$xrds">
    <XRD>
        <Service priority="1">
            <Type>http://specs.openid.net/auth/2.0/return_to</Type>
            <URI><%=Url.Action("authenticate", "user", null, Request.Url.Scheme)%></URI>
        </Service>
    </XRD>
</xrds:XRDS>

Demo

To finish up I'm going to run through how the two step registration process looks in action. I'll start off by manually hacking in a request to /user/register.

User Login

I haven't chosen a JavaScript control for helping the user out with OpenID providers yet, so I'll manually enter the Google OpenID provider address (https://www.google.com/accounts/o8/id).

User Register

If I enter some dodgy details...

User Register Invalid

The validation attributes on my User class kick in. After I enter some valid details...

User Register Complete

Registration is complete.

Conclusion

I'm still not done. I haven't chosen a JavaScript OpenID control and I don't have buttons for logging in or out yet. I'm happy with how OpenID is working, but unfortunately this has taken up most of my Sunday -- maybe a username and password would have been quicker.

There are 1 comments.


Comments

srinivas wrote on Friday, 28 March, 2014 @ 5:27 AM

Thanks

Leave a Comment

Please register or login to leave a comment.


Older
Simple Messages in ASP.NET MVC

Newer
A Simple OpenID Selector

Older
Simple Messages in ASP.NET MVC

Newer
A Simple OpenID Selector

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM