Chamada webapi não autorizada retornando página de login em vez de 401


180

Como configuro meu projeto mvc / webapi para que um método webapi chamado de uma exibição de navalha não retorne a página de login quando ela não é autorizada?

É um aplicativo MVC5 que também possui controladores WebApi para chamadas via javascript.

Os dois métodos abaixo

[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
}

são chamados pelo seguinte código angular:

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

Portanto, não estou logado e o primeiro método retorna dados com êxito. o segundo método retorna (na função de sucesso) dados que contêm o equivalente a uma página de login. ou seja, o que você obteria no mvc se solicitasse uma ação do controlador que estivesse estampada com [Autorizar] e você não estivesse logado.

Quero que ele retorne um 401 não autorizado, para que eu possa exibir dados diferentes para os usuários com base em se eles estão conectados ou não. Idealmente, se o usuário estiver conectado, desejo poder acessar a propriedade User do Controlador para que eu possa retornar dados específicos para esse Membro.

ATUALIZAÇÃO: Como nenhuma das sugestões abaixo parece funcionar mais (alterações em Identity ou WebAPI), criei um exemplo bruto no github que deve ilustrar o problema.

Respostas:


78

Existem duas implementações AuthorizeAttribute e você precisa se referir à correta para APIs da Web. Há System.Web.Http.AuthorizeAttribute que é usado para APIs da Web e System.Web.Mvc.AuthorizeAttribute que é usado para controladores com exibições. Http.AuthorizeAttribute retornará um erro 401 se a autorização falhar e o Mvc.AuthorizeAttribute será redirecionado para a página de logon.

Atualizado 26/11/2013

Parece que as coisas mudaram drasticamente com o MVC 5, como Brock Allen apontou em seu artigo . Eu acho que o pipeline OWIN assume e introduz algum novo comportamento. Agora, quando o usuário não está autorizado, um status 200 é retornado com as seguintes informações no cabeçalho HTTP.

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

Você pode alterar sua lógica no lado do cliente para verificar essas informações no cabeçalho para determinar como lidar com isso, em vez de procurar um status 401 na ramificação de erro.

Eu tentei substituir esse comportamento em um costume AuthorizeAttribute , definindo o status na resposta nos OnAuthorization e HandleUnauthorizedRequest métodos.

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

Mas isso não funcionou. O novo pipeline deve pegar essa resposta mais tarde e modificá-la para a mesma resposta que eu estava recebendo antes. Lançar uma HttpException também não funcionou, pois acabou de mudar para um status de erro 500.

Testei a solução de Brock Allen e funcionou quando estava usando uma chamada de jQuery ajax. Se não está funcionando para você, acho que é porque você está usando angular. Execute seu teste com o Fiddler e veja se o seguinte está no seu cabeçalho.

X-Requested-With: XMLHttpRequest

Se não for, então esse é o problema. Eu não estou familiarizado com angular, mas se ele permitir que você insira seus próprios valores de cabeçalho, adicione-o às suas solicitações de ajax e provavelmente começará a funcionar.


eu acho que estou usando System.web.http.authorizeattribute, pelo menos este webapicontroller não tem um uso para system.web.mvc, e indo para a definição do atributo authorize me envia para system.web.http
Tim

Oi @ kevin-junghans completamente confuso aqui. o exemplo acima do shiva usa um atributo de autorização mvc que certamente não deveria ser aplicado a uma ação de webapi.
Tim

1
apenas encontrei esta resposta (acho que o stackoverflow não envia notificações) Adicionei um exemplo do github para ilustrar o problema e agora adicionei sua correção aos cabeçalhos angulares. Obrigado. No entanto, não parece certo que não haja uma propriedade no atributo de autorização que eu possa verificar ou a funcionalidade original mencionada não funcione mais.
Tim

3
Usando carteiro e param cabeçalho X requerido-Com: graças XMLHttpRequest funciona para mim ...
chemitaxis

Então, e se você tiver o que pretende ser um projeto de API da Web puro fazendo isso? Estou trabalhando em um projeto que outra pessoa configurou e o Authorize está redirecionando conforme descrito aqui, mas tenho um projeto de API diferente que funciona bem. Deve haver algo que faça com que isso ache que é um aplicativo MVC, e não um aplicativo de API, mas não consigo encontrar o que pode estar interferindo nele.
Derek Greer

123

Brock Allen tem um bom post sobre como retornar 401 para chamadas ajax ao usar autenticação de cookie e OWIN. http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

Coloque isso no método ConfigureAuth no arquivo Startup.Auth.cs:

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

68
Uma variação disso: se todas as suas chamadas à API da Web passarem por um determinado caminho, por exemplo /api, você pode usar o caminho para determinar se deve redirecionar. É especialmente útil se você tiver clientes que usam outros formatos como JSON. Substitua a chamada por IsAjaxRequestcom if (!context.Request.Path.StartsWithSegments(new PathString("/api"))).
Edward Brey

Tarde para a festa, mas esse método é o único que funciona para mim e parece ser mais "preciso".
Stephen Collins

Mesmo atrasado (r) para a parte, mas isso provou ser muito útil ... me surpreende que o código gerado padrão faça isso tão errado, de uma maneira tão frustrantemente difícil de depurar.
Nick

Se você procura uma solução WebApi, a resposta de Manik é uma boa alternativa ao comentário altamente votado aqui.
Dunc

5
Usando C # 6, aqui é uma versão menor do IsAjaxRequest: private static bool IsAjaxRequest(IOwinRequest request) { return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; }
Peter Örneholm

85

Se você estiver adicionando WebApi do asp.net no site MVC do asp.net, provavelmente desejará responder não autorizado a algumas solicitações. Mas a infraestrutura do ASP.NET entra em jogo e, quando você tenta definir o código de status da resposta como HttpStatusCode.Unauthorized, você recebe o redirecionamento 302 para a página de login.

Se você estiver usando a identidade asp.net e a autenticação baseada em owin aqui, um código que pode ajudar a resolver esse problema:

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

1
Eu mudei o discriminante para verificar se as solicitações aceitam text / html ou application / xhtml como resposta; caso contrário, presumo que seja um cliente "automatizado" solicitando, tal solicitação ajax
L.Trabacchin

4
Eu prefiro essa abordagem também. A única adição que fiz foi converter LocalPath .ToLower () caso eles solicitassem "/ API" ou algo assim.
FirstDivision

1
Muito obrigado. Isso salvou o meu dia. :)
Amit Kumar

Alguém está tendo sorte com isso? CookieAuthenticationOptions não possui mais uma propriedade Provider a partir do núcleo 1.1 do aspnet.
Jeremy

27

Eu tenho a mesma situação quando o OWIN sempre redireciona a resposta 401 para a página de login da WebApi. Nossa API da Web suporta não apenas chamadas ajax de Angular, mas também chamadas móveis, Win Form. Portanto, a solução para verificar se a solicitação é ajax não está realmente classificada para o nosso caso.

Suppress-RedirectOptei por outra abordagem: injetar uma nova resposta de cabeçalho: se as respostas vierem do webApi. A implementação está no manipulador:

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

E registre esse manipulador no nível global da WebApi:

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

Portanto, na inicialização do OWIN, você pode verificar se o cabeçalho de resposta possui 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;
}

Obrigado ! Nossas APIs funcionavam em todas as formas de chapa, exceto Xamarin / Android. Usará esta solução
Jurion

17

Nas versões anteriores do ASP.NET, você precisava fazer várias coisas para fazer isso funcionar.

A boa notícia é que você está usando o ASP.NET 4.5. você pode desativar o redirecionamento da autenticação de formulários usando o novo propriedade HttpResponse.SuppressFormsAuthenticationRedirect .

Em Global.asax:

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

EDIT : você também pode querer dar uma olhada neste artigo de Sergey Zwezdin, que tem uma maneira mais refinada de realizar o que você está tentando fazer.

Trechos de código relevantes e narração do autor colados abaixo. Autor original do código e narração - Sergey Zwezdin .

Primeiro - vamos determinar se a solicitação HTTP atual é uma solicitação AJAX. Se sim, devemos desativar a substituição do HTTP 401 pelo 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);
    }
}

Segundo - vamos adicionar uma condição :: se o usuário for autenticado, enviaremos HTTP 403; e HTTP 401 caso contrário.

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

Bem feito. Agora devemos substituir todos os usos do AuthorizeAttribute padrão por esse novo filtro. Pode não ser aplicável para sime guys, que é um esteta de código. Mas não conheço outra maneira. Se você tiver, vamos aos comentários, por favor.

O último, o que devemos fazer - adicionar o tratamento HTTP 401/403 no lado do cliente. Podemos usar ajaxError no jQuery para evitar duplicação de código:

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

O resultado -

  • Se o usuário não estiver autenticado, ele será redirecionado para uma página de login após qualquer chamada AJAX.
  • Se o usuário estiver autenticado, mas não tiver permissões suficientes, ele verá uma mensagem de erro amigável.
  • Se o usuário estiver autenticado e tiver permissões suficientes, não haverá erros e a solicitação HTTP será prosseguida normalmente.

Estou usando a nova estrutura de identidade para auth via mvc. Essa configuração não impediria o login do mvc de funcionar, assim como as chamadas da webapi?
Tim

5
Quando verifiquei este exemplo, parece que o Atributo de Autorização que está sendo usado é a versão do MVC e não a versão do WebApi. no entanto, a versão webapi não possui opções para suprimir a autenticação de formulários.
Tim

minha solicitação não possui um método IsAjaxRequest.
Tim

1
Tim olha isso para o IsAjaxRequest: brockallen.com/2013/10/27/… Se você estiver usando AngularJs sem editar os cabeçalhos, não terá "XMLHttpRequest" e o adicionará ou procure por outra coisa.
Tim

10

Se você estiver executando o seu Web APIde dentro do seu MVCprojeto, precisará criar um personalizado AuthorizeAttributepara aplicar aos seus APImétodos. Dentro do arquivo, IsAuthorized overridevocê precisa pegar a corrente HttpContextpara impedir o redirecionamento, assim:

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        if (string.IsNullOrWhiteSpace(Thread.CurrentPrincipal.Identity.Name))
        {
            var response = HttpContext.Current.Response;
            response.SuppressFormsAuthenticationRedirect = true;
            response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
            response.End();
        }

        return base.IsAuthorized(actionContext);
    }

8

Usando a integração do Azure Active Directory, a abordagem do CookieAuthenticationmiddleware não funcionou para mim. Eu tive que fazer o seguinte:

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ...
        Notifications = new OpenIdConnectAuthenticationNotifications
        {   
            ...         
            RedirectToIdentityProvider = async context =>
            {
                if (!context.Request.Accept.Contains("html"))
                {
                    context.HandleResponse();
                }
            },
            ...
        }
    });

Se a solicitação vier do próprio navegador (e não uma chamada AJAX, por exemplo), o cabeçalho Accept conterá a string html em algum lugar. Somente quando o cliente aceitar HTML, considerarei um redirecionamento algo útil.

Meu aplicativo cliente pode lidar com o 401 informando ao usuário que o aplicativo não tem mais acesso e precisa recarregar para fazer login novamente.


Isto é muito semelhante à proposta de solução para uma questão relacionada: stackoverflow.com/questions/34997674/...
Guillaume LaHaye

6

Eu também tinha um aplicativo MVC5 (System.Web) com WebApi (usando OWIN) e só queria impedir que 401 respostas do WebApi fossem alteradas para 302 respostas.

O que funcionou para mim foi criar uma versão personalizada do WebApi AuthorizeAttribute assim:

public class MyAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        base.HandleUnauthorizedRequest(actionContext);
        HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true;
    }
}

E para usá-lo no lugar do WebApi AuthorizeAttribute padrão. Usei o MVC AuthorizeAttribute padrão para manter o comportamento do MVC inalterado.


Funciona, mas agora tenho o problema de que o cliente recebe o status -1 em vez de 401 #
Sebastián Rojas

@ SebastiánRojas Não sei ao certo o que causaria isso - definir a SuppressFormsAuthenticationRedirect bandeira fez com que ela retornasse apenas o 401 existente para mim.
Jono Job

3

Basta instalar seguindo o Pacote NeGet

Pacote de instalação Microsoft.AspNet.WebApi.Owin

Escreva o seguinte código no arquivo WebApiConfig.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Web API configuration and services
        //Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }
}

Tudo o que eu precisava fazer era adicionar este filtro e seu trabalho config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));caso contrário, User.Identity.IsAuthenticatedé semprefalse
Ricardo Saracino

1

se você deseja capturar Content-Type == application / json, pode usar esse código:

private static bool IsAjaxRequest(IOwinRequest request)
    {
        IReadableStringCollection queryXML = request.Query;
        if ((queryXML != null) && (queryXML["X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }

        IReadableStringCollection queryJSON = request.Query;
        if ((queryJSON != null) && (queryJSON["Content-Type"] == "application/json"))
        {
            return true;
        }

        IHeaderDictionary headersXML = request.Headers;
        var isAjax = ((headersXML != null) && (headersXML["X-Requested-With"] == "XMLHttpRequest"));

        IHeaderDictionary headers = request.Headers;
        var isJson = ((headers != null) && (headers["Content-Type"] == "application/json"));

        return isAjax || isJson;

    }

Saudações!!


1

Eu estava com dificuldade para obter o código de status e uma resposta de texto trabalhando nos métodos OnAuthorization / HandleUnauthorizedRequest. Essa acabou sendo a melhor solução para mim:

    actionContext.Response = new HttpResponseMessage()
    {
        StatusCode = HttpStatusCode.Forbidden,
        Content = new StringContent(unauthorizedMessage)
    };

1

Após muita confusão tentando evitar os redirecionamentos para a página de login, percebi que isso é realmente bastante apropriado para o atributo Autorizar. Está dizendo vá e obtenha autorização. Em vez disso, para chamadas da API que não são autorizadas, eu só queria não revelar nenhuma informação a hackers. Esse objetivo foi mais fácil de alcançar diretamente, adicionando um novo atributo derivado de Authorize, que oculta o conteúdo como um erro 404:

public class HideFromAnonymousUsersAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
         actionContext.Response = ActionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "Access Restricted");
    }
}

1

Misturando MVC e WebAPI, se a solicitação não for autorizada, ele será redirecionado para a página de login, mesmo na solicitação WebAPI também. Para isso, podemos adicionar o código abaixo para enviar uma resposta ao aplicativo móvel

protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
    var httpContext = HttpContext.Current;
    if (httpContext == null)
    {
        base.HandleUnauthorizedRequest(actionContext);
        return;
    }

    actionContext.Response = httpContext.User.Identity.IsAuthenticated == false ?
        actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Unauthorized, "Unauthorized") :
       actionContext.Request.CreateErrorResponse(
      System.Net.HttpStatusCode.Forbidden, "Forbidden");

    httpContext.Response.SuppressFormsAuthenticationRedirect = true;
    httpContext.Response.End();
}

0

Obrigado rapazes!

No meu caso, combinei as respostas de cuongle e Shiva e obtive algo parecido com isto:

No manipulador OnException () do controlador para exceções da API:

filterContext.ExceptionHandled = true;
//...
var response = filterContext.HttpContext.Response;
response.Headers.Add("Suppress-Redirect", "true");
response.SuppressFormsAuthenticationRedirect = true;

No código de configuração de inicialização do aplicativo:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider {
            OnValidateIdentity = ctx => {
                return validateFn.Invoke(ctx);
            },
            OnApplyRedirect = ctx =>
            {
                bool enableRedir = true;
                if (ctx.Response != null)
                {
                    string respType = ctx.Response.ContentType;
                    string suppress = ctx.Response.Headers["Suppress-Redirect"];
                    if (respType != null)
                    {
                        Regex rx = new Regex("^application\\/json(;(.*))?$",
                            RegexOptions.IgnoreCase);
                        if (rx.IsMatch(respType))
                        {
                            enableRedir = false;
                        }  
                    }
                    if ((!String.IsNullOrEmpty(suppress)) && (Boolean.Parse(suppress)))
                    {
                        enableRedir = false;
                    }
                }
                if (enableRedir)
                {
                    ctx.Response.Redirect(ctx.RedirectUri);
                }
            }
        }
    });

-1

No MVC 5 com o Dot Net Framework 4.5.2, estamos obtendo "application / json, texto de reclamação .." no cabeçalho "Aceitar" Será bom usar o seguinte:

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.