Jan 17 2011

Handling Authorization Failures for AJAX Requests in ASP.NET MVC Applications

Category: Matt @ 16:29

Authorization failures in ASP.NET MVC applications are usually not something you worry about.  The ASP.NET pipeline has solid support for this scenario and will redirect the user to your application’s login page for you automagically.  Unfortunately, things become more complicated once you introduce AJAX requests into the mix.  Without doing some extra work, you can create a bad user experience and increase the burden of developing AJAX-enabled client script.  In this post, I’ll show you an elegant way to handle authorization failures that overcomes these problems.

The Problem in Detail

Thanks to the power of libraries like jQuery and jQuery UI, it is now trivial to AJAX-enable most web applications.  This is especially true in ASP.NET MVC, where we can quite easily render our actions using AJAX instead of requiring a complete reload of the entire page.  While things work great the vast majority of the time, there’s one very common edge case that you are very likely to run in to related to authorization.  Here’s the scenario.

Let’s say you have an application with a “members only” area.  You authenticate your users with the standard ASP.NET forms authentication infrastructure, and you decorate the controllers and actions you want to protect with the Authorize attribute, like so:

[Authorize]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

When an unauthenticated user requests a protected action, they are redirected to your application’s login form:

image

After logging in, they are redirected back to the protected action. 

image

In an effort to keep your application secure, you configure Forms Authentication so that users’ sessions are expired after one hour. 

One of the things you want to display to the user is a “message of the day”.  You decide you will use jQuery and jQuery UI to make an AJAX request to a MessageOfTheDay action, and you will display the response’s HTML in a modal dialog, like so:

image

Everything works great!  Let’s go home!

Not so fast.  That MessageOfTheDay action is protected: only authenticated users can call it.  No problem though, because only authenticated users are ever going to be on our “members only” view, so we’ll never have an unauthenticated user invoking the action, right?  Wrong.  What happens when Average Joe User takes a 2 hour lunch while using your application, then decides to click “Get Message of the Day” after returning from lunch?  His session has timed out, so instead of getting the expected “message of the day” or something nice and clean such as a redirect to the login form, your user is now going to get this:

image

Ugh.   Our complete login form has been rendered inside the modal dialog.  This is definitely not the user experience we want.  What happened?? 

It turns out that ASP.NET did exactly what it’s supposed to do.  A request came in, the request was not authorized for the requested resource since the user’s session timed out, and so it denied the user access to the resource.  Unfortunately, the rest of the pipeline kicked in, which involved redirecting the request to the login action.  jQuery followed the redirect, and the end result is the login form showing up in our dialog, and a very confused user. 

The same thing could happen even if we weren’t displaying the raw result of the AJAX request.  What if we were posting a form back over AJAX?  At best, the action would silently fail.  At worst, the user may see an ugly JavaScript error because our AJAX callback wasn’t prepared to receive the site’s login form as the action’s response.

The Desired Solution

What should happen in this scenario?  There’s a few different ways that you could handle it.  We could correctly (meaning outside of the modal dialog) redirect the user back to the login form, but this may not make your user happy.  The user may be accustomed to whatever link they clicked not reloading the entire page.  What if the user was in the middle of filling out a form when they clicked the link?  All of their potentially important work would be lost.  We don’t want to break the user experience.  Instead, we could still present them with a modal dialog, one that indicates that their session has timed out, and asks them to please log in again after taking whatever steps they can to save their work.

There’s one more requirement for our desired solution that is not related to user experience, but is instead related to development experience.  We don’t want developers on our application worrying about the behavior of the ASP.NET authorization infrastructure for every AJAX-enabled feature they add to the application.  We want the correct handling to be baked into the framework so that they don’t have to think about it.  Developers should write normal JavaScript and MVC code, and authorization failures should automatically be handled correctly.  When an AJAX request is denied because of an authorization failure, we want this:

image

The Implementation

Arriving at our desired solution seems simple at first: all we need to do is detect, on the client-side, that an AJAX request has failed due to a an authorization error, display a dialog informing the user of this, then redirect to the login page.  But how do we tell that a request has failed because of authorization?  Remember that ASP.NET is not sending an HTTP error code, it’s actually sending an HTTP 302 result to the client.  We don’t want to try detecting the redirect client-side.  There could be times that an action wants to redirect to another action for a legitimate reason, and one of the requirements of our desired solution is that developers shouldn’t have to think about how we’re handling authorization failures.  What we really want is for ASP.NET to send us a HTTP 401 (Access Denied) result instead of the 302 when it denies an AJAX request.

Let’s assume we can make ASP.NET send back an HTTP 401 (Access Denied) result (because there has to be an easy way to do that, right?!?).  If that’s true, we can leverage jQuery’s global ajaxError handler to check for a 401 and handle it.  First, let’s add an invisible div to our master page containing the content we want to show to users if an AJAX request is not authorized:

<div id="main">
    ...

    <div id="session-timeout-dialog">
        <h3>You have been logged out...</h3>
        <p>You have been logged out of the system due to inactivity.  
        Please <%=Html.ActionLink<AccountController>(c => c.LogOn(), "login") %> again.</p>
    </div>

    ...
</div>

Next, let’s wire up jQuery’s global error handle in our Site.master:

$(function () {
    $(document).ajaxError(function (event, request, options) {

        if (request.status === 401) {
            $("#session-timeout-dialog").dialog({
                width: 500,
                height: 400,
                modal: true,
                buttons: {
                    Ok: function () {
                        $(this).dialog("close");
                    }
                }
            });
        }
        else {
            //TODO: Another error occurred, which we could handle globally here. 
        }
    });
});

Now whenever an AJAX request fails with a HTTP 401 response, users will see our modal dialog and be directed to the login page.  That’s all we need to do client-side, but server-side, we’re about to open an ugly can of worms.

As it turns out, ASP.NET really doesn’t want you to return a 401 response.  Deep within the bowels of the pipeline, ASP.NET watches for 401 responses and helpfully translates them into 302 redirects to your applications login page.  This logic is buried deep within the beast that is ASP.NET; it’s not part of the newer, more cleanly abstracted, and more flexible MVC plumbing. 

Fortunately, there is one thing we can do.  If we hook the application at exactly the right place, we can swap ASP.NET’s 302 redirect back to a 401.  This is a little tricky though, as we don’t want to replace all 302’s with 401’s (remember that there could be other reasons why we’re sending a redirect back for an AJAX request).  We only want to make the switch when the request was not authenticated. 

Solving the problem one step at a time, let’s first hook in to the request pipeline at the point where we can make the switch from a 302 back to a 401: the PreSendRequestHeaders event.  This is the only place I’ve found that you can safely make the switch.  Any later in the pipeline, and the 302 has already been sent to the client.  Any earlier, the bowels of ASP.NET will switch it back to a 401.  To  attach to this stage of the pipeline, we’ll create a custom HTTP module and hook it in to each request:

public class AjaxAuthorizationModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PreSendRequestHeaders += CheckForAuthFailure;
    }

    private void CheckForAuthFailure(object sender, EventArgs e)
    {
        var app = sender as HttpApplication;
        var response = new HttpResponseWrapper(app.Response);
        var request = new HttpRequestWrapper(app.Request);
        var context = new HttpContextWrapper(app.Context);

        CheckForAuthFailure(request, response, context);
    }

    internal void CheckForAuthFailure(HttpRequestBase request, HttpResponseBase response, HttpContextBase context)
    {
        if (response.StatusCode == 302 && request.IsAjaxRequest())
        {
            response.StatusCode = 401;
            response.ClearContent();
        }
    }

    public void Dispose()
    {
    }
}

There’s a problem with this code: we don’t want to make the switch for all 302 responses to AJAX requests.  We only want to make the swap when the redirect is the result of an authorization failure.  Let’s pretend that something magic is going to insert a well-known key into the HttpContext’s Items dictionary whenever a request fails the auth checks in our application.  All we need to do now is check for the presence of this key, and make the swap if the key is present:

internal void CheckForAuthFailure(HttpRequestBase request, HttpResponseBase response, HttpContextBase context)
{
    if (true.Equals(context.Items["RequestWasNotAuthorized"]) && request.IsAjaxRequest())
    {
        response.StatusCode = 401;
        response.ClearContent();
    }
}

Sadly there is no bit of magic that will add this key for us.  We have to do it ourselves.  We can achieve this easily by extending the Authorize action filter, like so, and using it in place of the standard Authorize attribute:

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

        if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.HttpContext.Items["RequestWasNotAuthorized"] = true;
        }
    }
}

With these two pieces in place server-side, we now get the desired user experience:

image

We do fail slightly at our development experience goal, unfortunately: developers have to know to use the custom AjaxAwareAuthorize filter instead of the standard one.  We can mitigate this by having a base controller for any controllers that host protected actions though, and applying the attribute there instead of requiring that developers add the attribute themselves.  It’s also likely that there’s a better extension point for bolting this on to the ASP.NET MVC pipeline than a custom action filter, but I haven’t invested the time to find it (yet).

Credit Where Credit Is Due

Solving this problem originally required a lot of hair-pulling and googling, and while I’m sure I picked up various small pieces for this solution from quite a few places across the web, the main source of inspiration was this Stack Overflow question and answer. 

Conclusion

In this post, I’ve shown you a technique for elegantly handling AJAX problems due to authorization failures.  Once implemented, the solution requires almost no extra work or special steps from those creating code on your project, and it provides a user experience that you can tailor to your particular application’s needs.

If you want to check out a working sample, you may download it below. It’s a bit rough around the edges.  For example, there’s a lot of inline JavaScript and inline CSS that you shouldn’t do in a production application, but it will provide you with a useful starting point for adding similar functionality to your own application.

Download Sample Application

Tags:

blog comments powered by Disqus