SharePoint 4 Developers

Additional reference guide in .NET / SharePoint Development

Limit only one session per account

Restrict multiple concurrent user sessions. Applicable to both SharePoint/ASP.NET web sites. Limit to only one session per user account.

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:

diagram
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
  1. using System;
  2.  
  3. namespace Core
  4. {
  5.     public class SessionContext
  6.     {
  7.         public string UserName { get; set; }
  8.         public string SessionID { get; set; }
  9.     }
  10. }

 

The core part of the solution is the httpModule that intercepts the requests:

Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Web;
  4. using System.Web.Security;
  5.  
  6. namespace Core
  7. {
  8.     public class SessionManagerModule :IHttpModule
  9.     {
  10.         public SortedDictionary<string, SessionContext> ASPNETContext { get; set; }
  11.  
  12.         #region IHttpModule Members
  13.  
  14.         public void Init(HttpApplication context)
  15.         {
  16.             // Initializes the Application variable
  17.             if (context.Application["sessionSortedList"] == null)
  18.             {
  19.                 ASPNETContext = new System.Collections.Generic.SortedDictionary<string, SessionContext>();
  20.                 context.Application["sessionSortedList"] = ASPNETContext;
  21.             }
  22.  
  23.             context.PostAcquireRequestState += new EventHandler(context_PostAcquireRequestState);
  24.         }
  25.  
  26.         void context_PostAcquireRequestState(object sender, EventArgs e)
  27.         {
  28.             HttpApplication application = (HttpApplication)sender;
  29.  
  30.             // Get the Application Context variable
  31.             var ASPNETContext = (SortedDictionary<string, SessionContext>)application.Application["sessionSortedList"];
  32.             HttpContext context = application.Context;
  33.             string filePath = context.Request.FilePath;
  34.             string fileExtension = VirtualPathUtility.GetExtension(filePath);
  35.  
  36.             if (fileExtension.Equals(".aspx"))
  37.             {
  38.                 if (application.Context.Session != null)
  39.                 {
  40.                     // Get the User Name
  41.                     string userName = (application.Session != null) ? (string)application.Session["userName"] : string.Empty;
  42.                     userName = userName ?? string.Empty;
  43.  
  44.                     //Try to get the current session
  45.                     SessionContext currentSessionContext = null;
  46.                     ASPNETContext.TryGetValue(userName, out currentSessionContext);
  47.  
  48.                     if (currentSessionContext != null)
  49.                     {
  50.                         // Validates old sessions
  51.                         bool session = currentSessionContext.SessionID == application.Session.SessionID;
  52.  
  53.                         if (!session)
  54.                         {
  55.                             // Sing out
  56.                             FormsAuthentication.SignOut();
  57.  
  58.                             // Remove from Session
  59.                             application.Session.Clear();
  60.                             application.Session.Abandon();
  61.                             application.Context.Response.Cookies["ASP.NET_SessionId"].Value = "";
  62.  
  63.                             // Redirect
  64.                             FormsAuthentication.RedirectToLoginPage();
  65.                         }
  66.                     }
  67.                 }
  68.             }
  69.         }
  70.  
  71.         public void Dispose() { }
  72.  
  73.         #endregion
  74.     }
  75. }

 

When Logging In the user, add the following code to the LoggedIn event of the Login control:

Code Snippet
  1. void Login1_LoggedIn(object sender, EventArgs e)
  2. {
  3.     if (HttpContext.Current.Session != null)
  4.     {
  5.         string sessionID = Session.SessionID;
  6.         string userName = Encoder.HtmlEncode(Login1.UserName);
  7.         DateTime dateStarted = DateTime.Now;
  8.  
  9.         Session["userName"] = userName;
  10.  
  11.         // Get the Application Context variable
  12.         var ASPNETContext = (SortedDictionary<string, SessionContext>)Application["sessionSortedList"];
  13.  
  14.         // Create a new SessionContext variable
  15.         var sContext = new SessionContext() { SessionID = sessionID, UserName = userName };
  16.  
  17.         // Refresh the object to the Application
  18.         if (ASPNETContext != null)
  19.         {
  20.             ASPNETContext[userName] = sContext;
  21.             Application["sessionSortedList"] = ASPNETContext;
  22.         }
  23.     }
  24. }

 

When Logging Out the user, add the following code to the LoggingOut event of the LoginStatus control:

Code Snippet
  1. void LoginStats_LoggingOut(object sender, LoginCancelEventArgs e)
  2. {
  3.     string userName = (string)Session["userName"];
  4.     userName = userName ?? string.Empty;
  5.  
  6.     // Get the Application Context variable
  7.     var ASPNETContext = (SortedDictionary<string, SessionContext>)Application["sessionSortedList"];
  8.  
  9.     //Try to get the current list
  10.     SessionContext currentSessionContext = null;
  11.     if (ASPNETContext != null)
  12.     {
  13.         ASPNETContext.TryGetValue(userName, out currentSessionContext);
  14.  
  15.         // Refresh the object to the Application
  16.         if (currentSessionContext != null)
  17.         {
  18.             ASPNETContext.Remove(userName);
  19.             Application["sessionSortedList"] = ASPNETContext;
  20.         }
  21.     }
  22.  
  23.     FormsAuthentication.SignOut();
  24.     Session.Clear();
  25.     Session.Abandon();
  26.     HttpContext.Current.Response.Cookies["ASP.NET_SessionId"].Value = "";
  27. }

 

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
  1. <system.web>
  2.   <httpModules>
  3.     <!-- any other modules above -->
  4.     <addname="SessionManagerModule"type="Core.SessionManagerModule, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=TYPEYOURKEYHERE" />
  5.   </httpModules>
  6. </system.web>

 

If you are running IIS7 in integrated mode:

Code Snippet
  1. <system.web>
  2.   <httpModules>
  3.     <!-- any other modules above -->
  4.     <addname="SessionManagerModule"type="Core.SessionManagerModule, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=TYPEYOURKEYHERE" />
  5.   </httpModules>
  6. </system.web>
  7. <system.webServer>
  8.   <modulesrunAllManagedModulesForAllRequests="true">
  9.     <!-- any other modules above -->
  10.     <addname="SessionManagerModule"type="Core.SessionManagerModule, Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=TYPEYOURKEYHERE" />
  11.   </modules>
  12. </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.

 

caml.net.intellisense

A Visual Studio Extension called caml.net.intellisense intends to facilitate the development by displaying details of the properties via IntelliSense.

Hi folks,

I have been using some extensions and tools in the SharePoint development and also to create SharePoint environments. They can be useful in your daily tasks.

As a SharePoint Best practice whenever possible try creating SharePoint objects utilising schemas (XML). This requires understanding of CAML (Collaborative Application Markup Language).

A Visual Studio Extension called caml.net.intellisense intends to facilitate the development by displaying details of the properties via IntelliSense.

The Figure below displays how the custom Intellisense works:

Intellisense

Figure 1 – Intellisense Details

This extension helps if you do not have time to search details of a property on the net. It's worth a look:

http://visualstudiogallery.msdn.microsoft.com/15055544-fda0-42db-a603-6dc32ed26fde

Reference:
http://msdn.microsoft.com/en-us/library/dd588092(v=office.11).aspx

Cheers,

Marcel Medina

Click here to read the same content in Portuguese.

ILSpy - Open-Source Tool

ILSpy is the open-source .NET assembly browser and decompiler.

Hi there,

A great tool was released this year to replace .NET reflector, that is not free anymore!

ILSpy is the open-source .NET assembly browser and decompiler:
http://wiki.sharpdevelop.net/ILSpy.ashx

From now on use this tool to investigate the Intermediate language. It does everything .NET Reflector did.

Cheers,

Marcel Medina

Click here to read the same content in Portuguese.