14

We have an internal ASP.NET MVC application that requires a logon. Log on works great and does what's expected. We have a session expiration of 15 minutes. After sitting on a single page for that period of time, the user has lost the session. If they attempt to refresh the current page or browse to another, they will get a log on page. We keep their request stored so once they've logged in they can continue on to the page that they've requested. This works great.

However, my issue is that on some pages there are AJAX calls. For example, they may fill out part of a form, wander off and let their session expire. When they come back, the screen is still displayed. If they simply fill in a box (which will make an AJAX call) the AJAX call will return the Logon page (inside of whatever div the AJAX should have simply returned the actual results). This looks horrible.

I think that the solution is to make the page itself expire (so that when a session is terminated, they automatically are returned to the logon screen without any action by them). However, I'm wondering if there are opinions/ideas on how best to implement this specifically in regards to best practices in ASP.NET MVC.

Update:

So I went ahead and implemented this in my OnActionExecuting (per Keltex's suggestion)

  if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
  {
    if (filterContext.HttpContext.Request.IsAjaxRequest())
    {
      filterContext.HttpContext.Response.Write("Invalid session -- please login!");
      filterContext.HttpContext.Response.End();
    }
    else
    {
      ...
    }
  }

This definitely makes things better -- now even if they have two tabs (one with some AJAX calls that they can trigger) and they log out explicitly in the second tab, they will immediately get something that makes more sense rather than a bunch of screwed up AJAX data.

I still think I will implement the Javascript countdown as well that womp suggested.

2
  • @Andrew - That's an elegant solution. Alernatively, would filterContext.HttpContext.Response.Redirect("/error/xxx"); (or something ) work? Commented Apr 7, 2010 at 22:05
  • @Keltex: I may move it to a view as you suggest -- but in many of my AJAX calls, they're returning raw data (like list of values) with no HTML whereas in others they're returning perhaps an entire table of nicely formatted data. So a "lowest common denominator" of just raw data may work best. I'll play with it. Commented Apr 7, 2010 at 23:00

6 Answers 6

16

Specifically, I don't know that there are any best practices regarding it, but I'm doing this right now for our app. We've opted for a client-side solution where we output the Session timeout value into some javascript in the master page, and calculate when the session will expire.

5 minutes before-hand, we pop up a modal dialog box saying "Are you still there?" with a countdown timer. Once the timer hits 0:00, we redirect the browser to the login page.

It's implemented with a minimal amount of javascript to do the time and timer calculations, and a simple .ashx handler that will refresh the session if the user clicks "I'm back!" on the dialog box before the session expires. That way if they return in time, they can refresh the session without any navigation.

Sign up to request clarification or add additional context in comments.

3 Comments

+1: This would be a lower-impact, less-code solution than checking the session with every request.
That's very nice. I'm totally going to gank this.
Had to look up what "gank" meant... :-) I think I'm going to implement this as well as providing some AJAX safeguards per Keltex's suggestion. Thank you!
7

I asked similar question yesterday. Here is my solution:

Modified Authorize attribute:

public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
    private class Http403Result : ActionResult
    {
        public override void ExecuteResult(ControllerContext context)
        {
            // Set the response code to 403.
            context.HttpContext.Response.StatusCode = 403;
            context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
        }
    }

    private readonly bool _authorize;

    public OptionalAuthorizeAttribute()
    {
        _authorize = true;
    }

    //OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller. 
    //That is why parameter is introduced.
    public OptionalAuthorizeAttribute(bool authorize)
    {
        _authorize = authorize;
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        //When authorize parameter is set to false, not authorization should be performed.
        if (!_authorize)
            return true;

        var result = base.AuthorizeCore(httpContext);

        return result;
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            //Ajax request doesn't return to login page, it just returns 403 error.
            filterContext.Result = new Http403Result();
        }
        else
            base.HandleUnauthorizedRequest(filterContext);
    }
}

HandleUnauthorizedRequest is overridden, so it returns Http403Result when using Ajax. Http403Result changes StatusCode to 403 and returns message to the user in response. There is some additional logic in attribute (authorize parameter), because I turn on [Authorize] in base controller and disable it in some pages.

Other important part is global handling of this response on client side. This is what I placed in Site.Master:

<script type="text/javascript">
    $(document).ready(
        function() {
            $("body").ajaxError(
                function(e,request) {
                    if (request.status == 403) {
                        alert(request.responseText);
                        window.location = '/Logout';
                    }
                }
            );
        }
    );
</script>

I place GLOBAL ajax error handler and when evert $.post fails with 403 error, response message is alerted and user is redirected to logout page. Now I don't have to handle error in every $.post request, because it is handled globally.

Why 403, not 401? 401 is handled internally by MVC framework (that is why redirection to login page is done after failed authorization).

What do you think about it?

EDIT:

About resigning from [Authorize] attribute: [Authorize] is not only about checking Identity.IsAuthenticated. It also handles page caching (so you don't cache material that requires authentication) and redirection. There is no need to copy this code.

Comments

2

You might look into the AjaxOptions that can be set in Ajax.BeginForm(). There is an OnBegin setting that you can associate with a javascript function, which could call a Controller method to confirm that the session is still valid, and if not, redirect to the login page using window.location.

Comments

1

Part of the problem appears to be that you're letting the framework do everything. I wouldn't decorate your AJAX method with the [Authorize] attribute. Instead check User.Identity.IsAuthenticated and if it returns false, create sensible error message.

2 Comments

Thanks! I implemented this in the OnActionExecuting (see above).
Authorize attribute is OK, but you have to modify it properly. Checking User.Identity.IsAuthenticated is not the same.
0

My solution uses one meta-tag on login form and a bit of Javascript/jQuery.

LogOn.cshtml

<html>
  <head>
    <meta data-name="__loginform__" content="true" />
    ...
  </head>
  ...
</html>

Common.js

var Common = {
    IsLoginForm: function (data) {
        var res = false;

        if (data.indexOf("__loginform__") > 0) {
            // Do a meta-test for login form
            var temp =
                $("<div>")
                    .html(data)
                    .find("meta[data-name='__loginform__']")
                    .attr("content");

            res = !!temp;
        }
        return res;
    }
};

AJAX code

$.get(myUrl, myData, function (serverData) {
    if (Common.IsLoginForm(serverData)) {
        location.reload();
        return;
    }

    // Proceed with filling your placeholder or whatever you do with serverData response
    // ...
});

Comments

-1

Here's how I did it...

In my base controller

 protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                filterContext.HttpContext.Response.StatusCode = 403;
                filterContext.HttpContext.Response.Write(SessionTimeout);
                filterContext.HttpContext.Response.End();
            }
        }
    }

Then in my global .js file

$.ajaxSetup({
error: function (x, status, error) {
    if (x.status == 403) {
        alert("Sorry, your session has expired. Please login again to continue");
        window.location.href = "/Account/Login";
    }
    else {
        alert("An error occurred: " + status + "nError: " + error);
    }
}

});

The SessionTimeout variable is a noty string. I omitted the implementation for brevity.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.