Thursday, June 12, 2008

Adding OpenID to an existing ASP.NET application

[Updated 22:00 12 June 2008] To include improvements suggested by Andrew Arnott.

In this post I'll describe the steps I took to add OpenID support to an existing ASP.NET app that used forms authentication. The application originally used the users email address as their username. The OpenID login process therefore needs to provide an email address to avoid having to rewrite loads of code. Not all OpenID providers allow email addresses to be sent so new users might have to partially re-register the first time they use their OpenID.

  1. Downloaded the latest DotNetOpenID zip file from http://dotnetopenid.googlecode.com/files/
  2. Unziped the package
  3. Copied and renamed login.aspx and login.cs from \Samples\RelyingPartyPortal to my project's root folder, renaming them loginOpenID.aspx and loginOpenID.cs. Changed codebehind attribute in loginOpenID.aspx from CodeBehind="login.aspx.cs" to CodeBehind="loginOpenID.aspx.cs". I then added a link named "Login with OpenID" on my original login.aspx page pointing to the new loginOpenID.aspx page. I then changed some of the properties on the OpenIdLogin control as I wanted to ask the OpenID provider to supply the users FullName and Email address. Unfortunately some providers (eg Yahoo.com) do not allow the FullName & Email info to be sent so we'll have to deal with this in code later.

    <RP:OpenIdLogin ID="OpenIdLogin1"
    runat="server"
    RequestFullName="Require"
    RequestEmail="Require"
    RememberMeVisible="True"
    PolicyUrl="~/PrivacyPolicy.aspx"
    TabIndex="1"
    OnLoggedIn="OpenIdLogin1_LoggedIn"    
    OnCanceled="OpenIdLogin1_Canceled"
    OnFailed="OpenIdLogin1_Failed"
    OnSetupRequired="OpenIdLogin1_SetupRequired" RememberMe="True"
    />

  4. Copied \Samples\RelyingPartyPortal\Code\state.cs to my project's App_Code folder
  5. Copied \Samples\RelyingPartyPortal\xrds.aspx to my project's root folder. Modified this file to point to my new loginOpenID.aspx page changing

    <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/login.aspx"))%></URI>

    to

    <URI><%=new Uri(Request.Url, Response.ApplyAppPathModifier("~/loginopenid.aspx"))%></URI>

  6. Copied \Samples\RelyingPartyPortal\privacypolicy.aspx to my project's root folder.
  7. I added the following to default.aspx (the default document for this domain).
    <%@ Register Assembly="DotNetOpenId" Namespace="DotNetOpenId" TagPrefix="openid" %>
    <openid:XrdsPublisher runat="server" XrdsUrl="~/xrds.aspx" />
    This was required to get myopenid.com accounts to work.
  8. Added a reference to the DotNetOpenID.dll from the \Samples\RelyingPartyPortal\bin folder
  9. I then added code to OpenIdLogin1_LoggedIn in loginOpenID.cs to ensure we have the user's real email address. If for whatever reason we cannot get their email address redirect them to the partially populated registration page.


     

    protected void OpenIdLogin1_LoggedIn (object sender, OpenIdEventArgs e)

        {

        State.ProfileFields = e.ProfileFields;

        

        //    Setup linq connection to SQL database                        

        DataClassesDataContext db = new DataClassesDataContext(ConfigurationManager.ConnectionStrings["DB_RW"].ConnectionString);

        People people = null;


     

        //    See if user has logged on using OpenID before                

        try

            {

            people = (from c in db.Peoples

                     where c.OpenID == e.ClaimedIdentifier.ToString()

                     select c).Single();

            }

        catch

            {

            }


     

        if (people == null)

            {

            //    This is the first time this OpenID identity has been used    

            if (e.ProfileFields.Email == null)

                {

                //    Force user to register as their OpenID provider did not send their email

                //    address (eg Yahoo.com) and this app needs their real email address.        

                e.Cancel = true;

                Response.Redirect("RegisterNewAccount.aspx?mode=OpenID_NoEmailSupplied");

                return;

                }

            else

                {

                //    See if user has created an account already                

                try

                    {

                    people = (from c in db.Peoples

                             where c.Email == e.ProfileFields.Email

                             select c).Single();

                    }

                catch

                    {

                    //    email address does not exist in our user table redirect user    

                    //    to registration page.                                            

                    e.Cancel = true;

                    Response.Redirect("RegisterNewAccount.aspx?mode=OpenID_UnknownEmail");

                    return;

                    }

                }

            }


     

        if (people.Status.StartsWith("Reject T&C"))

            {

            e.Cancel = true;

            loginFailedLabel.Text = "Your account has been suspended because you have rejected our T&C's.";

            loginFailedLabel.Visible = true;

            return;

            }


     

        if (people.Status != "Verified")

            {

            e.Cancel = true;

            loginFailedLabel.Text = "Your account has not been verified yet. Check your email for further instructions.";

            loginFailedLabel.Visible = true;

            return;

            }


     


     

        //    Remember OpenID identity for next time            

    people.OpenID = e.ClaimedIdentifier.ToString();
    people.LoginCount = (people.LoginCount ?? 0) + 1;

        db.SubmitChanges();


     

        //    I set some other Session variables here

    Session["UserEmail"] = people.Email;


     

    //    The openID code will now redirect to the requesting page    

    //    and set context.user.identity.name to the OpenID identity    

    //    eg http://andrew.jones.myopemid.com                            

    }

  10. The RegisterNewAccount.aspx pre-populates as much information as it can. Any additional user information sent by the OpenID provider is available in State.ProfileFields.

2 comments:

Joel said...

Hi, thanks for doing this, im trying to do the same thing. I dont have an App_Code folder in my project (using studio 2008), I tried creating one, but my aspx page cant find the state class, is there anything I have to do to get studio to find this class in App_Code folder, I have tried with and without namespaces and no luck. According to the MS docs the App_Code should work automatically.

Andrew Jones said...

App_Code is a bit of an old concept from the VS2005 days. Try placing the file in your root folder.

Watch out I've updated this post with new improved code!