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


By Peter Bromberg
Printer Friendly Version
  

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".



Biography
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking ,financial and telephony for 20 years. Pete focuses exclusively on the .NET Platform, and his samples at GotDotNet.com have been downloaded over 56,000 times. Peter enjoys producing 3D raytraced digital photo collage with Maya, the beach, and fine wines. You can view Peter's UnBlogIttyUrl, and BlogMetafinder sites.
Please post questions at forums, not via email!

button

 
Article Discussion: Integrate OpenId Authentication with ASP.NET Membership, Roles, and Profile
Peter Bromberg posted at 03-Feb-08 09:06
Original Article

 
Do you have any recommended reading.
Josh Lewis replied to Peter Bromberg at 10-Feb-08 06:12

If the Web Application Project development model  is the proper way to do things do you have any recommended links that could help me better understand that method.

Thanks,

Josh


 
Could you elaberate on how to set up the project.
Josh Lewis replied to Peter Bromberg at 06-Feb-08 10:53

I attempted to set the project up as you stated below but I get an error when OpenIdAuth attempts to make the folder a web application. Is there another step that I need to take to allow VS 2008 to do this.

Thanks,

Josh


 
Not sure what your issue is.
Peter Bromberg replied to Josh Lewis at 08-Feb-08 02:19
The OpenIdAuth project is a WAP (Web Application Project). If it doesn't load, that's because the  folder you unzipped it into isn't an IIS Application. Just 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".

 
RE: Not sure what your issue is.
Josh Lewis replied to Peter Bromberg at 10-Feb-08 12:45

I followed your reply instrustions and got it to work. It turns out I also had another problem of not have asp.net set up correctly on Vista/IIS 7. I found a step by step to set that up here. If you don't mind I do have a follow up question. I've tried to pull the code out of your project and intigrate parts of it into mine. It seems that part of your project requires using an IIS application. Is that necessary for the tie into the membership model? Because I didn't believe it was? Before I had found your article I had been trying to do the same thing but hadn't found the need to move out of the Visual Studio development web server. I guess the real question is why IIS over VS development web server? I comfortable with the coding but still trying to get a handle using the development tools to their full potential.

 

Thanks,

Josh Lewis


 
for a production site, yes.
Peter Bromberg replied to Josh Lewis at 10-Feb-08 05:33
This is why I generally do not recommend using the built-in Visual Studio development server, and to use the Web Application Project development model vs. the Web Site model (Visual Studio 2005, 2008 support both).