Greg Roberts
Greg Roberts

Reputation: 2562

In Asp.Net MVC 2 is there a better way to return 401 status codes without getting an auth redirect

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.

Upvotes: 7

Views: 3645

Answers (5)

davidtbernal
davidtbernal

Reputation: 13684

Another way of doing this is to implement a custom ActionResult. In my case, I wanted one anyway, since I wanted a simple way of sending data with custom headers and response codes (for a REST API.) I found the idea of doing a DelegatingActionResult and simply added to it a call to Response.End(). Here's the result:

public class DelegatingActionResult : ActionResult
{
    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        Command(context);
        // prevent ASP.Net from hijacking our headers
        context.HttpContext.Response.End();
    }

    private readonly Action<ControllerContext> Command;

    public DelegatingActionResult(Action<ControllerContext> command)
    {
        if (command == null)
            throw new ArgumentNullException("command");

        Command = command;
    }
}

Upvotes: 2

Greg Roberts
Greg Roberts

Reputation: 2562

I'm still using the end request technique, so I thought I would make that the answer, but really either of the options listed here are generally what I would say are the best answers so far.

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

Upvotes: 0

SDReyes
SDReyes

Reputation: 9954

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.

Upvotes: 0

Ashley Tate
Ashley Tate

Reputation: 593

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.

Upvotes: 0

Darin Dimitrov
Darin Dimitrov

Reputation: 1039328

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();
    }
}

Upvotes: 5

Related Questions