Click here to Skip to main content
15,883,830 members
Articles / Web Development / HTML

Simple, Cross-browser JavaScript Session and Forms Authentication Timeout Prevention / Handling

Rate me:
Please Sign up or sign in to vote.
4.47/5 (9 votes)
15 Dec 2009CPOL8 min read 52.8K   47   9
A client-side solution to warn users their session is about to timeout and refresh their authentication ticket, if necessary.

Introduction

This article creates a centralized solution that warns the user that their login session is about to timeout and gives them the option to "refresh" their forms authentication ticket without posting back the page they are on or navigating away from it. Because it is based in JavaScript and does not alter the Forms Authentication ticket directly, the solution is fast and secure and does not postback to the server when not necessary.

Background

I was looking around for an effective, simple, and cross-browser friendly method that would gracefully handle an ASP.NET 2.0 application's session timeout and the related Forms Authentication logout event. I realized the need to have one after an UpdatePanel I wrapped around a GridView (for the purposes of making paging and sorting appear "smoother" to the user) failed very un-gracefully when a partial page post-back was sent to the server after the session had timed-out. When this happens, the user gets an ugly, jargon-heavy pop-up message from the UpdatePanel which doesn't explain what happened or tell them what to do next (unless they understand AJAX!).

I decided I could either code all my UpdatePanels to handle the session timing out, or I could create a centralized solution that elegantly handles all the session timeouts on every Forms Authentication secured page. To do that, I needed a solution that did three things:

  1. After x number of minutes of inactivity, warn the user that their session is about to expire, and log them out before the session actually times-out,
  2. Give the user a button to click, which renews their session ticket without posting back the page or navigating to another page, and
  3. Worked mostly client-side to prevent unnecessary postbacks to the server.

I found several solutions online, but none that met all my needs, so I created my own using some of the best aspects of other solutions, along with my own tweaks to make them more useful. Of course, my solution cannot compromise the security of the Forms Authentication service or the authentication ticket, etc., so I avoided that possibility by staying out of the actual authentication ticket/cookie, and only calling FormsAuthentication.SignOut() to log out a user when/if their session expires. The next best solution I found relied on AJAX to constantly refresh the session, but that method creates unnecessary callbacks to the server, which I did not want in my application.

Using the Code

First, the JavaScript. Place this script tag in the Head of your Master page, or in the head of the base-class from which your secured pages are derived. The tag can also be built with a StringBuilder from the code-behind and added to the pages dynamically with a ScriptManager and its script registering methods; just be sure to add it to the Head. If you're not using a single Master page or page base-class to derive all your Forms Authenticated ASPX pages from, I highly recommend it. It's just too amazingly handy to have all your pages' content and layout based on one centralized page that cascades changes to all its derived pages. If you don't use a Master page, place this script tag in the Head of every one of your Forms Authentication secured ASPX pages:

JavaScript
<script type="text/javascript" language="javascript">
// set your timeout period in minutes, minus 1;
// ie: 9 minutes + 1 minute countdown = 10 minute timeout
var timeoutPeriod = 9;
// declare global variables to track the timeout
// and interval timers;
// this allows Javascript to cancel them as necessary
var intervalCountdown;
var timeoutWarning;

function showWarning(){
  // show the warning divs
  document.getElementById('divTimeOut').style.display = 'block';
  intervalCountdown = setInterval(countDown,1000);
  // start the countdown
}
function countDown(){
  var divCountDown = document.getElementById('divCountDown');
  var intCount = parseInt(divCountDown.innerHTML) -1;
  // get remaining countdown time, minus 1 second

  if (intCount <= 10)
    {divCountDown.style.color = '#ff0000';}
     // show red text when only 10 seconds remain
  if (intCount >= 0)
    {divCountDown.innerHTML = intCount.toString();}
    // update the countdown div
  else // logout user after 10 minutes
    {__doPostBack('ctl00$lnkTimeOut','');}
    // if your Master page is named something other than
    // "ctl00," change that here; you can also
    // call the "click" event of the button instead
}
function refreshPage(){
clearInterval(intervalCountdown); // stop countdown
clearTimeout(timeoutWarning);
// reset timeout warning to original 9 minutes
timeoutWarning = setTimeout(showWarning,timeoutPeriod * 60000);
var divCountDown = document.getElementById('divCountDown');
divCountDown.style.color = '#000000';
divCountDown.innerHTML = '60'; // reset countdown to 60
// create random number between 0 and 1000
var decRound = Math.round(Math.random()*1000);
var imgRefresh = new Image(1,1);
// change '../keepalive.aspx?id=' based on keepalive.aspx's location
// in relation to your secured pages, ie '/keepalive.aspx?id=' + 
//  decRound.toString(); for a keepalive.aspx page placed in the application root
imgRefresh.src = '../keepalive.aspx?id=' + decRound.toString();
document.getElementById('divTimeOut').style.display = 'none';
// hide the warning divs
}
</script>

The JavaScript functions do the following: showWarning() shows the warning div onscreen, and starts a 60-second "count down". countDown() fires every second, and reduces the count by 1. At zero, countDown() calls a postback on a "logout" LinkButton. refreshPage() turns off the countdown when the user clicks on the "Stay Logged In" button and requests the "keepalive.apsx" page from the server with a randomly generated querystring. (The above JavaScript can also be placed in a .js file you have linked to your Master page's Head.)

Next, set a timeout in JavaScript with the body onload event of your Master page to fire the "showWarning" function after 9 minutes:

HTML
<body onload="timeoutWarning = setTimeout(showWarning, timeoutPeriod * 60000);">
<form id="form1" runat="server"><div>aspcontent ...blah blah blah etc...</div>
</form>
</body>

Without a Master page, place the above onload event code in the body tag of the page(s) you have secured with Forms Authentication, or add it dynamically with Page.RegisterStartupScript. After 9 minutes of inactivity (no full postbacks or page redirections), the timeout warning div appears, and starts a 60 second countdown after which the user is logged-out. This essentially creates a session timeout after 10 client-side minutes (change the value of the timeoutPeriod global variable to suit your needs, just be sure to adjust your web.config accordingly, as explained below). If the logout warning timer gets to 0, JavaScript calls __doPostBack('ctl00$lnkTimeOut','');, which fires the code-behind associated with the asp:LinkButton, "lnkTimeOut". This button can either be your regular logout button, or a "hidden" one with "display:none;" as a CSS style setting. More on that later...

Now, the ASPX page that "refreshes" the session and the Forms Authentication ticket cookie... "keepalive.aspx":

HTML
<%@ Page Language="vb" AutoEventWireup="false" %>
<%@ OutputCache Location="None" VaryByParam="None" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
</body>
</html>

That's it for "keepalive.aspx". No code behind. No need for anything else. The options OutputCache Location="None" and VaryByParam="None" have been added to the page to attempt to keep browsers from caching the page client-side, which would defeat the page's job of refreshing the session each and every time it's requested. I found this solution to be a bit sketchy depending on the browser in question, so the JavaScript refreshPage() function uses the Math.random() number function to create a unique querystring to append to the URL each time a request is made to keepalive.aspx. This prevents the browser from assuming that we're calling the same keepalive.aspx page again and then "helping" us by getting it from the cache instead of directly from the server. The JavaScript request to keepalive.aspx will actually look like keepalive.aspx?id=79 or keepalive.aspx?id=420 etc., forcing the browser to send the request all the way to the server. I suppose after ~1000 requests to keepalive.aspx from the same page, this method will fail, but who would want to refresh their "session" 1000 times, waiting 9 or 10 minutes between each refresh???

For the actual pop-up style div that warns the user, place this in your Master page's body:

HTML
<div id="divTimeOut" class="divTOwarning">
   <div id="divTimeOutI" class="divTOwarningI">
   Your login session is about to expire... Click below if you want to stay logged in.
<input type="button" id="btnRefresh" onclick="refreshPage();" 
  value="Stay Logged In" style="margin:4px 0 4px 0;" />
<div style="height:18px;">Logging out in:<div 
  id="divCountDown" style="font-size:20px;">60</div></div>
</div></div>
<asp:LinkButton ID="lnkTimeOut" runat="server" 
  style="display:none;"></asp:LinkButton>

This code snippet also shows the hidden log-out LinkButton, "lnkTimeOut". I used a "hidden" logout button (different from my regular logout button) so that when a user is logged-out because of a session timeout, the "login.aspx" page will display a div that says, "You were logged out after 10 minutes of inactivity. Please re-login to continue." This simple message was sorely missing from ASP.NET's implementation of the Forms Authentication timeout function. Personally, I know when I get logged out of a website because of inactivity, I want to know what happened instead of just being presented with a blank login page! The code behind for lnkTimeOut:

VB
Private Sub lnkTimeOut_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles lnkTimeOut.Click
    Session.Clear()
    Session.Abandon()
    FormsAuthentication.SignOut()
    Dim strUrl As String = Server.UrlEncode(Request.RawUrl)
    Response.Redirect("../../login.aspx?id=1&ReturnUrl=" & strUrl)
    'Change the location and name of your login page here
End Sub

The "id=1" querystring tells my login.aspx page to show a div that is normally hidden, letting the user know what happened. The second query string "ReturnUrl" tells the login.aspx page to redirect the user back to the page they were viewing when they were logged out, after they log back in.

Here's the related CSS:

CSS
.divTOwarning {width:100%;height:100%;z-index:140;
   background-image:url(../images/bkTimeout.png);display:none;
   background-repeat:repeat-x;background-position:left top;
   background-attachment:fixed;position:fixed;top:0;left:0;}

.divTOwarningI {width:200px;height:130px;position:fixed;top:40%;
   left:35%;border:solid 1px #000000;
   z-index:150;background-color:#9EFA82;font-size:14px;
   text-align:center;padding:3px 3px 3px 3px;}

This CSS creates a small "inner" div, with the Refresh button, warning message, and countdown centered inside a page-wide div that "grays out" the rest of the page. The image I used as a background, bkTimeout.png, is a 2 pixel wide x 800 pixel tall PNG image that is dark-gray and 50% translucent. This PNG fills the page (using repeat-x) and gives the user the impression that the rest of the page has been "locked" until they choose to "refresh" their login session by clicking the button. Setting the z-index assures that the warning divs appear on top of the other page content, and setting display:none; hides the divs until they are "shown" by showWarning().

***A note about using style="display:none;" vs visible="false"... setting visible="false" on an element in the ASPX page or the code-behind will tell ASP.NET to not render the element in the page, meaning it is not available for JavaScript to interact with. Setting display:none; in CSS will still render the object in question, but simply make it invisible to the user. That way, JavaScript can access the invisible element client-side and "show" it if necessary.***

Lastly, we do not want the standard ASP.NET Forms authentication timeout occurring (again, the built-in timeout event is simply not graceful), so we set the timeout interval in our web.config file to be one minute more than the actual 10-minute timeout we've decided on. This guarantees that the user will never be logged-out by the ASP.NET application without warning:

XML
<authentication mode="Forms">
    <forms name=".ASPXFORMSAUTH"
           loginUrl="login.aspx"
           protection="All"
           timeout="11"
           path="/"
           requireSSL="false"
           slidingExpiration="true"
           defaultUrl="default.aspx"
           cookieless="UseDeviceProfile"
           enableCrossAppRedirects="false"/>
</authentication>

Some coders have noted that setting slidingExpiration="true" in the web.config file means the authentication cookie is only refreshed if more than 50% of its expiration time has passed. I don't know if that's true or not, but it shouldn't affect this solution unless you set your timeout values extremely short, like 1 to 4 minutes.

That's all that is needed to keep your users from being ungracefully logged-out of your Forms authentication secured ASP.NET pages because of session timeouts. Note that if you use UpdatePanels to do partial page postbacks, you'll need to implement a PageRequestManager on those pages to reset the JavaScript timeout interval to nine minutes and "refresh" the authentication ticket with each asynchronous postback. That should look something like this tag, placed in the body of the page(s) containing an UpdatePanel:

JavaScript
<script type="text/javascript" language="javascript">
  var prm = Sys.WebForms.PageRequestManager.getInstance();
  prm.add_pageLoaded(refreshPage);
</script>

Also, if there are any buttons or links in a page which you need to refresh the timeout period with (if those controls don't do a full-postback or navigate to a new page), you can make those links or buttons reset the JavaScript "warning" timer, and refresh the authentication ticket by adding OnClientClick="refreshPage();" if the control is an ASP.NET control, or onclick="refreshPage();" for an element not processed by the server, like an anchor or input type="button" control.

This ASP.NET 2.0 code and the related JavaScript has been tested with IE 7, IE 8, Firefox 3.5, Opera 10, Safari 4 (for Windows), and Chrome 3.

Happy coding!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President http://www.CheckMyARMonline.com
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionASP.NET 4.0 and IIS 7.5 - redirect not working Pin
Member 806906330-Dec-11 10:04
Member 806906330-Dec-11 10:04 
GeneralWhat if you're already on the log in page? Pin
beep6-May-11 20:08
beep6-May-11 20:08 
GeneralRe: What if you're already on the log in page? Pin
big_novak7-May-11 8:07
big_novak7-May-11 8:07 
GeneralRe: What if you're already on the log in page? Pin
beep7-May-11 15:39
beep7-May-11 15:39 
GeneralSweet! Pin
beep6-May-11 11:26
beep6-May-11 11:26 
Generallogin.aspx Pin
Ajay Kale New27-Sep-10 0:14
Ajay Kale New27-Sep-10 0:14 
GeneralA few things about javascript Pin
Helbrax15-Dec-09 4:02
Helbrax15-Dec-09 4:02 
GeneralRe: A few things about javascript Pin
big_novak15-Dec-09 8:09
big_novak15-Dec-09 8:09 
Thanks for the info. I changed the setTimeout and setInterval functions accordingly. I was not aware that passing strings to those functions caused eval() to fire, which is not needed here and kinda costly.

I'm leaving in language="javascript" because even though it's completely depreciated, it doesn't really hurt to have it there. I'm not worried about changing the parseInt function to include a radix parameter. parseInt()'s radix parameter is optional; parseInt() defaults to a base-10 number when radix is left blank and the number string submitted to it leads-off with a non-zero digit, which will always be the case in this code.
GeneralRe: A few things about javascript Pin
Helbrax15-Dec-09 9:10
Helbrax15-Dec-09 9:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.