views:

875

answers:

4

What is the best way to show a message to user after session is expired? By then, the user should be logged out and redirected to start page. I can use javascript to redirect user to start page. I just want to show a message on start page.

+1  A: 

Usually a page will use JavaScript to keep a timer and display a pop-up or dhtml warning a minute or two before the session expires. My bank does this.

It's very annoying though, 'cause if the user has multiple tabs open to the same site, one tab will know the session is about to expire but another tab won't, so you can get the warning even when it's not true. If you try to use an AJAX call to ask the server for a time till the session expires, you've just extended the session timeout and effectively made the session never expire as long as the browser window is open (which may not be a bad thing depending on your specific situation).

Sam
Hey sam.. the spike i knocked out last night solves all of those problems and provides a reference implementation for both session state and forms auth. maybe some bank should hire me... ;-)
Sky Sanders
@Sky, I'm sure you're overqualified for my bank. They apparently only hire idiots.
Sam
+2  A: 

Previous version of this answer contains some simple source for a solution to an question that no longer exists.

The scenario was that he wanted to be notified before a session timed out so that an action could be taken.

It was not clear whether it was an asp.net session or a forms authentication ticket but really is the same issue.

Basically amounts to async session state management without triggering session/ticket renewals. A common wish for ajax apps.

The solution is pretty simple but not exactly straight forward. There are quite a few issues to deal with but I spiked out a solution that works reliable for both simple session state as well as forms authentication.

It is composed of a very light httpmodule and accompanying client script library, neither is more that 50 or 60 effective lines of code.

The strategy is to provide an http endpoint that can query the expiration state of a session or forms ticket without renewing them. The module simply sits on the front of the pipeline and services a couple 'virtual' endpoints with javascript dates and a very simple js lib to make the calls.

The demo and source are here: AsynchronousSessionAuditor

Some of you are wondering why I would be concerned with this, but probably more of you are saying hell yeah, what a pain in the patoot. I know I have come up with brittle smelly workarounds in the past and am pretty happy with this one.

It can serve in a standard postback model app or more likely an ajax application. Also... You can learn more about the Http request lifecycle here and here. If interested, learn more about using raw xmlhttp against asp.net endpoints here

Cutting previous spike code out. check the revision history if you are interested.

Here are the relevant source files. A full reference implementation is available from the link provided above....

HttpModule

// <copyright project="AsynchronousSessionAuditor" file="AsynchronousSessionAuditorModule.cs" company="Sky Sanders">
// This source is a Public Domain Dedication. 
// http://spikes.codeplex.com
// Attribution is appreciated.
// </copyright> 

using System;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;

namespace Salient.Web.Security
{
    /// <summary>
    /// AsynchronousSessionAuditorModule provides a mechanism for auditing the lifecycle
    /// of an ASP.Net session and/or an ASP.Net FormsAuthentication cookie in the interest
    /// of providing better monitoring and control of a user session. This is becoming more
    /// important as more and more web apps are single page apps that may not cycle a page for
    /// many minutes or hours.
    /// 
    /// Storing a token in the Asp.net Session was for many years the default authentication strategy.
    /// The are still valid applications for this techique although FormsAuthentication has far
    /// surpassed in security and functionality.
    /// 
    /// What I am providing is a manner in which to transparently monitor the expiration status of each
    /// by implementing a module that recognizes two virtual endpoints:
    /// 
    ///   http://mysite/.aspnetsession
    ///   http://mysite/.formsauthticket
    /// 
    /// By making a request to these urls you will be delivered a javascript date in numeric form that
    /// represents the expiration dateTime of either the current ASP.Net session, if any, or the current
    /// FormsAuthentication ticket expiration, if any.
    /// 
    /// If the requested item does not exists, zero is returned. Any value served by this module should
    /// be cast to a date and compared with Now. If less than you should take action. You should have
    /// taken action on the client before the session timed out, aided by the output of this module, but
    /// hey, nobody is perfect.
    /// </summary>
    public class AsynchronousSessionAuditorModule : IHttpModule
    {
        // note: these must remain in sync with the string keys in the javascript
        private const string AspSessionAuditKey = ".aspnetsession";

        private const string FormsAuthAuditKey = ".formsauthticket";

        #region IHttpModule Members

        public void Init(HttpApplication context)
        {
            // this is our audit hook. get the request before anyone else does
            // and if it is for us handle it and end. no one is the wiser.
            // otherwise just let it pass...
            context.BeginRequest += HandleAuditRequest;

            // this is as early as we can access session. 
            // it is also the latest we can get in, as the script handler is coming
            // right after and we want to beat the script handler to the request
            // will have to set a cookie for the next audit request to read in Begin request.
            // the cookie is used nowhere else.
            context.PostAcquireRequestState += SetAuditBugs;
        }

        public void Dispose()
        {
        }

        #endregion

        private static void SetAuditBugs(object sender, EventArgs e)
        {
            HttpApplication app = (HttpApplication) sender;

            if ((app.Context.Handler is IRequiresSessionState || app.Context.Handler is IReadOnlySessionState))
            {
                HttpCookie sessionTimeoutCookie = new HttpCookie(AspSessionAuditKey);

                // check to see if there is a session cookie
                string cookieHeader = app.Context.Request.Headers["Cookie"];
                if ((null != cookieHeader) && (cookieHeader.IndexOf("ASP.NET_SessionId") >= 0) &&
                    !app.Context.Session.IsNewSession)
                {
                    // session is live and this is a request so lets ensure the life span
                    app.Context.Session["__________SessionKicker"] = DateTime.Now;
                    sessionTimeoutCookie.Expires = DateTime.Now.AddMinutes(app.Session.Timeout).AddSeconds(2);
                    sessionTimeoutCookie.Value = MilliTimeStamp(sessionTimeoutCookie.Expires).ToString();
                }
                else
                {
                    // session has timed out; don't fiddle with it
                    sessionTimeoutCookie.Expires = DateTime.Now.AddDays(-30);
                    sessionTimeoutCookie.Value = 0.ToString();
                }
                app.Response.Cookies.Add(sessionTimeoutCookie);
            }
        }

        private static void HandleAuditRequest(object sender, EventArgs e)
        {
            HttpContext context = ((HttpApplication) sender).Context;
            bool formsAudit = context.Request.Url.PathAndQuery.ToLower().StartsWith("/" + FormsAuthAuditKey);
            bool aspSessionAudit = context.Request.Url.PathAndQuery.ToLower().StartsWith("/" + AspSessionAuditKey);

            if (!formsAudit && !aspSessionAudit)
            {
                // your are not the droids i am looking for, you may move along...
                return;
            }

            double timeout;
            // want to know forms auth status
            if (formsAudit)
            {
                HttpCookie formsAuthCookie = context.Request.Cookies[FormsAuthentication.FormsCookieName];
                if (formsAuthCookie != null)
                {
                    FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(formsAuthCookie.Value);
                    timeout = MilliTimeStamp(ticket.Expiration);
                }
                else
                {
                    timeout = 0;
                }
            }
                // want to know session status
            else
            {
                // no session here, just take the word of SetAuditBugs
                HttpCookie sessionTimeoutCookie = context.Request.Cookies[AspSessionAuditKey];
                timeout = sessionTimeoutCookie == null ? 0 : Convert.ToDouble(sessionTimeoutCookie.Value);
            }

            // ensure that the response is not cached. That would defeat the whole purpose
            context.Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(-1));
            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            context.Response.Cache.SetNoStore();
            // the money shot. a javascript date.
            context.Response.Write(timeout.ToString());
            context.Response.Flush();
            context.Response.End();
        }

        /// <summary>
        /// Found Code: http://forums.asp.net/t/1044408.aspx
        /// </summary>
        /// <param name="TheDate"></param>
        /// <returns></returns>
        private static double MilliTimeStamp(DateTime TheDate)
        {
            DateTime d1 = new DateTime(1970, 1, 1);
            DateTime d2 = TheDate.ToUniversalTime();
            TimeSpan ts = new TimeSpan(d2.Ticks - d1.Ticks);

            return ts.TotalMilliseconds;
        }
    }
}

Client Library

// <copyright project="AsynchronousSessionAuditor" file="AsynchronousSessionAuditor.js" company="Sky Sanders">
// This source is a Public Domain Dedication. 
// http://spikes.codeplex.com
// Attribution is appreciated.
// </copyright> 


var AsynchronousSessionAuditor = {
    /// this script really should be served as a resource embedded in the assembly of the module
    /// especially to keep the keys syncronized

    pollingInterval: 60000, // 60 second polling. Not horrible, except for the server logs. ;)

    formsAuthAuditKey: ".formsauthticket", // convenience members
    aspSessionAuditKey: ".aspnetsession",

    errorCallback: function(key, xhr) {
        /// <summary>
        /// Default behavior is to redirect to Default and provide the xhr error status text
        /// in the loggedout query param.
        ///
        /// You may replace this default behaviour with your own handler. 
        /// e.g.  AsynchronousSessionAuditor.errorCallback = myMethod;
        /// </summary>
        /// <param name="key" type="String"></param>
        /// <param name="xhr" type="XMLHttpRequest"></param>
        window.location = "Default.aspx?loggedout=Error+" + xhr.statusText;
    },

    timeoutCallback: function(key, xhr) {
        /// <summary>
        /// Default behavior is to redirect to Default and provide the key value
        /// in the loggedout query param.
        ///
        /// You may replace this default behaviour with your own handler.
        /// e.g.  AsynchronousSessionAuditor.timeoutCallback= myMethod;
        /// </summary>
        /// <param name="key" type="String"></param>
        /// <param name="xhr" type="XMLHttpRequest"></param>    
        window.location = "Default.aspx?loggedout=" + key;
        // or just refresh. you will be sent to login.aspx
    },

    statusCallback: function(value) {
        /// <summary>
        /// Default behavior is to do nothing, which is not very interesting.
        /// This value is set when AsynchronousSessionAuditor.init is called
        /// </summary>
        /// <param name="value" type="String">
        /// The responseText of the audit request. Most certainly is a JavaScript Date
        /// as a number. Just cast to date to get the requested expiration dateTime.
        /// e.g. var exp = new Date(parseFloat(value)); if (isNaN(exp)){this should never happen}
        /// </param>

        window.location = "Default.aspx?loggedout=" + key;
        // or just refresh. you will be sent to login.aspx
    },

    createXHR: function() {
        /// <summary>
        /// This xhr factory is not the best I have see.
        /// You may wish to replace it with another or
        /// use your favorite ajax library to make the
        /// call.
        /// </summary>
        var xhr;

        if (window.XMLHttpRequest) {
            xhr = new XMLHttpRequest();
        }
        else if (window.ActiveXObject) {
            xhr = new ActiveXObject('Microsoft.XMLHTTP');
        }
        else {
            throw new Error("Could not create XMLHttpRequest object.");
        }
        return xhr;
    },


    auditSession: function(key) {
        /// <summary>
        /// Make a request that will be serviced by the audit module to determine the 
        /// state of the current FormsAuthentication ticket or Asp.Net session 
        ///
        /// The return value is a JavaScript date, in numeric form, that represents the
        /// expiration of the item specified by key.
        /// Just cast it to date, i.e. new Date(parseFloat(xhr.resposeText))
        /// </summary>
        /// <param name="key" type="String">
        /// the server key for the item to audit.
        ///
        /// use ".formsauthticket" to get the expiration dateTime for the forms authentication
        /// ticket, if any.
        ///
        /// use ".aspnetsession" to get the expiration of the current ASP.Net session.
        ///
        /// Both have convenience members on this object.
        /// </param>

        var xhr = AsynchronousSessionAuditor.createXHR();

        xhr.open("GET", key, true);

        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status != 200) {
                    AsynchronousSessionAuditor.errorCallback(key, xhr);
                }
                else {

                    var timeout = parseFloat(xhr.responseText)
                    if (isNaN(timeout) || (new Date(timeout) < new Date())) {
                        AsynchronousSessionAuditor.timeoutCallback(key, xhr);
                    }
                    else {
                        AsynchronousSessionAuditor.statusCallback(xhr.responseText);
                    }
                }

            }
        };
        xhr.send(null);
    },

    init: function(key, statusCallback) {
        // set the statusCallback member for reference.
        AsynchronousSessionAuditor.statusCallback = statusCallback;
        // check right now
        AsynchronousSessionAuditor.auditSession(key);
        // and recurring
        window.setInterval((function() { AsynchronousSessionAuditor.auditSession(key) }), AsynchronousSessionAuditor.pollingInterval);
    }
};



function callScriptMethod(url) {
    /// <summary>
    /// 
    /// Simply makes a bogus ScriptService call to a void PageMethod name DoSomething simulating
    /// an async (Ajax) call.
    /// This resets the session cookie in the same way a postback or refresh would.
    ///
    /// The same would apply to a ScriptService enabled XML Webservice call.
    /// </summary>

    var xhr = AsynchronousSessionAuditor.createXHR();
    xhr.open("POST", url, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status != 200) {
                alert("script method call failed:" + xhr.statusText);
            }
        }
    };
    xhr.setRequestHeader("content-type", "application/json");
    var postData = null;
    xhr.send(postData);
}

Usage

<script src="AsynchronousSessionAuditor.js" type="text/javascript"></script>

<script type="text/javascript">

    function reportStatus(value) {
        /// <summary>
        /// In a typical session/ticket lifetime you might display a warning at T-5 minutes that the session 
        /// is expiring and require an action.
        /// </summary>

        document.getElementById("sessionTimeout").innerHTML = "Session expires  in " + parseInt((new Date(parseFloat(value)) - new Date()) / 1000) + " seconds.";
    }

    function init() {

        // default is 60 seconds. Our session is only 1 minute so lets get crazy and poll every second
        AsynchronousSessionAuditor.pollingInterval = 1000;
        AsynchronousSessionAuditor.init(AsynchronousSessionAuditor.aspSessionAuditKey, reportStatus);
    }

</script>
Sky Sanders
OH EM GEE I've been looking for something like this for days. Gonna try it out tomorrow!!! :)
TheGeekYouNeed
@Cen, cool. be sure to let me know how it works out. I am getting ready to dig it up and use it myself in a few days.
Sky Sanders
+1  A: 

Other answers are suggesting showing the notification on the page that times out.. but I think you are asking how to show a message on the page the user is redirected to.

One possibility would be to just pass a URL request parameter to your start page only on session timeout. Your script or asp.net code could then decide to display the message when the parameter is present.

markt
the question was edited mark. you are right, that is exactly what it looks like. now. lol.
Sky Sanders
+1  A: 

In your JavaScript redirect add a querystring parameter to your redirect URL.

<script>
...
// If session is about to timeout...
location.href = '/home.aspx?action=expired';
...
</script>

Then in your start page, you can have code to check the querystring and display a message if that's the case.

<%
'home.aspx
If Request.QueryString("action") = "expired" Then 
  Response.Write("<p>You have been logged out due to inactivity.</p>")
End If
%>
Shawn Steward
Response.Write! Aaaaaaaahh!!! :)
markt
kinda smelly, Shawn. ;-p The mechanism is artificial and somehow must be enforced at every endpoint. And.. how are you to know 'If session is about to timeout...' on the client? [insert many answers here].. Ok, but what if the user accesses another page of the site in another tab or the page makes an Ajax request bumping the session? Not bagging, just sayin....
Sky Sanders
Oh I hear you... But at one point he said in a comment (that seems to be gone now) that he already had the Session timeout working and just wanted to display a message on the start page he's redirecting to. So I assumed he had a location.href set somewhere and was just showing him how to modify that portion to display a message on the start page.
Shawn Steward
For clarification, I was just responding to his request of: "I just want to show a message on start page."
Shawn Steward
yup, seems he gutted the question. I spiked out a pretty damn fine async session module last night to meet the requirements. That are now gone...Not as obvious as it sounds but I have run into and dealt with scenarios like this before. Check it out in my answer if you are interested...
Sky Sanders
To Shawn Steward: yes, you are right. Sorry for the confusion.To Sky: Thanks for sharing your idea, I will look at it soon.
crocpulsar