Handling Session Timeout Gracefully
Nerd-Face
Have you ever been to those sites that show you a nice helpful popup to let you know that your session will expire in a few minutes? You know, the ones that show you a countdown timer, and let you Continue Your Session, or Log Out? And if you’ve been gone so long that your session really did expire because you were feeding your nerd-face, it automatically logs you out and shows you a nice message letting you know it was only to protect you? That it was For Your Own Good?
Yeah, me too! Then I said, “I want one!” and set out to build my own.
Gravy
Here’s what it looks like when it first pops up:
When you click “Continue”, the dialog box closes and re-establishes the session. If you then stay idle for a while, the dialog box will reopen and the countdown starts again.
When you click “Log Out”, the dialog box closes, logs you out, and sends you to the home page. It includes a return URL, so if you log in again, then Hey!, you’re right back where you were.
Oh, and check this out: it dynamically updates the HTML title, which (at least on Windows) will show you the countdown in your task bar. That’s just gravy!
But that’s just how I do it. Everything in the code is easily tweakable. And there’s just not that much code.
Bottle Opener
Bloggers have to make assumptions. And sometimes the code we write has dependencies. These are mine:
- I assume you are clever and funky
- I assume you know HTML, JavaScript, and a little jQuery
- I assume you own a bottle opener
- I assume you value quality
- My code depends on jQuery and jQuery UI. You don’t have to have them to make this approach work, but it simplifies some things.
- My server-side code is written in ASP.NET MVC 3. It is part of a larger framework that includes ASP.NET Membership, T4MVC, and StructureMap for dependency injection. Again, you don’t have to have these, but you will need some sort of back-end that supports sessions. Otherwise, why are you here? Were you just searching for “gravy” and found this?
- I assume you like pumpkin pie
But really, I wish I didn’t have to assume and depend so much. But where I do, I’ll try to at least link to info that will be helpful.
Gnarly Diamond
The magic in this code comes from the power of JavaScript, which is a dark and dangerous art in the wrong hands, and some kind of gnarly diamond if you’re Douglas Crockford. His book is awesome, very dense, and makes a whole lot of assumptions about its audience.
My code follows the Module Pattern (invented by Mr. Crockford), which minimizes global variables, and allows you to decide what in your code is public and private. As for the countdown timer, that’s just those old stand-bys setTimeout(functionName, milliseconds), which waits a specified number of milliseconds before calling your function, and setInterval(functionName, milliseconds), which calls your function relentlessly every [milliseconds] milliseconds.
Viola!
At this point, you are probably itchy to see some code. So then, viola! Let’s build up the JavaScript SessionManager module step by step:
Structure
First, here’s the basic JavaScript structure we’ll be using. Lots of pseudocode here, but it should give the basic idea. If you read the comments, you might learn a thing or two.
var SessionManager = function() {
// Private Variables will go here...
// Private Functions
// endSession: Called when the session expires
var endSession = function() {
// Close the dialog
// Redirect to the "expire" URL
};
// displayCountdown: Wrapper for updating the countdown display every second
var displayCountdown = function() {
// Inner countdown function, which we can call right away, then
// call every second (setInterval waits before its first function call)
var countdown = function() {
// Get countdown minutes and seconds
// Update the HTML title
// Update the countdown display
// If the countdown timer reaches zero,
// Update the HTML title to "Session Expired"
// Call the endSession() function
// Decrement the counter
};
// Call the countdown() function immediately
countdown();
// Call the countdown() function every second thereafter
window.setInterval(countdown, 1000);
};
// promptToExtendSession: Display the jQuery dialog
// and kick off the countdown timer
var promptToExtendSession = function() {
// Build up a jQuery dialog
// with buttons to "Continue" (extend session)
// and "Log Out" (end session)
displayCountdown();
};
// startSessionManager: Calls promptToExtendSession() after 5 minutes
var startSessionManager = function() {
window.setTimeout(promptToExtendSession, 300000);
};
// refreshSession: Refresh the session
var refreshSession = function() {
// Refresh the session using the "extend" session url
// Restart the countdown to the popup
startSessionManager();
};
// Public Functions
// These will be the only public methods available to outside callers
return {
// Start the session (call using SessionManager.start())
start: function() {
startSessionManager();
},
// Extend the session (call using SessionManager.extend())
extend: function() {
refreshSession();
}
};
}(); // See those parentheses? They will execute this function immediately and
// return the anonymous object with two public functions (start and extend)
// And now, we can call our public method to plant the popup seed!
SessionManager.start();
Fill ‘Er Up
Okay, so that’s the basic layout. Let’s fill in the blanks. Here we’ll add our private variables, our jQuery Dialog with real live countdown display, and implement the rest of our functions.
I’ll highlight some of the neato stuff in the comments.
var SessionManager = function() {
// Private Variables
var sessionTimeoutSeconds = 20 * 60, // Session timeout is 20 minutes
promptSeconds = 5 * 60, // Prompt shows for 5 minutes
secondsBeforePrompt = sessionTimeoutSeconds - countdownSeconds, // 15 minutes until prompt pops up
$dlg, // jQuery Dialog
displayCountdownIntervalId, // setInterval id, for clean up
promptToExtendSessionTimeoutId, // setTimeout id, for clean up
originalTitle = document.title, // grab the HTML <title> (for later)
extendSessionUrl = '/Session/Extend', // URL to call when extending session
expireSessionUrl = '/Session/Expire'; // URL to call when expiring session
// Private Functions
var endSession = function() {
$dlg.dialog('close'); // Close the jQuery Dialog
location.href = expireSessionUrl; // Redirect to the expiration URL
};
var displayCountdown = function() {
var countdown = function() {
var cd = new Date(count * 1000), // Returns milliseconds since 01/01/70
minutes = cd.getUTCMinutes(), // Grab the minutes
seconds = cd.getUTCSeconds(); // Grab the seconds
document.title = 'Expire in ' + minutes + ':' + seconds; // Update the HTML title
$('#sm-countdown').html(minutes + ':' + seconds); // Update the countdown display
if (count === 0) { // If we reached zero,
document.title = 'Session Expired'; // update the HTML title
endSession(); // and end the session
}
count--;
};
countdown(); // Call the function once
displayCountdownIntervalId = window.setInterval(countdown, 1000); // Call the function every second
};
var promptToExtendSession = function() {
$dlg = $('#sm-countdown-dialog').dialog({ // See the HTML below
title: 'Session Timeout Warning',
buttons: {
'Continue': function() {
$(this).dialog('close'); // Close the dialog
refreshSession(); // Refresh the session
document.title = originalTitle; // Change the title back
},
'Log Out': function() {
endSession(false); // End the session
}
}
});
count = promptSeconds; // Set our counter
displayCountdown(); // Show that dialog!
};
var refreshSession = function() {
window.clearInterval(displayCountdownIntervalId); // Stop calling countdown so
// we can start a new timer
var img = new Image(1, 1); // Create a tiny image
img.src = extendSessionUrl; // and set its source to the
// extend session url (like
// poor man's Ajax!)
window.clearTimeout(promptToExtendSessionTimeoutId); // Clear the timeout so we can...
startSessionManager(); // ... start it all over!
};
// Just a private implementation to actually start our countdown before popup
var startSessionManager = function() {
promptToExtendSessionTimeoutId = window.setTimeout(promptToExtendSession, secondsBeforePrompt * 1000);
};
// Public Functions
return {
start: function() {
startSessionManager();
},
extend: function() {
refreshSession();
}
};
}();
Two Monologues Do Not Make a Dialog
Here’s the HTML for the dialog:
<div id="sm-countdown-dialog" style="display:none">
<p>Your session will expire in:</p>
<div id="sm-countdown"><!-- Placeholder for dynamic countdown --></div>
<p>Click "Continue" to keep working, or "Log Out" if you are finished.</p>
</div>
Impatience
I also added support for testing this when you don’t want to wait 15 minutes for your popup to show up, and you don’t want to keep changing your session timeout on the back end and restarting your server…
It uses a couple query string variables to set the total session timeout (smt) and the number of seconds before the countdown dialog appears (smc). You can use it like this on any page where the JavaScript exists:
http://flumko.me/pork/edit?smt=30&smc=20
What that does is show the dialog after 20 seconds, and it will start a 10 second countdown before it expires the session.
Helpy Helperton
Things like getting a query string and padding a string are really utilities, and should be relegated to their own modules. To support this, I’m adding a new module called HtmlHelpers, so we can grab the query string value easily, then I just use that if provided, otherwise I fall back to the default (that’s the coolness of the || operator):
// HtmlHelpers Module
// Call by using HtmlHelpers.getQueryStringValue("myname");
var HtmlHelpers = function() {
return {
// Based on http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
getQueryStringValue: function(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace( /\+/g , ' '));
}
};
}();
var SessionManager = function() {
// Private Variables
var countdownSeconds = HtmlHelpers.getQueryStringValue('smc') || 300,
sessionTimeoutSeconds = HtmlHelpers.getQueryStringValue('smt') || 1200;
// Lots of code deleted here...
}();
I do something similar with another module called StringHelpers, which adds a padLeft function to pretty up our countdown display:
// StringHelpers Module
// Call by using StringHelpers.padLeft("1", "000");
var StringHelpers = function() {
return {
// Pad string using padMask. string '1' with padMask '000' will produce '001'.
padLeft: function(string, padMask) {
string = '' + string; // If it ain't a string, make it one (ye olde type coercion!)
return (padMask.substr(0, (padMask.length - string.length)) + string);
}
};
}();
The Final Countdown
So, here’s our final JavaScript code, incorporating everything we’ve seen so far, and a little bit more. Super shiny!
<!-- Countdown Dialog HTML -->
<div id="sm-countdown-dialog" style="display:none">
<p>Your session will expire in:</p>
<div id="sm-countdown"><!-- Placeholder for dynamic countdown --></div>
<p>Click "Continue" to keep working, or "Log Out" if you are finished.</p>
</div>
<script type="text/javascript">
$(function() { // Wrap it all in jQuery documentReady because we use jQuery UI Dialog
// HtmlHelpers Module
// Call by using HtmlHelpers.getQueryStringValue("myname");
var HtmlHelpers = function() {
return {
// Based on http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript
getQueryStringValue: function(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace( /\+/g , ' '));
}
};
}();
// StringHelpers Module
// Call by using StringHelpers.padLeft("1", "000");
var StringHelpers = function() {
return {
// Pad string using padMask. string '1' with padMask '000' will produce '001'.
padLeft: function(string, padMask) {
string = '' + string;
return (padMask.substr(0, (padMask.length - string.length)) + string);
}
};
}();
// SessionManager Module
var SessionManager = function() {
// NOTE: I use @Session.Timeout here, which is Razor syntax, and I am pulling that value
// right from the ASP.NET MVC Session variable. Dangerous! Reckless! Awesome-sauce!
// You can just hard-code your timeout here if you feel like it. But I might cry.
var sessionTimeoutSeconds = HtmlHelpers.getQueryStringValue('smt') || (@Session.Timeout * 60),
countdownSeconds = HtmlHelpers.getQueryStringValue('smc') || 300,
secondsBeforePrompt = sessionTimeoutSeconds - countdownSeconds,
$dlg,
displayCountdownIntervalId,
promptToExtendSessionTimeoutId,
originalTitle = document.title,
count = countdownSeconds,
extendSessionUrl = '/Session/Extend',
expireSessionUrl = '/Session/Expire?returnUrl=' + location.pathname;
var endSession = function() {
$dlg.dialog('close');
location.href = expireSessionUrl;
};
var displayCountdown = function() {
var countdown = function() {
var cd = new Date(count * 1000),
minutes = cd.getUTCMinutes(),
seconds = cd.getUTCSeconds(),
minutesDisplay = minutes === 1 ? '1 minute ' : minutes === 0 ? '' : minutes + ' minutes ',
secondsDisplay = seconds === 1 ? '1 second' : seconds + ' seconds',
cdDisplay = minutesDisplay + secondsDisplay;
document.title = 'Expire in ' +
StringHelpers.padLeft(minutes, '00') + ':' +
StringHelpers.padLeft(seconds, '00');
$('#sm-countdown').html(cdDisplay);
if (count === 0) {
document.title = 'Session Expired';
endSession();
}
count--;
};
countdown();
displayCountdownIntervalId = window.setInterval(countdown, 1000);
};
var promptToExtendSession = function() {
$dlg = $('#sm-countdown-dialog')
.dialog({
title: 'Session Timeout Warning',
height: 205,
width: 250,
bgiframe: true,
modal: true,
buttons: {
'Continue': function() {
$(this).dialog('close');
refreshSession();
document.title = originalTitle;
},
'Log Out': function() {
endSession(false);
}
}
});
count = countdownSeconds;
displayCountdown();
};
var refreshSession = function() {
window.clearInterval(displayCountdownIntervalId);
var img = new Image(1, 1);
img.src = extendSessionUrl;
window.clearTimeout(promptToExtendSessionTimeoutId);
startSessionManager();
};
var startSessionManager = function() {
promptToExtendSessionTimeoutId =
window.setTimeout(promptToExtendSession, secondsBeforePrompt * 1000);
};
// Public Functions
return {
start: function() {
startSessionManager();
},
extend: function() {
refreshSession();
}
};
}();
SessionManager.start();
// Whenever an input changes, extend the session,
// since we know the user is interacting with the site.
$(':input').change(function() {
SessionManager.extend();
});
});
</script>
Honey Badger
But Noah, your variable names are comically long and descriptive!
You’re right. I love that. But it may make you angrier than a honey badger, so please allow me to talk you down.
Because all of my verbose variable names are truly private inside the module, your favorite minifier/compressor can safely mangle those variable names to a single letter, reducing client load by eleventy billion percent. Huzzah for the shopkeep!
My Back End Sings
Fantastic. All this JavaScript, and nothing on the server side to make it work. Let’s fix that.
We are using ASP.NET MVC 3 on my current project, so here’s how I made my back end sing. First, we have our two urls from the JavaScript:
var extendSessionUrl = '/Session/Extend',
expireSessionUrl = '/Session/Expire';
So we’ll add an ASP.NET MVC 3 SessionController with Extend and Expire actions, using the SeeSharp language all the kids are talking about:
using System.Web.Mvc;
namespace Fairway.Web.Controllers
{
public partial class SessionController : BaseController
{
private readonly IFormsAuthenticationService _formsAuthenticationService;
public SessionController(IFormsAuthenticationService formsAuthenticationService)
{
_formsAuthenticationService = formsAuthenticationService;
}
// This is used from JavaScript to re-establish the user's session
[Authorize]
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")] // Never Cache
public virtual ActionResult Extend()
{
// Re-establish the session timeout
Session.Timeout = 20;
return new EmptyResult();
}
[Authorize]
public virtual ActionResult Expire(string returnUrl)
{
_formsAuthenticationService.SignOut();
// Redirect to the role-specified "session expired" view
// This needs to be a separate Action because we need to issue a separate
// request once the session has been abandoned in order to have the correct
// context (that the user is logged out).
return RedirectToAction(MVC.Session.SessionExpired(returnUrl));
}
public virtual ActionResult SessionExpired(string returnUrl)
{
ViewData["ReturnUrl"] = returnUrl;
return View(MVC.Account.Views.SessionExpired);
}
}
}
Now we can respond to those calls from JavaScript! The key points here are:
- The Extend() method cannot be cached, or your session may not refresh correctly
- The Expire() method signs the user out, but has to redirect to a new action, which will know that the user is logged out
Nerd-Glue
We’ve got some JavaScript. We’ve got some HTML. We’ve got some back end. How do you stick it all together in a useful way? With nerd-glue, naturally!
- Add the HTML and JavaScript from “The Final Countdown” (above) to the bottom of every page in your site where you want the session timeout logic
- Of course, you should probably put it in an include, or a partial view, and then drop it in your site layout, master page, or equivalent
- You should probably also wrap the JavaScript and HTML code in logic that ensures that the request is coming from an authenticated user with an active session
- Add the server-side code to handle the “extend” and “expire” urls
- I guess that’s it
Imperial
I’ve told you a few things about my approach for gracefully handling session timeout. Here are some things I haven’t told you:
- It’s not a jQuery plugin, but could be (shameless plug for FlexBox plugin!)
- My favorite color is mercury
- The code could use more cleanup (but most code could)
- Hess Brewing makes a mean Rye Imperial Stout
Enjoy!
6 Comments
Leave a comment
Recent Posts
- iOS Unit Testing With OCMock
- Why Stakeholders Need To Be Involved In Scrum
- NuGet Config File Transformation Causes Duplicate Entries On Update
- Load Testing with Locust on Windows
- Writing A Custom LINQ Provider With Re-linq
- AutoMapper Profile Organization
- Rails 3.2: A Nested-Form Demo Part 4: Switch to Targeting Computer!
- SharpRepository: Configuration
- Rails 3.2: A Nested-Form Demo, Part 3: We’re Starting Our Attack Run!
- Rails 3.2: A Nested-Form Demo, Part 2: Accelerate to Attack Speed!
- Rails 3.2: A Nested-Form Demo, Part 1: All Wings Report In!
- iOS Behind the Curve
- Distributed Transaction Coordinators, Port 135, and Firewalls – Oh My!
- SharpRepository: Getting Started
- Find Performance Problems Using JMeter, MySQL and Xdebug/Webgrind
- Taming Hot Key Context Shifting When Running A Windows VM In Virtualbox On OSX
- Integrating Twitter’s Bootstrap Into Your Project
- Mobile payments, tags and more using NFC
- Stress Pig
- Dear Client Services, What Works?
- What Would Steve Do?
- Still Using Fiddler to Test & Debug Your REST Services?
- Write-through and Generational Caching Make a Great Team
- Thinking Recursively
- Development Incentives, What’s the Payoff?
- How do you like them Apples?
- “Optional” Software Development Practices Series — Code Review
- Adding Images to Select Lists in MVC3
- “Optional” Software Development Practices Series
- You Get What You Pay For…
- Outsourcing Safety Tips
- Facebook IPO
- The Ballad of Tim Toady
- The Little Schemer
- Newsflash: Mom leaves tech job at 5p.m.
- Flashback!
- I <negative_emotion> Windows 8!
- Prefix vs. Postfix Increment and Decrement Operators in C++
- Corporate videos: viral boon or epic fail?
- Recruitin’ Time!
- Reference vs. pointer parameters in C++
- The IE8 "hover" Bug: The Most Awesome IE Bug Ever?
- When is perfect perfect enough?
- SOPA/PIPA: Anti-Censorship Protest or Techies Revenge?
- A Decade of Fairway
- Handling Session Timeout Gracefully
- Generating Software Diagrams
- The Audacity of Nope
- The Origins of Culture
- Scrum Overview in Prezi – not another boring slideshow
- Numbers don’t lie: LinkedIn Statistics
- What is your favorite software development tool?
- Best Practices for Selecting Onshore, Nearshore or Offshore Information Technology Outsourcing (ITO) Providers
- Sign of the Times
- Advantages and Risks of Offshoring, Nearshoring or Onshoring
- Does Outsourcing Mean Offshoring?
- Too little, too late?
- New Favorite Lunch Spot
- Why should I care about functions as first-class citizens?
- PHP Remote Debugging with XDebug and NetBeans
- Installing SubText with Web PI
- ROI Primer
- Learn Domain-Driven Design
- Learn Behavior-Driven Development
- Mario Kart Tournament
- F# in 90 Seconds
- Website Vulnerabilities
- Scrum Overview
- Language Club
- Top 12 Favorite Podcasts Ever…
- Fairway Dart Tournament
- Learn Lean Software Development and Kanban Systems
- Android – Eclipse Quick Start
- Learn Functional Programming
- Backup & Restore Strategy
- Smartphone Screens – Another Wireless Variable
- Wireless Application Market
- Head First AOP






1st things 1st Noah… I enjoyed reading your tutorial. You are most entertaining.
I am guessing I am the 1st to comment and ill be sure to share this link around.
Question: I followed your tut carefully and when I stuck the SessionController : BaseController class in it is expecting a using reference or another class or something. I am hoping and guessing that you left some code out or perhaps I am just too stupid..lol.
What does that base controller depend on?
Thanks in advance Michael
Michael,
Thanks for your comment!
The IFormsAuthenticationService in the SessionController constructor is being injected by StructureMap, and that code is not represented in my example. If you just want to get it to run, you can remove the constructor, and instantiate FormsAuthenticationService() inside the Expire() method.
Noah
Thank you buddy..you saved my day. Excellent post
Great article, but do you mind posting the solution within zip file?
I tried pulling it all together, but I must be missing some pieces to get it to work.
Thanks
Hi ,
I am able to implement the functionality by going through the code. I have made one change in the code as mentioned below:
location.href = expireSessionUrl; in the endSession method to the following code:
window.location.replace(expireSessionUrl);
and once user clicks on the Log Out button present in the Session warning message dialog box he is navigated to the LogOut view. But here if he clicks the browser back button he is navigated to the previous page.
I have the following modified Expire method as mentioned bleow:
[Authorize]
public virtual ActionResult Expire()
{
Session.Clear();
FormsService.SignOut();
HttpContext.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
HttpContext.Response.Cache.SetValidUntilExpires(false);
HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
HttpContext.Response.Cache.SetNoStore();
return Redirect(“/”);
}
Can you please guide me in resolving the issue.
Thanks In Advance
Santosh Kumar Patro
Grt article. Can you please provide us with downloadable code?
Thanks