Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Notify user when session time out in asp.net

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.

like image 543
yang-qu Avatar asked Dec 02 '22 06:12

yang-qu


1 Answers

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>
like image 117
Sky Sanders Avatar answered Dec 03 '22 20:12

Sky Sanders