Skip to content

Using cookie authentication middleware with Web API and 401 response codes

October 27, 2013

If you want to use cookie authentication middleware with a project that contains both ASP.NET code (WebForms or MVC) and Web API, then in the new Visual Studio 2013 you might notice some odd behavior when your Web API issues an unauthorized (401) HTTP response code. The assumption here is that the Web API code wants the authentication outcome from the cookie middleware, so you will not use SuppressDefaultHostAuthentication (for a little context see this post).

Normally when using cookie authentication middleware, when the server (MVC or WebForms) issues a 401, then the response is converted to a 302 redirect to the login page (as configured by the LoginPath on the CookieAuthenticationOptions). But when an Ajax call is made and the response is a 401, it would not make sense to return a 302 redirect to the login page. Instead you’d just expect the 401 response to be returned. Unfortunately this is not the behavior we get with the cookie middleware — the response is changed to a 200 status code with a JSON response body with a message:

{"Message":"Authorization has been denied for this request."}

I’m not sure what the requirement was for this feature. To alter it, you must take over control of the behavior when there is a 401 unauthorized response by configuring a CookieAuthenticationProvider on the cookie authentication middleware:

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

Notice it handles the OnApplyRedirect event. When the call is not an Ajax call, we redirect. Otherwise, we do nothing which allows the 401 to be returned to the caller.

The check for IsAjaxRequest is simply copied from a helper in the katana project:


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

HTH

32 Comments leave one →
  1. October 28, 2013 8:53 am

    Great observation and solution:)

  2. November 24, 2013 3:57 am

    Brock is this example still valid with the latest released versions of the owin components? im using 2.0.2 and when i use the code above, a ajax request from a mvc view to a api controller in the same project isnt recognised as an ajax request. Other methods of using a custom Authorization attribute dont work either http://stackoverflow.com/questions/20149750/.

    • November 24, 2013 5:39 pm

      Well, it does and doesn’t. It does, in that the cookie MW will issue a 302 if you’ve set the login redirect path property, but when you’re also doing WebAPI in the same project you tend to set the “suppress host authentication” filter thus the cookie MW won’t get involved.

      • November 25, 2013 5:34 pm

        so is there a way to handle both potential options? ive seen several suggestions for Attribute filters or application_endrequest events but unless im missing something ive not found one which i can easily apply to a api controller in a mvc project which i can use from javascript and get a detectable combination of return values (depending on authenticated status) without getting a loginpage redirect in the json response.

  3. December 2, 2013 5:07 am

    as per Kevin Junghans suggestion http://stackoverflow.com/a/20151805/600188 i’ve added headers to angular and your example works like a treat. https://github.com/tbertenshaw/MVCAPI

  4. January 13, 2014 4:56 pm

    Little late to the game, but HUGE thank you. Kept googling for “MVC”, “API” “AuthorizeAttribute” “200 OK” (and any permutation thereof) and found nothing. Finally found this post and had great success. Works as expected with a 401 StatusCode (like anticipated but not receiving).

    Big thank you!

  5. monkeysquid permalink
    February 4, 2014 3:22 pm

    Thank you Brock. I’ve extended your solution to also check for Content-Type: application/json to catch other calls to the API controllers.

    private static bool IsJsonRequest(IOwinRequest request)
    {
    IHeaderDictionary headers = request.Headers;
    return ((headers != null) && (headers[“Content-Type”] == “application/json”));
    }

  6. KARTHIKEYAN N R permalink
    July 25, 2014 12:52 am

    You save my day Mr. Brock… One ques.. If the user logout and press the back button as you said in your article nothing happens and that is ok for normal operations, Is it possible to redirect to login page (if it is an ajax call) for an unauthorized request. ?

    • July 25, 2014 8:48 am

      For the back button the browser uses the client cache heavily, so you’d need to disable that caching first. This is a common question/issue — I’d suggest searching for the common solutions.

  7. Dmitri M permalink
    October 20, 2014 3:57 pm

    Running into an issue with the new Owin release 3.0.0. If I keep notification.HandleResponse() I get the 200 status and 401 in X-Responded-JSON header, if I remove it – i get 302 redirect. I tried setting the response code manually after HandleResponse() is called, but that doesn’t seem to change anything. Any help would be appreciated.

    Thanks!

    RedirectToIdentityProvider = notification =>
    {
    if (IsAjaxRequest(notification.Request) || IsJsonRequest(notification.Request))
    {
    notification.HandleResponse();
    return Task.FromResult(0);
    }
    }

  8. November 12, 2014 12:40 am

    I was able to fix this by simply removing the Login redirect from the config altogether, since I was only using this site as a Web API and login host for a JQuery based mobile/web app.

    The app_start/Startup.Auth.vb was:
    .LoginPath = New PathString(“/Account/Login”)
    and I converted it to
    .LoginPath = Nothing

  9. KARTHICK permalink
    November 25, 2014 12:42 am

    Dear Brockallen, Is it possible to redirect Ajax call to session expire page in the Startup class ….?

    • November 25, 2014 7:44 am

      I’m not sure what you’re trying to do. Ajax calls shouldn’t do 302s – they should either return 401 or the normal result.

  10. tafs7 permalink
    December 4, 2014 4:24 pm

    Is this solution still the only way to make it work as of Dec 2014, when using Web API 2.2 in an MVC5 app that authenticates with cookies using ASP.NET Identity 2.2?

    My login and home/index (secured) views are regular MVC views, but once you hit the home/index, I load an Angular module and it’s most SPA from that point on, just talking with Web API controllers.

    I don’t want to use bearer tokens because the api controllers will only be used from the context of this web app via a browser. Also, I’ll have some other pages that won’t be SPA in the future, so ideally I should only have 1 method of authentication (cookies).

    Seems like a pretty common scenario to support out of the box when using the VS2013 project template that loads both WebAPI and MVC with Identity, but that sample is really convoluted.

    • tafs7 permalink
      December 4, 2014 5:00 pm

      so i dug into the source for CookieAuthenticationProvider, and it looks like the default ctor sets the OnApplyRedirect to a DefaultBehavior.ApplyRedirect, which is an Action, and it does something similar…but still doesn’t seem to prevent the 302s for me:

      if (IsAjaxRequest(context.Request))
      {
      var respondedJson = new RespondedJson
      {
      Status = context.Response.StatusCode,
      Headers = new RespondedJson.RespondedJsonHeaders
      {
      Location = context.RedirectUri
      },
      };

      context.Response.StatusCode = 200;
      context.Response.Headers.Append(“X-Responded-JSON”, respondedJson.ToString());
      }
      else
      {
      context.Response.Redirect(context.RedirectUri);
      }

    • December 5, 2014 5:42 am

      You should use bearer tokens even if the client is just JS from the same app — this is due to XSRF issues.

      • tafs7 permalink
        December 5, 2014 3:37 pm

        so here’s the thing…I’ve already built it out with cookies (no tokens), and my login view is a regular MVC view. I do have concerns about the XSRF issue you brought about, though.

        I am not clear on the effort needed to retrofit tokens here. Ideally, I would like to have both in tandem, so that when I login using the MVC controller I can issue the cookie with the cookie middleware, as well as issue a bearer token. Then, from AngularJS, when an AJAX call is made, it sends the token and the cookie to the server (read on for why I need the cookie).

        I add custom claims (roles and current tenant ID) to my user when the sign in happens so those get placed in the auth cookie. I actually have my roles table defined outside of Identity’s roles (in my app’s DbContext, and I keep Identity in its separate DB with the default IdentityDbContext). I have to support a business req in my domain where a user role needs to be tied to a “tenant” (IOW, a user can have access to multiple tenants but have different roles for each tenant).

        I am not sure how bearer tokens alone would allow me to do all this, as if I understand it correctly, I wouldn’t get the auth cookie so I can keep the roles and current tenant ID for the logged in user.

  11. Turlough permalink
    February 3, 2015 8:19 am

    I am new at asp and am not sure where I should put the following code?
    You said the cookie authentication middleware!

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

    Thanks !

  12. June 5, 2015 3:43 pm

    Still an issue in June of 2015, thanks for the fix, Brock! Rather than check for a particular header (which Angular doesn’t pass anyway), I’m checking if they are requesting something under the /api path…

    if (!ctx.Request.Uri.AbsolutePath.ToLower().StartsWith(“/api/”))
    {
    ctx.Response.Redirect(ctx.RedirectUri);
    }

  13. bob permalink
    March 4, 2016 12:23 pm

    I have ajax requests outside of the webapi or for ajax loading html views in a single page app. I am using suppressauthentication for my webapi, so should I just remove the IsAjaxRequest and do a redirect for ajax as well? Or would it be preferable to do this on the client?

    • March 4, 2016 5:11 pm

      Redirects for login pages only really work from a browser window. I don’t think you want ajax calls for partial views showing a login screen.

  14. Imran Rashid permalink
    October 8, 2016 1:15 am

    Thanks Brock.

    Been banging my head over this for hours.

  15. Matt permalink
    November 3, 2016 3:24 pm

    Thank you, thank you! This solved a confusing issue that I was running into with cookie authentication for AJAX calls.

    Basically, the 401 responses were being converted into 200 responses calls (with X-Responded-JSON header containing the 401 status), and IE11 was caching them (since the no-cache header wasn’t being set on these 200 responses). Even after the user logged in, IE11 still refused to make the call and instead returned the cached response with X-Responded-JSON header. I know I could work around it by adding cache:false in the jQuery config (which appends a random value to the request), but that seems like a hack when there is a better solution available.

    By returning a proper 401, IE11 stopped caching the results.

  16. December 17, 2016 12:24 am

    I personally added this to the IsAjaxRequest as if they leave the authentication header out entirely both of those checks return null.

    if (headers != null && headers[“Accept”].Equals(“application/json”)) return true;

Trackbacks

  1. ASP.NET MVC, [Authorize] y jQuery.load - Burbujas en .NET
  2. Always success on ajax post with HttpResponseMessage 401 | Ziniewicz Answers
  3. 不安全的直接对象引用:你的 ASP.NET 应用数据是否安全? – 码农网

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: