JillElaine / jquery-idleTimeout

Idle activity timeout and logout redirect for jQuery for multiple windows & tabs
Other
73 stars 79 forks source link

Session lost even if clicked on Stay Logged In if counter is <=5 seconds #18

Open nicolenielsen opened 9 years ago

nicolenielsen commented 9 years ago

Hi,

First of all, great plugin. However, I'm encountering intermittent issues such that my session timeout is set to 30 minutes, I set the idleTimeActivity to 27 minutes and the countdown timer to 3 minutes. When the timer shows up in 27 minutes in, I wait for the countdown timer to count until less than 5 seconds, and clicks on Stay Logged In button. Sometimes, the session is gone but sometimes it remains.

Any idea why this happens? Is it because ASP.NET's session expires sooner?

Thank you. Any help would be greatly appreciated!

JillElaine commented 9 years ago

First of all, there is nothing in the jquery-idleTimeout plugin's code that does anything directly to the user's session. The plugin can 'ping' the server regularly (configurable via sessionKeepAliveTimer & sessionKeepAliveUrl variables) to let the server know the user is still alive, and, hopefully, keep the server from 'killing' the user's session before the user is 'done'.

You can turn this ping off completely: set sessionKeepAliveTimer=false. By default, the plugin pings the server every 10 minutes.

Make sure the sessionKeepAliveUrl variable is set to a page within your site. The server must 'see' the pings, so it 'thinks' the user is still alive and active. The sessionKeepAliveUrl default is '/'.

Note that the plugin does not ping the server while the warning dialog (countdown timer) is visible. Your server (sometimes) kills the session when the warning dialog is visible, so this indicates to me that your server session timeout setting is shorter than the interval between pings. Or maybe the plugin's pings are not 'seen' by the server for some reason?

ASP's default session timeout is 20 minutes. Typically, you'd want to set the server's session timeout to greater than your app's session timeout. For example, if you want users to be 'timed out' after 30 minutes of inactivity, set the server's session timeout to 35 minutes. A basic ASP session tutorial is here: http://www.w3schools.com/asp/asp_ref_session.asp

Sometimes the session can be lost even though the user is active (pings, new pages, etc). This page gives some other possibilities for timeouts: http://machinesaredigging.com/2013/10/29/how-does-a-web-session-work/

Make sure you use the correct variable names to configure the plugin: spelling and case matters. It's idleTimeLimit, not 'idleTimeActivity', though I think that's just a typo above, right? https://github.com/JillElaine/jquery-idleTimeout/wiki/Public-Configuration-Variables

Post back what you learn so that others may benefit! Thank you!

JillElaine commented 9 years ago

Also, please read this: http://stackoverflow.com/questions/648992/session-timeout-in-asp-net

nicolenielsen commented 9 years ago

Hi,

Thank you for your reply. I have set the sessionKeepAliveTimer = 600 but did not specify a sessionKeepAliveUrl. I believe this will get the current url instead right? I have also checked in firebug that the url is successfully being 'ping-ed'. Also my session timeout (set in web.config and IIS) is already set to 30 minutes, so it's larger than the interval between the pings. And you're right, the idleTimeActivity is only a typo. I'm using the correct name.

I tried setting the idleTimeLimit to a value less than 30 minutes, however, I still get timed out if clicking on Stay Logged In with only a few seconds left (<=5). I'm using MVC and OWIN authentication, the cookie being created by OWIN is also set to 30 minutes timespan expiration. Is there any chance someone have encountered this issue?

Many thanks!

JillElaine commented 9 years ago

You should load the jquery-idleTimeout-for-testing.js and watch the console in Firebug. What happens when you click the Stay Logged In button? Are you redirected to the logout page anyway?

Again, there is nothing in the jquery-idleTimeout code that actually causes a user timeout: all it does is track idleness.

It is up to the implementer (you) to cause your idle users to timeout. Typically this is done with code that executes when the user is redirected to the logout page...or with code added to the optional customCallback variable. How have you implemented this timeout for your idle users? How do you log out your idle users? Are you using the customCallback?

No one else has reported this problem, so I am puzzled. I am not familiar with ASP,sorry. Is the OWIN cookie set to expire after 30 minutes even if the user is not idle? What happens if you set all your server side timeouts to 35 minutes?

JillElaine commented 9 years ago

Okay, I can confirm that there is a bug. The plugin does 'time out' just a few seconds before the 'countdown' completes. In other words, Time Remaining may display 2-3 seconds left when timeout occurs. I will patch this as soon as possible. Thank you for your patience.

clevelandj commented 8 years ago

Hello,

I was wondering whether any progress has been made on this issue? I am running into the same problem.

Thanks!

JillElaine commented 8 years ago

I have not had time to work on code recently, but I think the code below will fix the problem. Would you please try it, and let me know?

The problem occurred because there is typically a several second delay between the starting of the dialog timer and the display of the warning dialog. The only change in the code below is to test that the warning dialog 'Time Remaining' display has reached zero before logging out the user. The result will be that the user will be given a few seconds 'grace' before being logged out.

/**
 * This work is licensed under the Creative Commons Attribution-Share Alike 3.0
 * United States License. To view a copy of this license,
 * visit http://creativecommons.org/licenses/by-sa/3.0/us/ or send a letter
 * to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
 *
 * Modified by: Jill Elaine
 * Email: jillelaine01@gmail.com
 *
 * Configurable idle (no activity) timer and logout redirect for jQuery.
 * Works across multiple windows and tabs from the same domain.
 *
 * Dependencies: JQuery v1.7+, JQuery UI, store.js from https://github.com/marcuswestin/store.js - v1.3.4+
 *
 * version 1.0.10
 **/

/*global jQuery: false, document: false, store: false, clearInterval: false, setInterval: false, setTimeout: false, clearTimeout: false, window: false, alert: false*/
/*jslint indent: 2, sloppy: true, plusplus: true*/

(function ($) {

  $.fn.idleTimeout = function (userRuntimeConfig) {

    //##############################
    //## Public Configuration Variables
    //##############################
    var defaultConfig = {
      redirectUrl: '/logout',      // redirect to this url on logout. Set to "redirectUrl: false" to disable redirect

      // idle settings
      idleTimeLimit: 1200,           // 'No activity' time limit in seconds. 1200 = 20 Minutes
      idleCheckHeartbeat: 2,       // Frequency to check for idle timeouts in seconds

      // optional custom callback to perform before logout
      customCallback: false,       // set to false for no customCallback
      // customCallback:    function () {    // define optional custom js function
          // perform custom action before logout
      // },

      // configure which activity events to detect
      // http://www.quirksmode.org/dom/events/
      // https://developer.mozilla.org/en-US/docs/Web/Reference/Events
      activityEvents: 'click keypress scroll wheel mousewheel mousemove', // separate each event with a space

      // warning dialog box configuration
      enableDialog: true,           // set to false for logout without warning dialog
      dialogDisplayLimit: 180,       // Time to display the warning dialog before logout (and optional callback) in seconds. 180 = 3 Minutes
      dialogTitle: 'Session Expiration Warning', // also displays on browser title bar
      dialogText: 'Because you have been inactive, your session is about to expire.',
      dialogTimeRemaining: 'Time remaining',
      dialogStayLoggedInButton: 'Stay Logged In',
      dialogLogOutNowButton: 'Log Out Now',

      // error message if https://github.com/marcuswestin/store.js not enabled
      errorAlertMessage: 'Please disable "Private Mode", or upgrade to a modern browser. Or perhaps a dependent file missing. Please see: https://github.com/marcuswestin/store.js',

      // server-side session keep-alive timer
      sessionKeepAliveTimer: 600,   // ping the server at this interval in seconds. 600 = 10 Minutes. Set to false to disable pings
      sessionKeepAliveUrl: window.location.href // set URL to ping - does not apply if sessionKeepAliveTimer: false
    },

    //##############################
    //## Private Variables
    //##############################
      currentConfig = $.extend(defaultConfig, userRuntimeConfig), // merge default and user runtime configuration
      origTitle = document.title, // save original browser title
      activityDetector,
      startKeepSessionAlive, stopKeepSessionAlive, keepSession, keepAlivePing, // session keep alive
      idleTimer, remainingTimer, checkIdleTimeout, checkIdleTimeoutLoop, startIdleTimer, stopIdleTimer, // idle timer
      openWarningDialog, dialogTimer, checkDialogTimeout, startDialogTimer, stopDialogTimer, isDialogOpen, destroyWarningDialog, countdownDisplay, dialogDisplaySeconds, // warning dialog
      logoutUser;

    //##############################
    //## Public Functions
    //##############################
    // trigger a manual user logout
    // use this code snippet on your site's Logout button: $.fn.idleTimeout().logout();
    this.logout = function () {
      store.set('idleTimerLoggedOut', true);
    };

    //##############################
    //## Private Functions
    //##############################

    //----------- KEEP SESSION ALIVE FUNCTIONS --------------//
    startKeepSessionAlive = function () {

      keepSession = function () {
        $.get(currentConfig.sessionKeepAliveUrl);
        startKeepSessionAlive();
      };

      keepAlivePing = setTimeout(keepSession, (currentConfig.sessionKeepAliveTimer * 1000));
    };

    stopKeepSessionAlive = function () {
      clearTimeout(keepAlivePing);
    };

    //----------- ACTIVITY DETECTION FUNCTION --------------//
    activityDetector = function () {

      $('body').on(currentConfig.activityEvents, function () {

        if (!currentConfig.enableDialog || (currentConfig.enableDialog && isDialogOpen() !== true)) {
          startIdleTimer();
        }
      });
    };

    //----------- IDLE TIMER FUNCTIONS --------------//
    checkIdleTimeout = function () {

      var timeIdleTimeout = (store.get('idleTimerLastActivity') + (currentConfig.idleTimeLimit * 1000));

      if ($.now() > timeIdleTimeout) {

        if (!currentConfig.enableDialog) { // warning dialog is disabled
          logoutUser(); // immediately log out user when user is idle for idleTimeLimit
        } else if (currentConfig.enableDialog && isDialogOpen() !== true) {
          openWarningDialog();
          startDialogTimer(); // start timing the warning dialog
        }
      } else if (store.get('idleTimerLoggedOut') === true) { //a 'manual' user logout?
        logoutUser();
      } else {

        if (currentConfig.enableDialog && isDialogOpen() === true) {
          destroyWarningDialog();
          stopDialogTimer();
        }
      }
    };

    startIdleTimer = function () {
      stopIdleTimer();
      store.set('idleTimerLastActivity', $.now());
      checkIdleTimeoutLoop();
    };

    checkIdleTimeoutLoop = function () {
      checkIdleTimeout();
      idleTimer = setTimeout(checkIdleTimeoutLoop, (currentConfig.idleCheckHeartbeat * 1000));
    };

    stopIdleTimer = function () {
      clearTimeout(idleTimer);
    };

    //----------- WARNING DIALOG FUNCTIONS --------------//
    openWarningDialog = function () {

      var dialogContent = "<div id='idletimer_warning_dialog'><p>" + currentConfig.dialogText + "</p><p style='display:inline'>" + currentConfig.dialogTimeRemaining + ": <div style='display:inline' id='countdownDisplay'></div></p></div>";

      $(dialogContent).dialog({
        buttons: [{
          text: currentConfig.dialogStayLoggedInButton,
          click: function () {
            destroyWarningDialog();
            stopDialogTimer();
            startIdleTimer();
          }
        },
          {
            text: currentConfig.dialogLogOutNowButton,
            click: function () {
              logoutUser();
            }
          }
          ],
        closeOnEscape: false,
        modal: true,
        title: currentConfig.dialogTitle,
        open: function () {
          $(this).closest('.ui-dialog').find('.ui-dialog-titlebar-close').hide();
        }
      });

      countdownDisplay();

      document.title = currentConfig.dialogTitle;

      if (currentConfig.sessionKeepAliveTimer) {
        stopKeepSessionAlive();
      }
    };

    checkDialogTimeout = function () {
      var timeDialogTimeout = (store.get('idleTimerLastActivity') + (currentConfig.idleTimeLimit * 1000) + (currentConfig.dialogDisplayLimit * 1000));

      if ((($.now() > timeDialogTimeout) && (dialogDisplaySeconds <= 0)) || (store.get('idleTimerLoggedOut') === true)) {
        logoutUser();
      }
    };

    startDialogTimer = function () {
      dialogTimer = setInterval(checkDialogTimeout, (currentConfig.idleCheckHeartbeat * 1000));
    };

    stopDialogTimer = function () {
      clearInterval(dialogTimer);
      clearInterval(remainingTimer);
    };

    isDialogOpen = function () {
      var dialogOpen = $("#idletimer_warning_dialog").is(":visible");

      if (dialogOpen === true) {
        return true;
      }
      return false;
    };

    destroyWarningDialog = function () {
      $("#idletimer_warning_dialog").dialog('destroy').remove();
      document.title = origTitle;

      if (currentConfig.sessionKeepAliveTimer) {
        startKeepSessionAlive();
      }
    };

    countdownDisplay = function () {
      dialogDisplaySeconds = currentConfig.dialogDisplayLimit, mins, secs;

      remainingTimer = setInterval(function () {
        mins = Math.floor(dialogDisplaySeconds / 60); // minutes
        if (mins < 10) { mins = '0' + mins; }
        secs = dialogDisplaySeconds - (mins * 60); // seconds
        if (secs < 10) { secs = '0' + secs; }
        $('#countdownDisplay').html(mins + ':' + secs);
        dialogDisplaySeconds -= 1;
      }, 1000);
    };

    //----------- LOGOUT USER FUNCTION --------------//
    logoutUser = function () {
      store.set('idleTimerLoggedOut', true);

      if (currentConfig.sessionKeepAliveTimer) {
        stopKeepSessionAlive();
      }

      if (currentConfig.customCallback) {
        currentConfig.customCallback();
      }

      if (currentConfig.redirectUrl) {
        window.location.href = currentConfig.redirectUrl;
      }
    };

    //###############################
    // Build & Return the instance of the item as a plugin
    // This is your construct.
    //###############################
    return this.each(function () {

      if (store.enabled) {

        store.set('idleTimerLastActivity', $.now());
        store.set('idleTimerLoggedOut', false);

        activityDetector();

        if (currentConfig.sessionKeepAliveTimer) {
          startKeepSessionAlive();
        }

        startIdleTimer();

      } else {
        alert(currentConfig.errorAlertMessage);
      }

    });
  };
}(jQuery));
clevelandj commented 8 years ago

Thanks @JillElaine! The fix you posted above mostly worked. I made two slight modifications and submitted a pull request (#31). The changes:

    countdownDisplay = function () {
      dialogDisplaySeconds = currentConfig.dialogDisplayLimit, mins, secs;

changed to

    countdownDisplay = function () {
      dialogDisplaySeconds = currentConfig.dialogDisplayLimit;
      var mins, secs;

as an error was thrown otherwise (claiming 'mins' wasn't defined). I also changed

    countdownDisplay = function () {
        ...
        dialogDisplaySeconds -= 1;
      }, 1000);
    };

to

    countdownDisplay = function () {
        ...
        if (dialogDisplaySeconds) { dialogDisplaySeconds -= 1; }
      }, 1000);
    };

in order to avoid displaying 'NaN' in the dialog after the counter had reached 0.

JillElaine commented 8 years ago

Thank you for this suggested change for the countdownDisplay to avoid NaN. I don't have time to work on my code now, but hope to improve it in the future.