views:

220

answers:

3

I have a portion of my site that has a lightweight xml/json REST API. Most of my site is behind forms auth but only some of my API actions require authentication.

I have a custom AuthorizeAttribute for my API that I use to check for certain permissions and when it fails it results in a 401. All is good, except since I'm using forms auth, Asp.net conveniently converts that into a 302 redirect to my login page.

I've seen some previous questions that seem a bit hackish to either return a 403 instead or to put some logic in the global.asax protected void Application_EndRequest() that will essentially convert 302 to 401 where it meets whatever criteria.

What I'm doing now is sort of like one of the questions, but instead of checking the Application_EndRequest() for a 302 I make my authorize attribute return 666 which indicates to me that I need to set this to a 401.

Here is my code:

protected void Application_EndRequest()
{
  if (Context.Response.StatusCode == MyAuthAttribute.AUTHORIZATION_FAILED_STATUS)
   {   
       //check for 666 - status code of hidden 401
        Context.Response.StatusCode = 401;
    }
 }

Even though this works, my question is there something in Asp.net MVC 2 that would prevent me from having to do this? Or, in general is there a better way? I would think this would come up a lot for anyone doing REST api's or just people that do ajax requests in their controllers. The last thing you want is to do a request and get the content of a login page instead of json.

+1  A: 

How about decorating your controller/actions with a custom filter:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class RequiresAuthenticationAttribute : FilterAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var user = filterContext.HttpContext.User;
        if (!user.Identity.IsAuthenticated)
        {
            filterContext.HttpContext.Response.StatusCode = 401;
            filterContext.HttpContext.Response.End();
        }
    }
}

and in your controller:

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

    [RequiresAuthentication]
    public ActionResult AuthenticatedIndex()
    {
        return View();
    }
}
Darin Dimitrov
right that's what I'm doing, but asp.net will take that 401 and convert it to a 302 redirect to the login page. I need to be able to go around that redirect.
Greg Roberts
Did you notice the `filterContext.HttpContext.Response.End();` call?
Darin Dimitrov
This may work but seems to cause other issues. Like throwing uncaught exceptions: "Cannot redirect after HTTP headers have been sent." Thanks, but I was looking for a cleaner solution that didn't have these side effects.
Greg Roberts
A: 

The simplest and cleanest solution I've found for this is to register a callback with the jQuery.ajaxSuccess() event and check for the "X-AspNetMvc-Version" response header.

Every jQuery Ajax request in my app is handled by Mvc so if the header is missing I know my request has been redirected to the login page, and I simply reload the page for a top-level redirect:

 $(document).ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
    // if request returns non MVC page reload because this means the user 
    // session has expired
    var mvcHeaderName = "X-AspNetMvc-Version";
    var mvcHeaderValue = XMLHttpRequest.getResponseHeader(mvcHeaderName);

    if (!mvcHeaderValue) {
        location.reload();
    }
});

The page reload may cause some Javascript errors (depending on what you're doing with the Ajax response) but in most cases where debugging is off the user will never see these.

If you don't want to use the built-in header I'm sure you could easily add a custom one and follow the same pattern.

Ashley Tate
A: 

TurnOffTheRedirectionAtIIS

From MSDN, This article explains how to avoid the redirection of 401 responses : ).

Citing:

Using the IIS Manager, right-click the WinLogin.aspx file, click Properties, and then go to the Custom Errors tab to Edit the various 401 errors and assign a custom redirection. Unfortunately, this redirection must be a static file—it will not process an ASP.NET page. My solution is to redirect to a static Redirect401.htm file, with the full physical path, which contains javascript, or a meta-tag, to redirect to the real ASP.NET logon form, named WebLogin.aspx. Note that you lose the original ReturnUrl in these redirections, since the IIS error redirection required a static html file with nothing dynamic, so you will have to handle this later.

Hope it helps you.

SDReyes
Thanks for your comment. I'm not sure this is really much better than what I'm already doing. I don't want to avoid the redirects all together, I just want better control for the api. What I'm doing works, it just seems like there could be a better way.
Greg Roberts