Securing User Sessions: Implementing Multi-Tab/Multi Browser Auto Logout in .NET Core

Introduction

In modern web applications, ensuring that user sessions are managed securely and consistently across multiple tabs is crucial. This tutorial focuses on implementing session auto logout across multiple tabs in a .NET Core application. By following this guide, you'll learn how to synchronize logout actions, so when a user logs out from one tab, all other open tabs automatically reflect this change. This not only enhances security but also improves the user experience by preventing any inconsistencies in session state across different tabs. Whether you are developing single-page applications or traditional web apps, these techniques will help you maintain robust session management in your .NET Core applications.

Step 1. Create a Modal class as below

    public class Session
    {
        public int? UserId { get; set; }
        public string Sessionid { get; set; }
    }

Step 2. In the Login post method capture the session ID and user Login ID below


#region--------------------------Insert Session into DB---------------
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
Session obj = new Session();
obj.UserId = loggedInUser.UserId;
obj.Sessionid = HttpContext.Session.Id;
var SaveSession = _commonRepository.SessionInsert(obj).Result;                            
#endregion-------------------------------------------------------------

Step 3. Create an Interface and add the below code

Task<CustomerResult> SessionInsert(Session customer);
Task<CustomerResult> DeleteSessionId(string sessionid, int userid);
Task<IEnumerable<Session>> GetSessionExist(string sessionid, int userid);

Step 4. Create a Repository and add the below code

 public Task<CustomerResult> SessionInsert(Session customer)
 {
     DynamicParameters param = new DynamicParameters();
     object[] objArray = new object[] {
             "ACTION","A",
             "UserId",customer.UserId,
             "Session",customer.Sessionid
        };
     param = objArray.ToDynamicParameters("PAR_OUT");
     var result = Connection.Query<string>("USP_M_SESSIONSTORE", param, commandType: System.Data.CommandType.StoredProcedure);
     string response = param.Get<string>("PAR_OUT");
     CustomerResult customerResult = new CustomerResult() { Remark = response };
     return Task.FromResult(customerResult);
 }
 public Task<CustomerResult> DeleteSessionId(string sessionid, int userid)
 {
     DynamicParameters param = new DynamicParameters();
     object[] objArray = new object[] {
             "ACTION","B",
             "Session",sessionid,
             "UserId",userid
        };
     param = objArray.ToDynamicParameters("PAR_OUT");
     var result = Connection.Query<string>("USP_M_SESSIONSTORE", param, commandType: System.Data.CommandType.StoredProcedure);
     string response = param.Get<string>("PAR_OUT");
     CustomerResult customerResult = new CustomerResult() { Remark = response };
     return Task.FromResult(customerResult);
 }
 public async Task<IEnumerable<Session>> GetSessionExist(string session, int userid)
 {
     DynamicParameters _params = new DynamicParameters();
     _params.Add("ACTION", "C");
     _params.Add("Session", session);
     _params.Add("UserId", userid);

     var result = await Connection.QueryAsync<Session>("USP_M_SESSIONSTORE", _params, commandType: CommandType.StoredProcedure);
     return result;
 }

Step 5. Tables and Procedure code


CREATE TABLE [dbo].[M_SESSION](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[UserId] [int] NULL,
	[SessionId] [varchar](50) NULL,
	[deletedflag] [bit] NULL,
	[CreatedOn] [datetime] NULL,
 CONSTRAINT [PK_SessionTable] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
 CREATE TABLE [dbo].[T_Transaction_Error](
	[intErrorId] [int] IDENTITY(1,1) NOT NULL,
	[vchErrorMessage] [varchar](512) NULL,
	[dtmErrorDate] [datetime] NULL,
	[vchProcedureName] [varchar](80) NULL,
	[vchLineNumber] [varchar](250) NULL,
	[vchErrorProcedure] [varchar](512) NULL,
 CONSTRAINT [PK_T_Transaction_Error] PRIMARY KEY CLUSTERED 
(
	[intErrorId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]
 
CREATE PROCEDURE [dbo].[USP_M_SESSIONSTORE]
  (
   @UserId INT=NULL,
   @Session VARCHAR(50)='',
   @ACTION CHAR(1)=NULL,
   @PAR_OUT VARCHAR(24)=NULL OUTPUT
  )
AS
 
BEGIN
BEGIN TRY
	BEGIN TRAN 
	
		SET NOCOUNT ON;
		SET XACT_ABORT ON;
	
		IF @ACTION='A'
			BEGIN
			 INSERT INTO M_SESSION
				(
					UserId,
					SessionId,
					CreatedOn,
					deletedflag
				)
				VALUES
				(
					@UserId,
					@Session,
					GETDATE(),
					0
				)
			 SET @PAR_OUT='1';
			END
		ELSE IF @ACTION='B'
			BEGIN
				/*--update SessionTable set deletedflag=1 where UserId=@UserId*/
				DELETE M_SESSION WHERE UserId=@UserId
				SET @PAR_OUT='2';
			END
		ELSE IF @ACTION='C'
			BEGIN
				select * 
				from M_SESSION 
				where SessionId=@Session 
					and deletedflag=0
					and UserId=@UserId
			END

	COMMIT
END TRY
BEGIN CATCH
		
		IF(@@TRANCOUNT>0)
		BEGIN
			ROLLBACK;
		END

		INSERT INTO T_TRANSACTION_ERROR
			(
			 VCHERRORMESSAGE
			,DTMERRORDATE
			,VCHPROCEDURENAME
			,[vchLineNumber]
			) 
		VALUES
			(
			 ERROR_MESSAGE()
			 ,GETDATE()
			 ,'SessionStore'
			 ,ERROR_LINE()
			 );

		SET @PAR_OUT='0';
END CATCH
END;

Step 6. In the Logout method add the below code

  [HttpGet]
  public IActionResult Logout()
  {
      LoggedInUser profile = HttpContext.Session.Get<LoggedInUser>(KeyHelper.UserKey);
      if (profile != null)
      {
     var DeleteSession = _commonRepository.DeleteSessionId(HttpContext.Session.Id, profile.UserId);
      }
      return RedirectToAction("Index", "Website");
  }
        

Step 7. Add the CheckSession method in the account controller


public IActionResult CheckSession()
{
    LoggedInUser profile = HttpContext.Session.Get<LoggedInUser>(KeyHelper.UserKey);
    if (profile != null)
    {
        var sessionExists = _commonRepository.GetSessionExist(HttpContext.Session.Id, profile.UserId).Result;
        if (sessionExists.Count() != 0)
        {
            return Ok("valid");
        }
        else
        {
            return Ok("expired");
        }
    }
    else
    {
        return RedirectToAction("Logout", "Account");
    }
}

Step 8. In the layout page add the below JavaScript code

<script type="text/javascript">
    (function poll() {
        setTimeout(function () {
            $.ajax({
                url: '/Account/CheckSession',
                success: function (data) {
                    if (data === 'expired') {
                        // Session expired, perform logout action
                        window.location.href = "/Account/Logout";
                    } else {
                        // Session still valid, continue polling
                        poll();
                    }
                },
                error: function (xhr, status, error) {
                    // Handle error
                    console.error(error);
                }
            });
        }, 8000); // Poll every 5 seconds
    })();

</script>

Conclusion

Implementing session auto logout across multiple tabs in .NET Core is crucial for enhancing security and protecting sensitive user data. By ensuring that sessions expire after a specified period of inactivity, you mitigate the risk of unauthorized access and potential data breaches.

Throughout this guide, we have explored various techniques to achieve this functionality, such as utilizing session expiration policies, leveraging client-side scripting, and employing server-side mechanisms to track user activity. Each approach offers its advantages and considerations, allowing developers to choose the most suitable method based on their project requirements and constraints.

By incorporating session auto logout functionality into your .NET Core applications, you not only enhance security but also provide a seamless and user-friendly experience. Users can confidently interact with your application across multiple tabs, knowing that their sessions will be terminated after a period of inactivity, safeguarding their privacy and sensitive information.

In conclusion, implementing session auto logout across multiple tabs in .NET Core is an essential aspect of developing secure and robust web applications. By following best practices and leveraging appropriate techniques, developers can effectively manage user sessions and mitigate the risk of unauthorized access, thereby enhancing the overall security posture of their applications.