tags:

views:

316

answers:

2

Using a technique I found in one of the recent ASP.NET MVC books, I have an action method on a controller that returns a partial view for an ajax request and a full view actionresult for a normal get request -- it checks the IsAjaxRequest property of the Request object to determine which type of actionresult to return. The partial view returned by the action method returns the HTML to display a table of records from a database. The controller that contains the action method is tagged with the Authorize attribute so that only logged in users can call the controller's methods. I am using forms authentication with a 30-minute timeout and sliding expiration.

The problem arises after the user's 30-minute timeout has been reached. Because the controller is marked with the Authorize attribute, a call to the action method after the expiration timeout redirects the user to the login page. However, because this is an ajax call, the html for my login page is returned and rendered in the middle of the page that should contain the HTML table of records that would normally be returned by the action method in the partial view. The ajax call is not really failing, just returning the html for the wrong page.

Has anyone encountered and dealt with this problem? I am trying to avoid having to move all of my server side code that handles ajax calls into a separate controller that does not require an authenticated user, but that seems like my only alternative at this point. Even that won't produce the behavior I would expect, because it will allow the user to continue using the web page even after the 30 minute timeout has been reached -- it will not redirect to the login page.

Thanks for any advice.


Edit

The solution below with the custom AuthorizeAttribute would seem to have me heading in the right direction, but I cannot even get to this code. It appears that the code in the custom AuthorizeAttribute is never reached after the expiration timeout is reached. It seems like forms authentication is causing a redirect to the login page long before the attribute code. The custom AuthorizeAttribute is the only one on my controller. I also have the following web.config values (the timeout value is set extremely low to trigger a timeout for testing):

<authentication mode="Forms">
        <forms loginUrl="~/Account/Login" timeout="1" slidingExpiration="true" defaultUrl="~/ErrorReport/Index" requireSSL="true" protection="All"/>
</authentication>
<authorization>
        <deny users="?"/>
        <allow users="*"/>
</authorization>
<location path="Content">
     <system.web>
      <authorization>
       <allow users="*"/>
      </authorization>
     </system.web>
    </location>
<location path="Scripts">
     <system.web>
      <authorization>
       <allow users="*"/>
      </authorization>
     </system.web>
</location>

Are the authorization web.config elements getting in the way? Should I not be using them with ASP.NET MVC?

+1  A: 

Yes, this problem is fairly common. It also has a simple solution - in the part of the Javascript which is handling the AJAX response, check for a timeout before using the response. If the timeout has occurred, redirect the user to the login page.

Alternately, if the server-side script is itself checking for a session timeout (which appears to be your case), it becomes even simpler. Do not return an entire login page, instead just return a flag like 'need_login'. Further, simply write the Javascript handler to check if the returned value is 'need_login' and load the login page if true.

Crimson
+2  A: 

I actually have a blog post queued up about this that I'll add to the comment when it has been posted, but in the time being here's what's happening.

At a high level, when the ASP.NET runtime sees a 401 response code it will automatically convert it to a redirect and send the user to the login page. What you want to do is bypass that 401.

In MVC, when you're using the AuthorizeAttribute, it checks to see if the user is authorized and, if not, returns an HttpUnauthorizedResult. This basically forces the runtime to redirect the user to the login page. What you want to do is override this behavior.

To do this, extend the AuthorizeAttribute as such:

public class AuthorizeWithAjaxAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.HttpContext.Response.StatusCode = 200;
            filterContext.Result = /* Some result recognized by the client */
        }
    }
}

When you make an AJAX request and the data of the response is equal to what you return as the filter context's result, simply redirect the user to the login page via javascript.

andymeadows
Thanks for this solution. This expands on what Crimson provided above, and I like the custom attribute solution. Just for my understanding, what is it about the code above where IsAjaxRequest == true that short-circuits MVC from executing the action method? Is it the setting of the StatusCode to 200 or setting the Result? Both? Once the statuscode is 200, I don't see what is keeping MVC from routing the request to the appropriate action method.
Rich
I can't make this code execute once a user's login has timed out. I compiled this code into my project and attached the AuthorizeWithAjax attribute to my controller. I put a breakpoint on the base.On... line, and the breakpoint is only hit when a request is made on the controller and the user's login has not timed out. Once timed out, the breakpoint is never hit. It's as if forms authentication reroutes the user to the login page before the controller code is ever called. It makes me wonder if the standard Authorize attribute is ever hit in this scenario.
Rich
To answer the first comment is yes, it is the 200 that prevents the redirect from firing. As for the second one, make sure you don't have a standard Authorize there as well. This one serves as both. If you only have the AuthorizeWithAjaxAttribute there and you're still getting the behavior of the breakpoint not firing, my second guess is attribute order.
andymeadows