Unauthorised webapi call returning login page rather than 401
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"));
}
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);
}
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.
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;
}
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.