Integrate OpenId Authentication with ASP.NET Membership, Roles, and Profile

Shows how to integrate OpenId authentication using the ExtremeSwank OpenId UserControl and simplified ASP.NET Membership, Role and Profile providers.

OpenId has come a long way since I last looked it over, about 8 months ago. It's estimated that there are now 120 million people who have OpenId Identities, and the various OpenId server hosters have added a number of new features, all standards-based, to the mix. 

With OpenID, you create a single username and password, along with some data about you.  You can then log into an increasing number of sites without registering each time. It's also free - forever. OpenID also provides you with a place to store your digital identity - a place where you can easily be found on the web. 

OpenId has been in testing by Google for over a year, and is currently used by Yahoo and other "big" sites. If the "Microhoo" deal goes through, that means even Microsoft will be using it.

The things I like most about OpenId are that it returns a consistent unique identity for an authenticated user, e.g. "pbromberg.myopenid.com" (with myopenid.com you actually get an identity page with optional photo). In addition, it usually returns the person's email address they gave when they signed up, which makes it a lot easier to pull together everything needed for ASP.NET Membership and Profile integration. Unlike LiveId, a site using OpenId for authentication does not need to "register" for some sort of Application key -- the OpenId Identity that comes back is unique and unencrypted. You can also instruct OpenId to "keep you logged in" which in effect creates a single sign-on mechanism for any site that supports OpenId.

This implementation is "barebones" - in fact, I actually did it by hacking a previous sample I had made for LiveId.  There have been several clean implementations of OpenId for .NET recently. One "Less is More" minimalist approach was done by Mads Kristensen here.  But if you want a more robust implementation that even has "hooks" for some of the newer features such as discovery and MicroId, I'd recommend the ExtremeSwank consumer and control here. This is the one I used for this sample.

There are three projects in the solution:

1) The ExtremeSwank OpenId Consumer and ASCX UserControl.

2) The PAB.Web.Providers simple Membership, Roles and Profile providers, which is an "even more simplified" modification of the Altairis Simple providers that is hosted on codeplex.com, and

3) An Asp.Net Web Application Project that has a couple of pages and ties it all together in a "barebones" sample implementation.

Using the controls is super-simple; what you have to be able to do if you want to integrate this with Membership, Roles and Profile is detect whether the user who has authenticated at your site via OpenId has already filled out the items that your site wants to capture.  Here's some sample code that illustrates the basics:

public OpenIDUser OpenIDUser
    {
        get
        {
            return OpenIDControl1.UserObject;
        }
    }

    protected void Page_PreRender(object sender, EventArgs e)
    {
        OpenIDUser oiu = this.OpenIDUser;
        string prefix = "openid.sreg.";
        if (oiu != null)
        {
            string email =  oiu.GetValue(prefix + "email");
            if (!IsUserRegistered(oiu.Identity))
                this.pnlRegister.Visible = true;
                this.txtUserName.Text = oiu.Identity;
                this.txtEmail.Text = email;
        }
    }


    protected bool IsUserRegistered(string userId)
    {
        bool registered = false;
       MembershipUser usr= Membership.GetUser(userId);
       if (usr!=null && usr.Email != null)
           registered = true;
        return registered;
    }

    protected void btnRegister_Click(object sender, EventArgs e)
    {
        string UserId = this.txtUserName.Text;
        MembershipUser user = Membership.CreateUser(UserId, UserId, txtEmail.Text);
        if (user != null)
        {
            FormsAuthentication.Authenticate(UserId, UserId);
            WebProfile Profile = new WebProfile();
            Profile.Initialize(UserId, true);
            Profile.FirstName = this.txtFirstName.Text;
            Profile.LastName = this.txtLastName.Text;
            Profile.Newsletter = this.chkNewsLetter.Checked;
            Profile.Email = this.txtEmail.Text;
            Profile.Save();

            GenericIdentity userIdentity = new GenericIdentity(UserId);
            GenericPrincipal userPrincipal =
              new GenericPrincipal(userIdentity, new string[] { "User" });
            Context.User = userPrincipal;

            if (!Roles.IsUserInRole(User.Identity.Name, "User"))
            {
                PAB.Web.Providers.SimpleSqlRoleProvider prov = new SimpleSqlRoleProvider();
                NameValueCollection config = new NameValueCollection();
                config["connectionStringName"] = "OpenId";
                System.Configuration.ConnectionStringSettings ConnectionStringSettings =
                    System.Configuration.ConfigurationManager.ConnectionStrings[config["connectionStringName"]];
                prov.Initialize("", config);
                prov.AddUsersToRoles(new string[] { User.Identity.Name }, new string[] { "User" });
            }
            // go to a page for users who are authenticated
            Response.Redirect("Default2.aspx");
        }
        else
        {
            //uh-oh! you handle it appropriately.
        }

    }
The Page_Prerender handler tests for a valid OpenId that has been captured by the ASCX control. If it is present, it then calls a little helper method to see if this is a returning user, e.g., we have already captured and stored their Membership, Role and Profile info. If IsUserRegistered returns false, we simply turn on the visibility of the Profile Fields in a panel and have them complete the sign-up process. There is no password here, they already "did that" up at their OpenId site when they authenticated. Consequently, I've commented out and do not use all the Hash / Salt stuff in the Membership provider, since it isn't needed (unless you want to "duplicate" with Forms Authentication and maintain your own ASP.NET Membership on top of OpenId).  I think it is important to stress here that an OpenId is not an "account" - it simply represents the authentication of an Identity. While there are optional and required fields that you can ask to be returned into the OpenId User object, including their email, that does not mean that the particular user has enabled this.

When the user completes the Registration form, I create a new MembershipUser and save it, authenticate them with Forms Auth (which in principle is not necessary because the OpenId Consumer maintains a copy of the User object in Session). Then I create a new simple Profile object, initialize, populate and save it. Finally, i give them a role and save that. After that we are done and we can redirect them to the page for authenticated and fullly registered users. At this point if you want / need to use FormsAuthentication to allow access to folders/ pages by role, you can do the regular FormsIdentity / ticket route as if your user had authenticated using the intrinsic ASP.NET FormsAuthentication scheme; the code to do this would be little different from regular Forms Authentication code.

I haven't added any Validation controls to the TextBox fields in the Panel, but you would want to do this to ensure that your users do fill in all the fields in order to "get registered' on your site.

To test for authentication on a particular page, I've used a very simple technique that looks like this:

 protected SessionPersister persister = null;   
  
   protected void Page_Load(object sender, EventArgs e)
    {
        persister = new SessionPersister();
        OpenIDUser oiu = (OpenIDUser) persister["UserObject"];
        if(oiu==null)
            Server.Transfer("~/Sample/default.aspx");
        lblMessage.Text = "User " + oiu.Identity + " logged in."; 

    }

Of course, you can get a lot more sophisticated than this. But I just wanted to illustrate how to get "the guts" of the process working. It's not difficult at all.

You can download the Visual Studio 2008 solution here. If you don't have 2008, just create a new blank 2005 Solution and add the projects to it.  To make this work:

1) Create a new SQL Server database "OPENID". Run the provided script to populate the 4 required tables.

2) Modify the connection string to suit your environment, in the web.config.

3) Ensure that the OpenIdAuth folder is an IIS Application. Visual Studio 2008 will prompt you when the project loads; with previous versions you need to set it up yourself:
go into IIS Manager and add a new IIS Application with the physical location pointing to the folder you unzipped the solution into. It's alias should be "OpenIdAuth".

By Peter Bromberg   Popularity  (8300 Views)