Prevent FormsAuthenticationModule of intercepting ASP.NET Web API responses
E

3

11

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.

Erect answered 18/6, 2012 at 17:7 Comment(2)
For anyone else looking, try this: blog.craigtp.co.uk/post/OWIN-Hosted-Web-API-in-an-MVC-ProjectUnassailable
The link posted by @Unassailable fixes my issue.... thanks!Japan
A
4

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.

Autocephalous answered 19/6, 2012 at 10:56 Comment(11)
I am using beta because last week I tried to get latest version, and the DependencyResolver was not working as expected, so Ninject MVC didn't work, I tried a couple of workarounds proposed for the mean time, but the didn't work. Do you know any thing about that? Thanks a million.Erect
@NullOrEmpty Oh OK. I am on the RC with Ninject (moved from Autofac as I had issues in the early days of the RC). This link describes an approach identical to mine and should work fine strathweb.com/2012/05/…Autocephalous
I am using the RC and forms auth still seems to be kicking requests to my login screen. I tried to allow unauthenticated to my api path in web.config but that doesn't seem to help.Ferry
@Ferry I will double check this weekend but just a thought - are you using the correct attribute for Authorise? #9483482Autocephalous
@Ferry Example project now on GitHub link. The web config has forms enabled but don't put the Authorise nodes in the webconfig e.g. deny "?". Instead you need to use the two versions of the Authorise attribute MVC Web [System.Web.Mvc.Authorize] Web API [System.Web.Http.Authorize] in the controllers AND/OR in the global filters Link.Autocephalous
Mark... ah see my problem is that this is a hybrid app which also contains web forms. So, if I remove the deny ? then all of the web form are accessible without authenticating.Ferry
@Ferry oh OK I will edit the answer to clarify the limitation. It looks like you may be needing some workarounds like inheriting all WebForms from a base page that checks OnLoad the HttpContext.Current.Request.IsAuthenticated and throws a 401 if not authenticated or a perhaps even more ugly a custom authentication handler codeproject.com/Articles/5353/…. Sorry I want able to help on this occasion.Autocephalous
@MarkJones I was hoping I could do something as simple as adding a location to my web config which allows access to my api path. But alas, that doesn't seem to work. Sigh. thanks for the info.Ferry
@Ferry - it is a bit rubbish really. I may have a dig around in the source and see what is happening in the http controller chains. It may be a legimate bug/enhancement...Autocephalous
@MarkJones I found a way around this. Luckily all of my .aspx forms are in sub folders. So, I am just adding a web.config to each with a deny anonymous. I'll probably blog about it this weekend.Ferry
@Ferry well done mate - that's a cunning solution. Give me shout if you wanted a link to your article on the question (or if you can edit yourself).Autocephalous
A
5

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..

Alert answered 6/7, 2013 at 6:8 Comment(0)
A
4

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.

Autocephalous answered 19/6, 2012 at 10:56 Comment(11)
I am using beta because last week I tried to get latest version, and the DependencyResolver was not working as expected, so Ninject MVC didn't work, I tried a couple of workarounds proposed for the mean time, but the didn't work. Do you know any thing about that? Thanks a million.Erect
@NullOrEmpty Oh OK. I am on the RC with Ninject (moved from Autofac as I had issues in the early days of the RC). This link describes an approach identical to mine and should work fine strathweb.com/2012/05/…Autocephalous
I am using the RC and forms auth still seems to be kicking requests to my login screen. I tried to allow unauthenticated to my api path in web.config but that doesn't seem to help.Ferry
@Ferry I will double check this weekend but just a thought - are you using the correct attribute for Authorise? #9483482Autocephalous
@Ferry Example project now on GitHub link. The web config has forms enabled but don't put the Authorise nodes in the webconfig e.g. deny "?". Instead you need to use the two versions of the Authorise attribute MVC Web [System.Web.Mvc.Authorize] Web API [System.Web.Http.Authorize] in the controllers AND/OR in the global filters Link.Autocephalous
Mark... ah see my problem is that this is a hybrid app which also contains web forms. So, if I remove the deny ? then all of the web form are accessible without authenticating.Ferry
@Ferry oh OK I will edit the answer to clarify the limitation. It looks like you may be needing some workarounds like inheriting all WebForms from a base page that checks OnLoad the HttpContext.Current.Request.IsAuthenticated and throws a 401 if not authenticated or a perhaps even more ugly a custom authentication handler codeproject.com/Articles/5353/…. Sorry I want able to help on this occasion.Autocephalous
@MarkJones I was hoping I could do something as simple as adding a location to my web config which allows access to my api path. But alas, that doesn't seem to work. Sigh. thanks for the info.Ferry
@Ferry - it is a bit rubbish really. I may have a dig around in the source and see what is happening in the http controller chains. It may be a legimate bug/enhancement...Autocephalous
@MarkJones I found a way around this. Luckily all of my .aspx forms are in sub folders. So, I am just adding a web.config to each with a deny anonymous. I'll probably blog about it this weekend.Ferry
@Ferry well done mate - that's a cunning solution. Give me shout if you wanted a link to your article on the question (or if you can edit yourself).Autocephalous
D
3

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.

Dominican answered 24/1, 2013 at 19:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.