Adding session timeout to ASP.NET MVC 5

As part of security improvements, we needed to add a session time out in one of our apps.

One of the challenges though is that users could be in the middle of submitting their work, go past the timer, then submit, and because their session has expired, they loose their work. Here is how we gracefully worked within the constraints but still enforced a secure system.

Overview

  • We need a way to get the end time for the session. Setting up a filter config will allow us to tap into anytime when a user completes an action which will allow us to reset the timeout countdown.
  • We need to poll the server and get that countdown time. When it reaches zero, we show a modal letting the user know their session has timed out and that they can resume their session by clicking the login link, completing the login. They can then resume their work.

The backend

Here is how we tapped into the request to set the session expiry.

namespace IdentitySample
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new DebugActionFilter());
        }
    }

    public class DebugActionFilter : System.Web.Mvc.ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext actionContext)
        {
            if (actionContext.HttpContext.Request.Headers["Polling"] == "true")
            {
                //dont reset the timer
            }
            else
            {
                //for all other requests, reset the timer
                if (actionContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
                {

                    //If the user is authenticated, we can reset these times.
                    actionContext.RequestContext.HttpContext.Session["AuthActiveDateTime"] = DateTime.Now;
                    actionContext.RequestContext.HttpContext.Session["AuthExpiryTime"] = DateTime.Now + TimeSpan.FromMinutes(actionContext.RequestContext.HttpContext.Session.Timeout);
                }
                if (!actionContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
                {
                    //If the user is not authenticated, we set these times to null.
                    actionContext.RequestContext.HttpContext.Session["AuthActiveDateTime"] = null;
                    actionContext.RequestContext.HttpContext.Session["AuthExpiryTime"] = null;
                }
            }



        }
    }
}

And here is our endpoint which looks at the session expiry time and returns that time. If the session expiry is null, it just returns the current time.

[AllowAnonymous]
        public JsonResult SessionTimeEndpoint()
        {
            if (Session["AuthExpiryTime"] != null)
            {
                return new JsonResult()
                {
                    Data = new
                    {
                        AuthExpiryTime = DateTime.Parse(Session["AuthExpiryTime"]?.ToString()).ToString("s")
                    },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };

            }
            else
            {
                return new JsonResult()
                {
                    Data = new
                    {
                        AuthExpiryTime = DateTime.Now.ToString("s")
                    },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
            }

        }

The front end

We need a way to poll the back end to check that countdown. Remember it could change from having another window open, so we can’t just set and then do something when that timer ends. It could change at any time.

var interval;
var loginModalShowing;


function GetSessionExpiry() {
    console.log("starting polling")
    $.ajax({
        beforeSend: function (jqXHR, settings) {
            jqXHR.setRequestHeader('Polling', "true");
        },
        dataType: "json",
        url: "/Account/SessionTimeEndpoint",
        jsonpCallback: "callback",
        success: callback,
        error: failed
    });
}

function callback(result) {
    var start = moment();
    var end = moment(result.AuthExpiryTime);

    var duration = moment.duration(end.diff(start));

    console.log(duration.asSeconds())

    console.log(location.pathname)
    if (!location.pathname.includes("Login")) {
        if (location.pathname != "/") {
            //we are at the root, and the root shows the login form, so no need to redirect
            if (duration.asSeconds() < 0) {

                if (!loginModalShowing) {
                    console.log("Showing login modal")
                    ShowLoginModal()
                }
            }
            else {
                HideLoginModal()
            }
        }
    }


}

function failed(xhr, ajaxOptions, thrownError) {
    alert(xhr.status);
    alert(thrownError);
}

function startPolling() {
    interval = setInterval(GetSessionExpiry, 10000);
}

function ShowLoginModal(data) {    
    $('#LoginModal').modal('show')
    loginModalShowing = true;  
}

function HideLoginModal() {
    $('#LoginModal').modal('hide')
    loginModalShowing = false;

}
startPolling()

We need to include this script wherever we want it to run. In my case, I pulled it into the application through the head section, so it would run pretty much everywhere on my site.

And here is my modal. You need to include this wherever the script is running, so that it is available whenever modal(‘show’) is called.

<div class="modal fade" id="LoginModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="myModal-label">Session Expired</h4>
            </div>
            <div class="modal-body">
                <div id="LoginModalBody">
                    <p>
                        Your session has expired, please click login below 
                        to open a fresh login window and log in.
                        Once complete, you can return here and continue your work.
                    </p>


                </div>
            </div>
            <div class="modal-footer">
                <a role="button" href="/Account/Login" target="_blank" type="button" class="btn btn-primary">Login</a>

            </div>

        </div>
    </div>
</div>

Once you put all of this together, you should have something like this. If you want to test, you can set

if (duration.asSeconds() < 0)

to a number a bit larger. You can check your console to find what your session expiry is, in my case it was the default 20 minutes, so setting that value to 1195 will mean it only counts down about 5 seconds before showing the modal.

The neat thing about this approach is that it will work where external authentication is used. Because the login is handled in another window, it won’t affect the state of the currently opened windows. Meaning users won’t lose any work they have not submitted.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *