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:
- 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.
- 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.
- 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.
- 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.
- 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 premarin price.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);
}