Returning 401 HTTP Status Code on Authentication Failure in MVC 5 Web API's

The behavior of how a Web API responds to authentication/authorization exceptions has significantly changed in MVC 5. First, a little background. In a previous article I demonstrated how to create a custom AuthorizeAttribute that mixes basic authentication with forms authentication when using Web API's.   This custom attribute was designed to return an HTTP status code of 401 (Unauthorized) if authentication failed and a 403 (Forbidden) if the user is not authorized.  The example code was written as part of the SimpleSecurity Project, which was originally written to decouple and enhance the ASP.NET membership provider SimpleMembership.  I recently ported this code to work with the new ASP.NET Identity which replaces SimpleMembership in MVC 5.  It turns out that during my testing of the port I did not do a good job of testing error conditions. Bad tester.

The security pipeline in OWIN and MVC 5 has changed and the custom attribute was no longer returning 401 and 403 status codes. Instead it was returning a 200 status code and inserting some additional information in the header.

X-Responded-JSON: {"status":401,"headers":{"location":"http:\/\/localhost:59540\/Account\/Login?ReturnUrl=%2Fapi%2FTestBasic"}}

So now the status code is in the header as part of this JSON object and it also contain the URL for the login page so that we can redirect to it. We could just change the logic in our client JavaScript to support this change and just get the status from the header and handle it accordingly. But if you want your application to behave the same way it was in MVC 4 there is a solution. I found the solution in this article.

You need to modify the code in your Startup.Auth.cs file to have it return the status codes you set in the response.  Here is what the updated code looks like.

    public partial class Startup
    {
        private static bool IsAjaxRequest(IOwinRequest request)
        {
            IReadableStringCollection query = request.Query;
            if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
            {
                return true;
            }
            IHeaderDictionary headers = request.Headers;
            return ((headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest"));
        }

        // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            // Enable the application to use a cookie to store information for the signed in user
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    OnApplyRedirect = ctx =>
                    {
                        if (!IsAjaxRequest(ctx.Request))
                        {
                            ctx.Response.Redirect(ctx.RedirectUri);
                        }
                    }
                }
            });
            // Use a cookie to temporarily store information about a user logging in with a third party login provider
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        }
    }

Now our application behaves like it did before we upgraded to MVC 5. I have updated the SimpleSecurity reference application with this modification.  If you prefer to handle this change in your client code you can just remove this modification.

Comments

Popular posts from this blog

Using Claims in ASP.NET Identity

Seeding & Customizing ASP.NET MVC SimpleMembership

Customizing Claims for Authorization in ASP.NET Core 2.0