Converting the ASP.NET MVC project into OpenID

When you create an ASP.NET MVC project it comes with a controller called AccountController that manages logging in, logging out, registering, changing password and so on. Since usernames and passwords are dead I converted it into OpenID and I’m just pasting it here for everybody to use.

I’m using the DotNetOpenAuth library which you have to download, put in your project and refer. The difference between what I’m pasting and the example provided by DotNetOpenAuth is that I’m actually storing the user in the membership database, like the original AccountController.

My work is based on the on the blog post Adding OpenID to your web site in conjunction with ASP.NET Membership. I really had to put a couple of hours on top of that, so I consider it worth it to post it. Scott Hanselman also provides useful information for integrating OpenID. I’m using the jQuery OpenID plug-in but I’m not going to post my views. They are really trivial and left as an exercise to the reader.

I’m not using any extra tables, I’m storing the OpenID identifier (the URI) in the field for the username. This has the advantage of not requiring any other fields but the disadvantage that you can have only one identifier per user. There are some unfinished parts but since you are likely to customize them anyway, I don’t feel too guilty about not finishing yet. If you find a bug, please, let me know.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;
using System.Text;
using DotNetOpenAuth.OpenId.RelyingParty;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using System.Security.Cryptography;

namespace MyProject.Controllers {

    [HandleError]
    public class AccountController : Controller {
        static private OpenIdRelyingParty openid = new OpenIdRelyingParty();

        // This constructor is used by the MVC framework to instantiate the controller using
        // the default forms authentication and membership providers.
        public AccountController()
            : this(null, null) {
        }

        // This constructor is not used by the MVC framework but is instead provided for ease
        // of unit testing this type. See the comments at the end of this file for more
        // information.
        public AccountController(IFormsAuthentication formsAuth, IMembershipService service) {
            FormsAuth = formsAuth ?? new FormsAuthenticationService();
            MembershipService = service ?? new AccountMembershipService();
        }

        public IFormsAuthentication FormsAuth {
            get;
            private set;
        }

        public IMembershipService MembershipService {
            get;
            private set;
        }

        public ActionResult LogIn() {
            // Stage 1: display login form to user
            return View();
        }

        [ValidateInput(false)]
        public ActionResult Authenticate(string returnUrl) {
            var response = openid.GetResponse();
            if (response == null) {
                // Stage 2: user submitting Identifier
                Identifier id;
                if (Identifier.TryParse(Request.Form["openid_identifier"], out id)) {
                    try {
                        var request = openid.CreateRequest(Request.Form["openid_identifier"]);
                        request.AddExtension(new ClaimsRequest {
                            FullName = DemandLevel.Request,
                            Email = DemandLevel.Request,
                            Country = DemandLevel.Request,
                            PostalCode = DemandLevel.Request,
                            TimeZone = DemandLevel.Request
                        });
                        return request.RedirectingResponse.AsActionResult();
                    } catch (ProtocolException ex) {
                        ViewData["Message"] = ex.Message;
                        return View("Login");
                    }
                } else {
                    ViewData["Message"] = "Invalid identifier";
                    return View("Login");
                }
            } else {
                // Stage 3: OpenID Provider sending assertion response
                switch (response.Status) {
                    case AuthenticationStatus.Authenticated:
                        MembershipUser user = MembershipService.CreateOrGetUser(response);
                        FormsAuth.SignIn(user.UserName, true);

                        if (!string.IsNullOrEmpty(returnUrl)) {
                            return Redirect(returnUrl);
                        } else {
                            return RedirectToAction("Index", "Home");
                        }
                    case AuthenticationStatus.Canceled:
                        ViewData["Message"] = "Canceled at provider";
                        return View("Login");
                    case AuthenticationStatus.Failed:
                        ViewData["Message"] = response.Exception.Message;
                        return View("Login");
                }
            }
            return new EmptyResult();
        }

        public ActionResult LogOut() {
            FormsAuth.SignOut();
            return RedirectToAction("Index", "Home");
        }

        // TODO: do we need this? find out and remove if not.
        protected override void OnActionExecuting(ActionExecutingContext filterContext) {
            if (filterContext.HttpContext.User.Identity is WindowsIdentity) {
                throw new InvalidOperationException("Windows authentication is not supported.");
            }
        }
    }

    // The FormsAuthentication type is sealed and contains static members, so it is difficult to
    // unit test code that calls its members. The interface and helper class below demonstrate
    // how to create an abstract wrapper around such a type in order to make the AccountController
    // code unit testable.

    public interface IFormsAuthentication {
        void SignIn(string userName, bool createPersistentCookie);
        void SignOut();
    }

    public class FormsAuthenticationService : IFormsAuthentication {
        public void SignIn(string userName, bool createPersistentCookie) {
            FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
        }
        public void SignOut() {
            FormsAuthentication.SignOut();
        }
    }

    public interface IMembershipService {
        MembershipUser CreateOrGetUser(IAuthenticationResponse response);
    }

    public class AccountMembershipService : IMembershipService {
        private MembershipProvider provider;

        public AccountMembershipService()
            : this(null) {
        }

        public AccountMembershipService(MembershipProvider provider) {
            this.provider = provider ?? Membership.Provider;
        }

        public MembershipUser CreateOrGetUser(IAuthenticationResponse response) {
            var user = provider.GetUser(response.ClaimedIdentifier, true);

            if (user == null) {
                var claimsResponse = response.GetExtension<ClaimsResponse>();
                string email = null;
                if (claimsResponse != null) {
                    email = claimsResponse.Email;
                    //fullName = claimsResponse.FullName;
                    //nickname = claimsResponse.Nickname;
                }

                MembershipCreateStatus status;
                user = provider.CreateUser(response.ClaimedIdentifier,
                    GenerateRandomString(64),
                    email,
                    "This is an OpenID account. You should log in with your OpenID.",
                    GenerateRandomString(64),
                    true,
                    null,
                    out status);
                if (status != MembershipCreateStatus.Success) {
                    throw new Exception("Failed to find or create user: " + status);
                }

                // TODO: set extra info in the profile, taking it from OpenID.
            }
            return user;
        }

        private static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider();
        private static string GenerateRandomString(int length) {
            byte[] buffer = new byte[length];
            CryptoRandomDataGenerator.GetBytes(buffer);
            return Convert.ToBase64String(buffer);
        }
    }
}

Reviewed by Daniel Magliola. Thank you!

Advertisements

5 Replies to “Converting the ASP.NET MVC project into OpenID”

  1. Great post. Just a few bits of feedback:

    Your response.GetExtension call collapsed to response.GetExtension() (note lack of generic parameter), probably due to HTML parsing.

    You are setting user passwords to new Guids. It’s good that you’re randomizing them, but guid generation is predictable, allowing someone to pretty easily brute-force attack what the password is. While your site MAY not expose a web page that allows username/password login at all anyway, it’s a good security mitigation to assign a cryptographically strong random password just to make sure.

    Here’s some code that generates cryptographically strong random strings:
    internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider();

    byte[] buffer = new byte[length];
    CryptoRandomDataGenerator.GetBytes(buffer);
    return Convert.ToBase64String(buffer);

    Consider initializing one static OpenIdRelyingParty field in your controller and reusing it for all logins. OpenIdRelyingParty is relatively heavy to instantiate, and it’s thread safe. So the recommended pattern is to reuse one for all your logins on a single page. I know the sample doesn’t do this (the sample should be updated).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s