Preventing a User From Having Multiple Concurrent Sessions

This article is largely based on information learned within the book "Professional ASP.NET 3.5 Security, Membership, and Role Management with C# and VB" by Bilal Haidar.

A common question asked regarding ASP.NET is how can you prevent a user logging on more than once at the same time. Unfortunately, the nature of ASP.NET means that you cannot tell if a user is logged in already. Sure you can log the fact a user has accessed your application, but there is no way to tell that they have abandoned their old session, perhaps by closing their browser, and that their new login is therefore valid.

However, if we look at the problem from another angle there is a way to prevent concurrent access by a user. The steps we take are as follows:

  1. When the user logs in we generate a unique guid token to represent their session. We store this token in the authentication cookie, and also in the membership database within the comment property for the user.

  2. Every time the user accesses a page where he requires authenticated access we will check the token stored in the cookie against that stored in the membership database, and as long as they are identical we will allow access. However, if the user has logged in from another computer the token in the membership database will have changed and we will deny access and log them off from their old session.

To implement this in a web site we need to make several changes.

  1. We will make code changes to the LoggedIn event from the login control (or to the custom code we have written once the user has logged in). I place this code in a static method within a custom static class that I then call from the event which makes it easier to reuse in other projects.

  2. We will create a class that implements IHttpModule that will execute code for the OnPostAuthenticate event for all our pages that are authenticated. This separate class also makes it easy to implement in other projects.

  3. We will register the HttpModule in our web.config file.

So, onto the code. First my SingleSessionPreparation class that contains the method CreateAndStoreSessionToken to be called from the LoggedIn event.

using System; using System.Web; using System.Web.Security; /// <summary> /// SingleSessionPreparation is used to help ensure /// users may only have one session active /// </summary> internal static class SingleSessionPreparation {     /// <summary>     /// Called during LoggedIn event. Need to pass username     /// as login process not fully completed     /// </summary>     internal static void CreateAndStoreSessionToken(string userName)     {         // Will be using the response object several times         HttpResponse pageResponse = HttpContext.Current.Response;         // 'session' token         Guid sessionToken = System.Guid.NewGuid();         // Get authentication cookie and ticket         HttpCookie authenticationCookie =             pageResponse.Cookies[FormsAuthentication.FormsCookieName];         FormsAuthenticationTicket authenticationTicket =             FormsAuthentication.Decrypt(authenticationCookie.Value);         // Create a new ticket based on the existing one that includes the 'session' token in the userData         FormsAuthenticationTicket newAuthenticationTicket =             new FormsAuthenticationTicket(             authenticationTicket.Version,             authenticationTicket.Name,             authenticationTicket.IssueDate,             authenticationTicket.Expiration,             authenticationTicket.IsPersistent,             sessionToken.ToString(),             authenticationTicket.CookiePath);         // Store session token in Membership comment         // You may want to store other information in the comment         // field, if so, you may have to implement some dilimited         // structure within it, perhaps xml         MembershipUser currentUser = Membership.GetUser(userName);         currentUser.Comment = sessionToken.ToString();         Membership.UpdateUser(currentUser);         // Replace the authentication cookie         pageResponse.Cookies.Remove(FormsAuthentication.FormsCookieName);         HttpCookie newAuthenticationCookie = new HttpCookie(             FormsAuthentication.FormsCookieName,             FormsAuthentication.Encrypt(newAuthenticationTicket));         newAuthenticationCookie.HttpOnly = authenticationCookie.HttpOnly;         newAuthenticationCookie.Path = authenticationCookie.Path;         newAuthenticationCookie.Secure = authenticationCookie.Secure;         newAuthenticationCookie.Domain = authenticationCookie.Domain;         newAuthenticationCookie.Expires = authenticationCookie.Expires;         pageResponse.Cookies.Add(newAuthenticationCookie);     } }

Then my SingleSessionEnforcement class that will stop the user from logging in multiple times:

using System; using System.Web; using System.Web.Security; /// <summary> /// Enforces a single login session /// Needs an entry in Web.Config, exactly where depends on the version of IIS, but you /// can safely put it in both places. /// 1: ///  <system.web> ///     <httpModules> ///      <add name="SingleSessionEnforcement" type="SingleSessionEnforcement" /> ///    </httpModules> ///  </system.web> /// 2: ///  <system.webServer> ///    <modules runAllManagedModulesForAllRequests="true"> ///      <add name="SingleSessionEnforcement" type="SingleSessionEnforcement" /> ///    </modules> ///  </system.webServer> /// Also, slidingExpiration for the forms must be set to false, also set a /// suitable timeout period (in minutes) ///  <authentication mode="Forms"> ///   <forms protection="All" slidingExpiration="false" loginUrl="login.aspx" timeout="600" /> ///  </authentication> /// </summary> public class SingleSessionEnforcement : IHttpModule {     public SingleSessionEnforcement()     {         // No construction needed     }     private void OnPostAuthenticate(Object sender, EventArgs e)     {         Guid sessionToken;         HttpApplication httpApplication = (HttpApplication)sender;         HttpContext httpContext = httpApplication.Context;         // Check user's session token         if (httpContext.User.Identity.IsAuthenticated)         {             FormsAuthenticationTicket authenticationTicket =                 ((FormsIdentity)httpContext.User.Identity).Ticket;             if (authenticationTicket.UserData != "")             {                 sessionToken = new Guid(authenticationTicket.UserData);             }             else             {                 // No authentication ticket found so logout this user                 // Should never hit this code                 FormsAuthentication.SignOut();                 FormsAuthentication.RedirectToLoginPage();                 return;             }             MembershipUser currentUser = Membership.GetUser(authenticationTicket.Name);             // May want to add a conditional here so we only check             // if the user needs to be checked. For instance, your business             // rules for the application may state that users in the Admin             // role are allowed to have multiple sessions             Guid storedToken = new Guid(currentUser.Comment);             if (sessionToken != storedToken)             {                 // Stored session does not match one in authentication                 // ticket so logout the user                 FormsAuthentication.SignOut();                 FormsAuthentication.RedirectToLoginPage();             }         }     }     public void Dispose()     {         // Nothing to dispose     }     public void Init(HttpApplication context)     {         context.PostAuthenticateRequest += new EventHandler(OnPostAuthenticate);     } }

Changes to web.config to ensure that SingleSessionEnforcement is called:

  <system.web>     <httpModules>       <add name="SingleSessionEnforcement" type="SingleSessionEnforcement" />     </httpModules>   </system.web>   <system.webServer>     <modules runAllManagedModulesForAllRequests="true">       <add name="SingleSessionEnforcement" type="SingleSessionEnforcement" />     </modules>   </system.webServer>

And finally, code to our LoggedIn event:

protected void LoginUser_LoggedIn(object sender, EventArgs e) {     TextBox userNameTextBox = (TextBox)LoginUser.FindControl("UserName");     SingleSessionPreparation.CreateAndStoreSessionToken(userNameTextBox.Text); }

This article is part of the GWB Archives. Original Author: Frez

New on Geeks with Blogs

  • We Won The One Award I Actually Care About

    Full Scale made the Inc. 5000 for the fifth year straight, the 12th listing across my three companies. Here is why the one award you cannot buy is worth stopping for.

  • Your Customers Build the Features Now

    I let a tool I liked sit dead for a year rather than build the features I wanted. An MCP server meant I never had to, and your customers can do the same to your product.

  • Get the Size of a Directory in Linux the Easy Way

    du -sh for the quick answer, ncdu for the cleanup, df for the disk itself: every command for checking directory size in Linux, plus why du and df never agree.

  • Vim Search and Replace: The Ultimate Guide

    One :%s command replaces every match in a file before a find dialog would even open. The Vim substitute patterns worth the muscle memory: flags, ranges, capture groups, and multi-file edits.