Question

In ASP.NET the FormsAuthenticationModule intercepts any HTTP 401, and returns an HTTP 302 redirection to the login page. This is a pain for AJAX, since you ask for json and get the login page in html, but the status code is HTTP 200.

What is the way of avoid this interception in ASP.NET Web API ?

In ASP.NET MVC4 it is very easy to prevent this interception by ending explicitly the connection:

public class MyMvcAuthFilter:AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest() && !filterContext.IsChildAction)
        {
            filterContext.Result = new HttpStatusCodeResult(401);
            filterContext.HttpContext.Response.StatusCode = 401;
            filterContext.HttpContext.Response.SuppressContent = true;
            filterContext.HttpContext.Response.End();
        }
        else
            base.HandleUnauthorizedRequest(filterContext);
    }
}

But in ASP.NET Web API I cannot end the connection explicitly, so even when I use this code the FormsAuthenticationModule intercepts the response and sends a redirection to the login page:

public class MyWebApiAuth: AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if(actionContext.Request.Headers.Any(h=>h.Key.Equals("X-Requested-With",StringComparison.OrdinalIgnoreCase)))
        {
            var xhr = actionContext.Request.Headers.Single(h => h.Key.Equals("X-Requested-With", StringComparison.OrdinalIgnoreCase)).Value.First();

            if (xhr.Equals("XMLHttpRequest", StringComparison.OrdinalIgnoreCase))
            {
                // this does not work either
                //throw new HttpResponseException(HttpStatusCode.Unauthorized);

                actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
                return;
            }
        }

        base.HandleUnauthorizedRequest(actionContext);
    }
}

What is the way of avoiding this behaviour in ASP.NET Web API? I have been taking a look, and I could not find a way of do it.

Regards.

PS: I cannot believe that this is 2012 and this issue is still on.

Was it helpful?

Solution

The release notes for MVC 4 RC imply this has been resolved since the Beta - which are you using?

http://www.asp.net/whitepapers/mvc4-release-notes Unauthorized requests handled by ASP.NET Web API return 401 Unauthroized: Unauthorized requests handled by ASP.NET Web API now return a standard 401 Unauthorized response instead of redirecting the user agent to a login form so that the response can be handled by an Ajax client.

Looking into the source code for MVC there appears to be an functionality added via SuppressFormsAuthRedirectModule.cs

http://aspnetwebstack.codeplex.com/SourceControl/network/forks/BradWilson/AspNetWebStack/changeset/changes/ae1164a2e339#src%2fSystem.Web.Http.WebHost%2fHttpControllerHandler.cs.

    internal static bool GetEnabled(NameValueCollection appSettings)
    {
            // anything but "false" will return true, which is the default behavior

So it looks this this is enabled by default and RC should fix your issue without any heroics... as a side point it looks like you can disable this new module using AppSettings http://d.hatena.ne.jp/shiba-yan/20120430/1335787815:

<appSettings> 
    <Add Key = "webapi:EnableSuppressRedirect"  value = "false" /> 
</appSettings>

Edit (example and clarification)

I have now created an example for this approach on GitHub. The new redirection suppression requires that you use the two correct "Authorise" attribute's; MVC Web [System.Web.Mvc.Authorize] and Web API [System.Web.Http.Authorize] in the controllers AND/OR in the global filters Link.

This example does however draw out a limitation of the approach. It appears that the "authorisation" nodes in the web.config will always take priority over MVC routes e.g. config like this will override your rules and still redirect to login:

<system.web>
    <authentication mode="Forms">
    </authentication>
    <authorization>
        <deny users="?"/> //will deny anonymous users to all routes including WebApi
    </authorization>
</system.web> 

Sadly opening this up for some url routes using the Location element doesn't appear to work and the WebApi calls will continue to be intercepted and redirected to login.

Solutions

For MVC applications I am simply suggest removing the config from Web.Config and sticking with Global filters and Attributes in the code.

If you must use the authorisation nodes in Web.Config for MVC or have a Hybrid ASP.NET and WebApi application then @PilotBob - in the comments below - has found that sub folders and multiple Web.Config's can be used to have your cake and eat it.

OTHER TIPS

In case someone's interested in dealing with the same issue in ASP.NET MVC app using the Authorize attribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class Authorize2Attribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new HttpStatusCodeResult((int) HttpStatusCode.Forbidden);
        }
        else
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
            }
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
} 

This way browser properly distinguishes between Forbidden and Unauthorized requests..

I was able to get around the deny anonymous setting in web.config by setting the following property:

Request.RequestContext.HttpContext.SkipAuthorization = true;

I do this after some checks against the Request object in the Application_BeginRequest method in Global.asax.cs, like the RawURL property and other header information to make sure the request is accessing an area that I want to allow anonymous access to. I still perform authentication/authorization once the API action is called.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top