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!