How To: Build A Sign On Service Like Windows Live Using ASP.Net

If you run a business that serves a variety of web-enabled products to your clients, the task of implementing sign-on logic for each one can become redundant and hard to maintain. Having a single sign on service can help modularize the sign on process for all of your products as well as add fluidity as your users move between your services.

First, I would suggest looking into one of the single sign on services already out there such as Windows Live or OpenID. There’s no real point in building your own service if it’s possible to use one of these readily available services. However, if for some reason these services just don’t fit your needs, this article will cover a simple solution.

I’ll be covering, in order, the following pieces:

  1. Database
  2. A Simple Sign-in Page
  3. Web Service
  4. Integrating an Application

All the code is available for download at the end of this article. You may want to download it in advance and follow along.

Database
I’ll be working in a database I’ve named SingleSignOn. The following script will create your tables:

Download SQL

For this guide we will keep things simple, starting with the database design. We only need 3 tables to do the job:

  • Users
  • UserSessions
  • Applications

The code in this article is going to assume you’ve already filled up your users table so you may want to fill it with some sample data (remember to hash the passwords using MD5); a registration screen won’t be difficult to add on later. A test user is included in the downloadable SQL file at the end of the article.

We will explore the tables more fully as we build our service.

A Simple Sign-in Page
Instead of having users for each of your products log in using yet another log in page, we will create a central page that will handle all login requests and then perform the appropriate redirect.

The sign-in page will do a few things:

  1. Identify the requesting application
  2. Authenticate the user
  3. Create a session for the user
  4. Redirect the user

Our page will check the query string for the application ID. This will allow us to display to the users what application they are logging into, but more importantly it tells our sign-in page where to send them once we’ve authenticated them.

Before we can start our sign-in page, we need to create an access layer for our database. For the sake of simplicity, I will use LINQ to SQL.

image

Now, let’s create a page with a few labels, a couple text-boxes, and a sign-in button.

image

On page load, we will check for an application ID in the query string. We don’t really need to do anything with it yet other than set our labels.

protected void Page_Load(object sender, EventArgs e)
{
    int appID;
    string sAppID = Request.QueryString["appid"];

    if (!String.IsNullOrEmpty(sAppID) && int.TryParse(sAppID, out appID))
    {
        SingleSignOn.Database.Context dbCon = new Context(DBSettings.SignOnConnectionString);
        SingleSignOn.Database.Application app = dbCon.Applications.Where(
            a => a.ApplicationID == appID
            ).FirstOrDefault();

        if (app != null)
        {
            lblApplicationName.Text = app.Name;
            lblPublisher.Text = app.Publisher;
        }
    }
}

Now, let’s stub out some helper methods. We will need methods to authenticate a user based on an email address and a password, as well as methods to create a session and redirect the user to the correct place.

protected int AuthenticateUser(string username, string passwordhash)
{
    return -1;
}

protected string CreateUserSession(int userID)
{
    return null;
}

protected void RedirectUser(string sessionID)
{

}

Let’s add an event to our sign-in button and write the code that will direct the flow.

protected void btnSignIn_Click(object sender, EventArgs e)
{
    int userID = AuthenticateUser(txtEmail.Text, txtPassword.Text);
    if (userID == -1)
    {
        lblError.Text = "Sign in failed. Please try again";
        return;
    }

    string sessionID = CreateUserSession(userID);

    if (String.IsNullOrEmpty(sessionID))
    {
        lblError.Text = "Error during sign in. Please try again.";
        return;
    }

    RedirectUser(sessionID);
}

Now, we just need to implement our methods.

AuthenticateUser will need to find the user trying to log in, test the hash of the entered password against the one stored in our database, and, on a successful match, return the user’s ID. On failure, we’re going to return –1.

protected int AuthenticateUser(string email, string password)
{
    string passwordHash = GetMd5Sum(password);
    SingleSignOn.Database.Context dbCon = new Context(DBSettings.SignOnConnectionString);
    SingleSignOn.Database.User user = dbCon.Users.Where(
        u => u.Email.ToLower() == email.ToLower() && u.Password == passwordHash
        ).FirstOrDefault();

    if (user != null)
    {
        return user.UserID;
    }

    return -1;
}

CreateUserSession takes the user ID we retrieved and initializes a session. It then returns the session ID.

protected string CreateUserSession(int userID)
{
    try
    {
        SingleSignOn.Database.Context dbCon = new Context(DBSettings.SignOnConnectionString);
        SingleSignOn.Database.UserSession newSession = new UserSession();
        newSession.Created = DateTime.Now;
        newSession.SessionID = Guid.NewGuid();
        newSession.UserID = userID;
        newSession.Expires = DateTime.Now.AddMinutes(1.0);
        dbCon.UserSessions.InsertOnSubmit(newSession);
        dbCon.SubmitChanges();
        return newSession.SessionID.ToString();
    }
    catch
    {
        return null;
    }
}

We’ve set the session to expire after a minute, which is more than enough time to redirect the user back to the application they are logging into. We’re using the “Expires” column to determine how long the user has to complete the sign-in handshake before this session is declared “dead”.

RedirectUser will determine where to send the user based on the application ID found in the query string. You may wish to handle the case where there is no application ID by dictating a default application ID. We’ll place the session ID in the query string so the application can handle the handshake with our web service.

protected void RedirectUser(string sessionID)
{
    int appID;
    string sAppID = Request.QueryString["appid"];

    if (!String.IsNullOrEmpty(sAppID) && int.TryParse(sAppID, out appID))
    {
        SingleSignOn.Database.Context dbCon = new Context(DBSettings.SignOnConnectionString);
        SingleSignOn.Database.Application app = dbCon.Applications.Where(
            a => a.ApplicationID == appID
            ).FirstOrDefault();

        if (app != null)
        {
            Response.Redirect(app.RedirectUrl + "?sid=" + sessionID);
        }
    }
}

Our sign-in page is now functional enough to perform its duties. Let’s move on to our web service.

Web Service

The web service really only needs a single method: GetUserSessionInfo. This web method will examine our session ID, make sure it is valid, and return a class (that we will also create) containing the user’s ID, email, and display name.

[WebMethod]
public UserInfo GetUserSessionInfo(string sessionId)
{
    SingleSignOn.Database.Context dbCon = new Context(DBSettings.SignOnConnectionString);

    UserSession theSession = dbCon.UserSessions.Where(
        us => us.SessionID == new Guid(sessionId)
        ).FirstOrDefault();

    if (theSession != null && theSession.Expires > DateTime.Now && theSession.Serviced == null)
    {
        theSession.Serviced = DateTime.Now;
        dbCon.SubmitChanges();
        return new UserInfo(
            theSession.User.UserID, 
            theSession.User.Email, 
            theSession.User.DisplayName);
    }

    return null;
}

[Serializable()]
public class UserInfo
{
    public int UserID { get; set; }
    public string Email { get; set; }
    public string DisplayName { get; set; }

    public UserInfo()
    {
    }

    public UserInfo(int userID, string email, string displayName)
    {
        this.UserID = userID;
        this.Email = email;
        this.DisplayName = displayName;
    }
}

Notice that once the Serviced time is set, the session is considered “used” and won’t be valid again. This is a security measure that prevents the user’s session from being re-used. Now, let’s see how everything pieces together by integrating an application into our new architecture.

Integrating an Application

For our first application, we will build a simple profile viewer site. First, let’s add the information for the application into our applications table (we are going to assume everything is running on a local IIS server):

INSERT INTO Applications (Name, Publisher, IntegrationDate, RedirectUrl)
VALUES ('My Profile', 'www.geekscrapbook.com', GetDate(), 'http://localhost/myprofile')

Let’s add a class to our project called IntegratedPage. This will handle all the sign-on logic. On each request, we will make sure the Asp.Net session variable “UserInfo” is set. If it isn’t, we will check to see if there is a session ID in the query string. If there is one, we will call the web service method and set the session information. Otherwise, we will redirect the user to the single sign on site.

public class IntegratedPage : System.Web.UI.Page
{
    protected override void OnPreInit(EventArgs e)
    {
        if (UserInfo == null)
        {
            
            string s = Request.QueryString["sid"];

            if (!String.IsNullOrEmpty(s))
            {

                localhost.SignOnService webService = new localhost.SignOnService();
                UserInfo = webService.GetUserSessionInfo(s);
            }
        }

        if (UserInfo == null)
        {
            Response.Redirect("http://localhost/signonsite/default.aspx?appid=1");
        }

        base.OnPreInit(e);
    }

    public localhost.UserInfo UserInfo
    {
        get
        {
            return (localhost.UserInfo)Session["UserInfo"];
        }
        set
        {
            Session["UserInfo"] = value;
        }
    }
    
}

Now all we have to do to require sign-in for a page is derive it from our new IntegratedPage:

public partial class _Default : IntegratedPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        lblEmail.Text = UserInfo.Email;
        lblDisplayName.Text = UserInfo.DisplayName;
    }
}

So, let’s put it all together. I simply added each of the three web projects as applications to my machine’s IIS server, allowing me to test locally:

image      image 

And there you have it: sign on handled independently from a central location.

Download the Solution

You can download a zip file of the solution by clicking the link below.

SingleSignOn.zip

Leave a Reply