Unauthorised webapi call returning login page rather than 401

100,660

Solution 1

There are two AuthorizeAttribute implementations and you need to make sure you are referencing the correct one for Web API's. There is System.Web.Http.AuthorizeAttribute which is used for Web API's, and System.Web.Mvc.AuthorizeAttribute which is used for controllers with views. Http.AuthorizeAttribute will return a 401 error if authorization fails and Mvc.AuthorizeAttribute will redirect to the login page.

Updated 11/26/2013

So it appears things have drastically changed with MVC 5 as Brock Allen pointed out in his article. I guess the OWIN pipeline takes over and introduces some new behavior. Now when the user is not authorized a status of 200 is returned with the following information in the HTTP header.

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

You could change your logic on the client side to check this information in the header to determine how to handle this, instead of looking for a 401 status on the error branch.

I tried to override this behavior in a custom AuthorizeAttribute by setting the status in the response in the OnAuthorization and HandleUnauthorizedRequest methods.

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

But this did not work. The new pipeline must grab this response later and modify it to the same response I was getting before. Throwing an HttpException did not work either as it is just changed into a 500 error status.

I tested Brock Allen's solution and it did work when I was using a jQuery ajax call. If it is not working for you my guess is that it is because you are using angular. Run your test with Fiddler and see if the following is in your header.

X-Requested-With: XMLHttpRequest

If it is not then that is the problem. I am not familiar with angular but if it lets you insert your own header values then add this to your ajax requests and it will probably start working.

Solution 2

Brock Allen has a nice blog post on how to return 401 for ajax calls when using Cookie authentication and OWIN. http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

Put this in ConfigureAuth method in the Startup.Auth.cs file:

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

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

Solution 3

If you are adding asp.net WebApi inside asp.net MVC web site you probably want to respond unauthorized to some requests. But then ASP.NET infrastructure come into play and when you try to set response status code to HttpStatusCode.Unauthorized you will get 302 redirect to login page.

If you are using asp.net identity and owin based authentication here a code that can help to solve that issue:

public void ConfigureAuth(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                if (!IsApiRequest(ctx.Request))
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}


private static bool IsApiRequest(IOwinRequest request)
{
    string apiPath = VirtualPathUtility.ToAbsolute("~/api/");
    return request.Uri.LocalPath.StartsWith(apiPath);
}

Solution 4

I got the same situation when OWIN always redirects 401 response to Login page from WebApi.Our Web API supports not only ajax calls from Angular but also Mobile, Win Form calls. Therefore, the solution to check whether the request is ajax request is not really sorted for our case.

I have opted another approach is to inject new header response: Suppress-Redirect if responses come from webApi. The implementation is on handler:

public class SuppressRedirectHandler : DelegatingHandler
{
    /// <summary>
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(task =>
        {
            var response = task.Result;
            response.Headers.Add("Suppress-Redirect", "True");
            return response;
        }, cancellationToken);
    }
}

And register this handler in global level of WebApi:

config.MessageHandlers.Add(new SuppressRedirectHandler());

So, on OWIN startup you are able to check whether response header has Suppress-Redirect:

public void Configuration(IAppBuilder app)
{
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationMode = AuthenticationMode.Active,
        AuthenticationType = DefaultApplicationTypes.ApplicationCookie,
        ExpireTimeSpan = TimeSpan.FromMinutes(48),

        LoginPath = new PathString("/NewAccount/LogOn"),

        Provider = new CookieAuthenticationProvider()
        {
            OnApplyRedirect = ctx =>
            {
                var response = ctx.Response;
                if (!IsApiResponse(ctx.Response))
                {
                    response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });
}

private static bool IsApiResponse(IOwinResponse response)
{
    var responseHeader = response.Headers;

    if (responseHeader == null) 
        return false;

    if (!responseHeader.ContainsKey("Suppress-Redirect"))
        return false;

    if (!bool.TryParse(responseHeader["Suppress-Redirect"], out bool suppressRedirect))
        return false;

    return suppressRedirect;
}

Solution 5

In previous versions of ASP.NET, you had to do a whole bunch of stuff to get this working.

The good news is, since you are using ASP.NET 4.5. you can disable forms authentication redirect using the new HttpResponse.SuppressFormsAuthenticationRedirect property.

In Global.asax:

protected void Application_EndRequest(Object sender, EventArgs e)
{
        HttpApplication context = (HttpApplication)sender;
        context.Response.SuppressFormsAuthenticationRedirect = true;
}

EDIT: You might also want to take a look at this article by Sergey Zwezdin which has a more refined way of accomplishing what you are trying to do.

Relevant code snippets and author narration pasted below. Original Author of code and narration -- Sergey Zwezdin.

First – let’s determine whether current HTTP-request is AJAX-request. If yes, we should disable replacing HTTP 401 with HTTP 302:

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;

        if (request.IsAjaxRequest())
            response.SuppressFormsAuthenticationRedirect = true;

        base.HandleUnauthorizedRequest(filterContext);
    }
}

Second – let’s add a condition:: if user authenticated, then we will send HTTP 403; and HTTP 401 otherwise.

public class ApplicationAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        var httpContext = filterContext.HttpContext;
        var request = httpContext.Request;
        var response = httpContext.Response;
        var user = httpContext.User;

        if (request.IsAjaxRequest())
        {
            if (user.Identity.IsAuthenticated == false)
                response.StatusCode = (int)HttpStatusCode.Unauthorized;
            else
                response.StatusCode = (int)HttpStatusCode.Forbidden;

            response.SuppressFormsAuthenticationRedirect = true;
            response.End();
        }

        base.HandleUnauthorizedRequest(filterContext);
    }
}

Well done. Now we should replace all usings of standard AuthorizeAttribute with this new filter. It may be not applicable for sime guys, who is aesthete of code. But I don’t know any other way. If you have, let’s go to comments, please.

The last, what we should to do – to add HTTP 401/403 handling on a client-side. We can use ajaxError at jQuery to avoid code duplication:

$(document).ajaxError(function (e, xhr) {
    if (xhr.status == 401)
        window.location = "/Account/Login";
    else if (xhr.status == 403)
        alert("You have no enough permissions to request this resource.");
});

The result –

  • If user is not authenticated, then he will be redirected to a login page after any AJAX-call.
  • If user is authenticated, but have no enough permissions, then he will see user-friendly erorr message.
  • If user is authenticated and have enough permissions, the there is no any errors and HTTP-request will be proceeded as usual.
Share:
100,660

Related videos on Youtube

Tim
Author by

Tim

SRE interested in Kubernetes, Service Fabric, c# ..

Updated on September 09, 2020

Comments

  • Tim
    Tim over 3 years

    How do I configure my mvc/webapi project so that a webapi method called from a razor view doesn't return the loginpage when its unauthorised?

    Its a MVC5 application which also has WebApi controllers for calls via javascript.

    The two methods below

    [Route("api/home/LatestProblems")]      
    [HttpGet()]
    public List<vmLatestProblems> LatestProblems()
    {
        // Something here
    }
    
    [Route("api/home/myLatestProblems")]
    [HttpGet()]
    [Authorize(Roles = "Member")]
    public List<vmLatestProblems> mylatestproblems()
    {
       // Something there
    }
    

    are called via the following angular code:

    angular.module('appWorship').controller('latest', 
        ['$scope', '$http', function ($scope,$http) {         
            var urlBase = baseurl + '/api/home/LatestProblems';
            $http.get(urlBase).success(function (data) {
                $scope.data = data;
            }).error(function (data) {
                console.log(data);
            });
            $http.get(baseurl + '/api/home/mylatestproblems')
              .success(function (data) {
                $scope.data2 = data;
            }).error(function (data) {
                console.log(data);
            });  
        }]
    );
    

    So I'm not logged in and the first method successfully returns data. the second method returns (in the success function) data which contains the equivalent of a login page. i.e. what you would get in mvc if you requested a controller action which was stamped with [Authorize] and you weren't logged in.

    I want it to return a 401 unauthorized, so that i can display different data for users based on if they are logged in or not. Ideally if the user is logged in i want to be able to access the Controller's User property so i can return data specific to that Member.

    UPDATE: Since none of the suggestions below seem to work anymore (changes to Identity or WebAPI) ive created a raw example on github which should illustrate the problem.

  • Tim
    Tim over 10 years
    Im I'm using the new identity framework for auth via mvc. Would this setting not prevent the mvc login from working as well as the webapi calls?
  • Tim
    Tim over 10 years
    i think im using System.web.http.authorizeattribute, at least this webapicontroller doesnt have a using for system.web.mvc, and going to definition of the authorize attribute sends me to system.web.http
  • Tim
    Tim over 10 years
    when i checked this example it appears the Authorize Attribute being used is the MVC version rather than the WebApi version. however the webapi version doesnt have options for suppressing forms authentiucation.
  • Tim
    Tim over 10 years
    Hi @kevin-junghans thoroughly confused here. the example above from shiva uses a mvc authorization attribute which surely i shouldnt be applying to a webapi action, The example from Brock allen doesnt appear to work either it doesnt think its a ajax request when i step through.
  • Tim
    Tim over 10 years
    my request doesn't have a IsAjaxRequest method.
  • Tim
    Tim over 10 years
    only just spotted this answer (think stackoverflow not sending notifications) I've added a github example to illustrate the problem, and now added your fix to the angular headers. Thanks. It doesn't seem right however that there isn't a property in the authorize attribute that I can check or the original functionality you mentioned doesn't work anymore.
  • Edward Brey
    Edward Brey over 10 years
    A variation on this: If all your Web API calls go through a certain path, e.g. /api, you can use the path to determine whether to redirect. It's especially useful if you have clients that use other formats like JSON. Replace the call to IsAjaxRequest with if (!context.Request.Path.StartsWithSegments(new PathString("/api"))).
  • Tim
    Tim about 10 years
    Tim look at this for the IsAjaxRequest: brockallen.com/2013/10/27/… If you are using AngularJs without editing the headers you won't have "XMLHttpRequest" and either add it or check for something else.
  • chemitaxis
    chemitaxis almost 10 years
    Using POSTMAN and header param X-Requested-With: XMLHttpRequest works for me... thanks
  • Stephen Collins
    Stephen Collins over 9 years
    Late to the party, but this method is the only one that worke for me, and seems to be more "accurate."
  • L.Trabacchin
    L.Trabacchin about 9 years
    i changed the discriminant to check if the requests accept text/html or application/xhtml as response, if they don't i assume it's an "automated" client requesting, such an ajax request
  • Nick
    Nick almost 9 years
    Even late(r) to the party, but this has proven very useful... it boggles my mind that the default generated code does this so wrong, in such a frustratingly difficult manner to debug.
  • Dunc
    Dunc almost 9 years
    If you're after a WebApi solution, Manik's answer is a good alternative to the highly voted comment here.
  • Derek Greer
    Derek Greer over 8 years
    So, what if you have what you intend to be a pure Web API project doing this? I'm working on a project someone else set up and the Authorize is redirecting as described here, but I have a different API project that works fine. There must be something making this think it's an MVC app rather than an API app, but I can't find what might be junking it up.
  • Kevin Junghans
    Kevin Junghans over 8 years
    @DerekGreer - You will need to provide more details and code for your specific issue in order to get any help. Recommend you create another Question with the details as comments is not a good mechanism for this.
  • Derek Greer
    Derek Greer over 8 years
    My issue ended up being that the web api project was configured with forms auth in IIS.
  • FirstDivision
    FirstDivision about 8 years
    I prefer this approach too. The only addition I made was to convert LocalPath .ToLower() in case they request "/API" or something.
  • Sandun Perera
    Sandun Perera about 8 years
    I'm with the same issue of detecting 401 status code. I'm using ajax calls and expecting JSON results. How can I detect 401 situation in jQuery promise?
  • Peter Örneholm
    Peter Örneholm almost 8 years
    Using C# 6, here is a smaller version of IsAjaxRequest: private static bool IsAjaxRequest(IOwinRequest request) { return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; }
  • Sebastián Rojas
    Sebastián Rojas almost 8 years
    Works, but now I have the problem that client receives status -1 instead of 401
  • Jono Job
    Jono Job almost 8 years
    @SebastiánRojas I'm not sure what would be causing that - setting the SuppressFormsAuthenticationRedirect flag caused it to just return the existing 401 for me.
  • Amit Kumar
    Amit Kumar over 7 years
    Thanks a lot. It saved my day. :)
  • Mark Sowul
    Mark Sowul over 7 years
    For WebAPI, you can look in the comments for IsJsonRequest and integrate that too; you could also use the Accept header
  • Guillaume LaHaye
    Guillaume LaHaye over 6 years
    This is very similar to the solution proposed for a related question: stackoverflow.com/questions/34997674/…
  • Jeremy
    Jeremy over 6 years
    Is anybody having luck with this? CookieAuthenticationOptions no longer has a Provider property as of aspnet core 1.1.
  • Laki Politis
    Laki Politis about 6 years
    My application requests from desktop software to the Web Api. Adding the XMLHttpRequest into my headers saved me HOURS of figuring out why I was getting a 404. I added logic to determine if I needed to add the parameter, and then did so (sorry, this app's in VB): If addXMLHttpRequest Then request.Headers.Add("X-Requested-With", "XMLHttpRequest") End If
  • Jurion
    Jurion over 4 years
    Thank you ! Our APIs worked on every plateform, except Xamarin/Android. Will use this solution
  • Ricardo Saracino
    Ricardo Saracino over 4 years
    All I needed to do was add this filter and its working config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); otherwise User.Identity.IsAuthenticated is always false
  • Koenman
    Koenman over 3 years
    Thank you really much man! Was searching for hour's on how to achieve this.
  • Santosh Karanam
    Santosh Karanam about 3 years
    i thought "X-Requested-With: XMLHttpRequest" is introduced for this specific sccenario. When making ajx calls just add this to your header and server should return proper response. Atleast this is handled in abp.io