Hi folks,
Here I am, back again in 2012. Today I want to show how to restrict multiple concurrent user sessions.
My approach here works for both SharePoint and ASP.NET applications.
Scenario
In SharePoint/ASP.NET web sites using Forms Authentication and Session State, when logging in, the ASP.NET grants a session to the user.
If you haven’t changed your solution, probably the same user can log in to the application simultaneously from different browsers/desktops.
This behaviour brings some security issues that you can avoid by limiting the user to one session per account.
Solution
When the user is logged in, map the session and the user ID that is established using the Application State and when the user is logged out remove this session from the mapping. Then on each request you need to intercept and validate it using a Module.
Diagram
To make it clearer, check the Sequence Diagram for this solution:
Figure 1 – Sequence Diagram
In this diagram the following users and objects are described:
-
Browser 1 and 2 – They represent different browsers that displays pages.
-
Page – Object that is requested by the browser.
-
httpApplication – Application State whose scope is global, at the Application level.
-
httpModule – Module that intercepts requests to pages.
Note: In all cases just one user (User A) is requesting pages from different browsers.
Here are the steps that describe the diagram above:
1 – First Request (from Browser 1)
In the First Request SharePoint/ASP.NET Application starts, which loads the Application State (httpApplication).
The httpModule (to be developed) is loaded by the httpApplication.
Then the initialization of the module (Init) creates a variable to store user sessions in the Application State and adds an event handler to PostAcquireRequestState event to be triggered on each request.
2 – User A Logs In (from Browser 1)
The user A logs in to the web site, and the user session (SessionContext) is mapped to the variable stored in the httpApplication.
3 – User A Logs In (from Browser 2)
The user A logs in to the web site, and the user session (SessionContext) is mapped to the variable stored in the httpApplication, which overwrites the previous user session.
4 – Request to Any Page (from Browser 1)
The user A try to get a different page, but now the user session is no longer available in the httpApplication. The SessionID stored is different from the current SessionID, so the module forces the Session to be abandoned, redirecting the user to the Login page.
5 – User A Logs Out (from Browser 2)
The user A logs out, the user session (SessionContext) is removed from the variable stored in the httpApplication and the session is abandoned, redirecting the user the Login page.
Alternative case
If the user closes the browser, the variable remains in the httpApplication and the session remains active till it expires. When the user logs in again, the variable is overwritten (case 3 above).
Code
Now that it is very clear the purpose of this solution, let’s check the code for that.
A class is created to store the Session Context of the user:
Code Snippet
- using System;
-
- namespace Core
- {
- public class SessionContext
- {
- public string UserName { get; set; }
- public string SessionID { get; set; }
- }
- }
The core part of the solution is the httpModule that intercepts the requests:
Code Snippet
- using System;
- using System.Collections.Generic;
- using System.Web;
- using System.Web.Security;
-
- namespace Core
- {
- public class SessionManagerModule :IHttpModule
- {
- public SortedDictionary<string, SessionContext> ASPNETContext { get; set; }
-
- #region IHttpModule Members
-
- public void Init(HttpApplication context)
- {
- // Initializes the Application variable
- if (context.Application["sessionSortedList"] == null)
- {
- ASPNETContext = new System.Collections.Generic.SortedDictionary<string, SessionContext>();
- context.Application["sessionSortedList"] = ASPNETContext;
- }
-
- context.PostAcquireRequestState += new EventHandler(context_PostAcquireRequestState);
- }
-
- void context_PostAcquireRequestState(object sender, EventArgs e)
- {
- HttpApplication application = (HttpApplication)sender;
-
- // Get the Application Context variable
- var ASPNETContext = (SortedDictionary<string, SessionContext>)application.Application["sessionSortedList"];
- HttpContext context = application.Context;
- string filePath = context.Request.FilePath;
- string fileExtension = VirtualPathUtility.GetExtension(filePath);
-
- if (fileExtension.Equals(".aspx"))
- {
- if (application.Context.Session != null)
- {
- // Get the User Name
- string userName = (application.Session != null) ? (string)application.Session["userName"] : string.Empty;
- userName = userName ?? string.Empty;
-
- //Try to get the current session
- SessionContext currentSessionContext = null;
- ASPNETContext.TryGetValue(userName, out currentSessionContext);
-
- if (currentSessionContext != null)
- {
- // Validates old sessions
- bool session = currentSessionContext.SessionID == application.Session.SessionID;
-
- if (!session)
- {
- // Sing out
- FormsAuthentication.SignOut();
-
- // Remove from Session
- application.Session.Clear();
- application.Session.Abandon();
- application.Context.Response.Cookies["ASP.NET_SessionId"].Value = "";
-
- // Redirect
- FormsAuthentication.RedirectToLoginPage();
- }
- }
- }
- }
- }
-
- public void Dispose() { }
-
- #endregion
- }
- }
When Logging In the user, add the following code to the LoggedIn event of the Login control:
Code Snippet
- void Login1_LoggedIn(object sender, EventArgs e)
- {
- if (HttpContext.Current.Session != null)
- {
- string sessionID = Session.SessionID;
- string userName = Encoder.HtmlEncode(Login1.UserName);
- DateTime dateStarted = DateTime.Now;
-
- Session["userName"] = userName;
-
- // Get the Application Context variable
- var ASPNETContext = (SortedDictionary<string, SessionContext>)Application["sessionSortedList"];
-
- // Create a new SessionContext variable
- var sContext = new SessionContext() { SessionID = sessionID, UserName = userName };
-
- // Refresh the object to the Application
- if (ASPNETContext != null)
- {
- ASPNETContext[userName] = sContext;
- Application["sessionSortedList"] = ASPNETContext;
- }
- }
- }
When Logging Out the user, add the following code to the LoggingOut event of the LoginStatus control:
Code Snippet
- void LoginStats_LoggingOut(object sender, LoginCancelEventArgs e)
- {
- string userName = (string)Session["userName"];
- userName = userName ?? string.Empty;
-
- // Get the Application Context variable
- var ASPNETContext = (SortedDictionary<string, SessionContext>)Application["sessionSortedList"];
-
- //Try to get the current list
- SessionContext currentSessionContext = null;
- if (ASPNETContext != null)
- {
- ASPNETContext.TryGetValue(userName, out currentSessionContext);
-
- // Refresh the object to the Application
- if (currentSessionContext != null)
- {
- ASPNETContext.Remove(userName);
- Application["sessionSortedList"] = ASPNETContext;
- }
- }
-
- FormsAuthentication.SignOut();
- Session.Clear();
- Session.Abandon();
- HttpContext.Current.Response.Cookies["ASP.NET_SessionId"].Value = "";
- }
Other task required for the module to run is that it needs to be added to a section in the web.config.
If you are running IIS 7 in classic mode or earlier versions of IIS:
Code Snippet
- <system.web>
- <httpModules>
- <!-- any other modules above -->
- <addname="SessionManagerModule"type="Core.SessionManagerModule, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=TYPEYOURKEYHERE" />
- </httpModules>
- </system.web>
If you are running IIS7 in integrated mode:
Code Snippet
- <system.web>
- <httpModules>
- <!-- any other modules above -->
- <addname="SessionManagerModule"type="Core.SessionManagerModule, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=TYPEYOURKEYHERE" />
- </httpModules>
- </system.web>
- <system.webServer>
- <modulesrunAllManagedModulesForAllRequests="true">
- <!-- any other modules above -->
- <addname="SessionManagerModule"type="Core.SessionManagerModule, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=TYPEYOURKEYHERE" />
- </modules>
- </system.webServer>
Note: As best practice, in SharePoint you need to create a feature to apply any changes in the web.config.
I hope this solution helps you.
Please have a look at the References if you need more details.
References:
http://msdn.microsoft.com/en-us/library/ms178329.aspx
http://msdn.microsoft.com/en-us/library/system.web.httpapplication.aspx
Cheers,
Marcel Medina
Click here to read the same content in Portuguese.