Llamada webapi no autorizada que devuelve la página de inicio de sesión en lugar de 401


180

¿Cómo configuro mi proyecto mvc / webapi para que un método webapi llamado desde una vista de afeitar no devuelva la página de inicio de sesión cuando no está autorizado?

Es una aplicación MVC5 que también tiene controladores WebApi para llamadas a través de javascript.

Los dos métodos a continuación

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

se llaman a través del siguiente 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);
        });  
    }]
);

Así que no he iniciado sesión y el primer método devuelve datos con éxito. el segundo método devuelve (en la función de éxito) datos que contienen el equivalente de una página de inicio de sesión. es decir, lo que obtendrías en mvc si solicitaras una acción de controlador que estuviera sellada con [Autorizar] y no estuvieras conectado.

Quiero que devuelva un 401 no autorizado, para que pueda mostrar diferentes datos para los usuarios en función de si están conectados o no. Idealmente, si el usuario ha iniciado sesión, quiero poder acceder a la propiedad de Usuario del Controlador para poder devolver datos específicos de ese Miembro.

ACTUALIZACIÓN: Dado que ninguna de las sugerencias a continuación parecen funcionar (cambios en Identity o WebAPI) he creado un ejemplo en bruto en github que debería ilustrar el problema.

Respuestas:


78

Hay dos implementaciones de AuthorizeAttribute y debe asegurarse de hacer referencia a la correcta para las API web. Existe System.Web.Http.AuthorizeAttribute, que se usa para API web, y System.Web.Mvc.AuthorizeAttribute, que se usa para controladores con vistas. Http.AuthorizeAttribute devolverá un error 401 si la autorización falla y Mvc.AuthorizeAttribute redirigirá a la página de inicio de sesión.

Actualizado 26/11/2013

Así que parece que las cosas han cambiado drásticamente con MVC 5 como señaló Brock Allen en su artículo . Supongo que la tubería OWIN se hace cargo e introduce un nuevo comportamiento. Ahora, cuando el usuario no está autorizado, se devuelve un estado de 200 con la siguiente información en el encabezado HTTP.

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

Puede cambiar su lógica en el lado del cliente para verificar esta información en el encabezado para determinar cómo manejar esto, en lugar de buscar un estado 401 en la rama de error.

Intenté anular este comportamiento en un AuthorizeAttribute personalizado estableciendo el estado en la respuesta en los métodos OnAuthorization y HandleUnauthorizedRequest .

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

Pero esto no funciono. La nueva tubería debe tomar esta respuesta más tarde y modificarla a la misma respuesta que estaba obteniendo antes. Lanzar una HttpException tampoco funcionó, ya que solo se cambió a un estado de 500 errores.

Probé la solución de Brock Allen y funcionó cuando estaba usando una llamada jQuery ajax. Si no te funciona, supongo que es porque estás usando angular. Ejecute su prueba con Fiddler y vea si lo siguiente está en su encabezado.

X-Requested-With: XMLHttpRequest

Si no es así, ese es el problema. No estoy familiarizado con angular, pero si le permite insertar sus propios valores de encabezado, agregue esto a sus solicitudes de ajax y probablemente comenzará a funcionar.


Creo que estoy usando System.web.http.authorizeattribute, al menos este webapicontroller no tiene un uso para system.web.mvc, y al ir a la definición del atributo autorizar me envía a system.web.http
Tim

Hola @ kevin-junghans completamente confundido aquí. el ejemplo anterior de shiva usa un atributo de autorización mvc que seguramente no debería aplicar a una acción webapi. El ejemplo de Brock Allen no parece funcionar o no cree que sea una solicitud ajax cuando paso.
Tim

1
solo descubrí esta respuesta (piense que stackoverflow no envía notificaciones). Agregué un ejemplo de github para ilustrar el problema, y ​​ahora agregué su solución a los encabezados angulares. Gracias. Sin embargo, no parece correcto que no haya una propiedad en el atributo de autorización que pueda verificar o que la funcionalidad original que mencionó ya no funcione.
Tim

3
Uso de POSTMAN y el parámetro de encabezado X-Requested-With: XMLHttpRequest funciona para mí ... gracias
chemitaxis

Entonces, ¿qué pasa si tiene lo que pretende ser un proyecto de API web pura haciendo esto? Estoy trabajando en un proyecto que otra persona configuró y Authorize está redirigiendo como se describe aquí, pero tengo un proyecto API diferente que funciona bien. Debe haber algo que haga pensar que es una aplicación MVC en lugar de una aplicación API, pero no puedo encontrar lo que podría estar desechando.
Derek Greer

123

Brock Allen tiene una buena publicación de blog sobre cómo devolver 401 para llamadas ajax cuando se utiliza la autenticación de cookies y OWIN. http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/

Ponga esto en el método ConfigureAuth en el archivo 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
Una variación de esto: si todas sus llamadas a la API web pasan por una ruta determinada, por ejemplo /api, puede usar la ruta para determinar si se debe redirigir. Es especialmente útil si tiene clientes que usan otros formatos como JSON. Reemplace la llamada a IsAjaxRequestcon if (!context.Request.Path.StartsWithSegments(new PathString("/api"))).
Edward Brey

Tarde a la fiesta, pero este método es el único que me funciona y parece ser más "preciso".
Stephen Collins

Incluso tarde (r) a la fiesta, pero esto ha demostrado ser muy útil ... me sorprende que el código generado por defecto lo haga tan mal, de una manera tan frustrantemente difícil de depurar.
Nick

Si buscas una solución de WebApi, la respuesta de Manik es una buena alternativa al comentario altamente votado aquí.
Dunc

55
Usando C # 6, aquí hay una versión más pequeña de IsAjaxRequest: private static bool IsAjaxRequest(IOwinRequest request) { return request.Query?["X-Requested-With"] == "XMLHttpRequest" || request.Headers?["X-Requested-With"] == "XMLHttpRequest"; }
Peter Örneholm

85

Si está agregando asp.net WebApi dentro del sitio web asp.net MVC, probablemente desee responder sin autorización a algunas solicitudes. Pero luego entra en juego la infraestructura ASP.NET y cuando intentas configurar el código de estado de respuesta en HttpStatusCode. Sin autorización, obtendrás una redirección 302 a la página de inicio de sesión.

Si está utilizando la identidad asp.net y la autenticación basada en Owin aquí, un código que puede ayudar a resolver ese 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
cambié el discriminante para verificar si las solicitudes aceptan texto / html o application / xhtml como respuesta, si no lo hacen, supongo que es un cliente "automatizado" que solicita, como una solicitud ajax
L.Trabacchin

44
Prefiero este enfoque también. La única adición que hice fue convertir LocalPath .ToLower () en caso de que soliciten "/ API" o algo así.
FirstDivision

1
Muchas gracias. Me salvó el día. :)
Amit Kumar

¿Alguien tiene suerte con esto? CookieAuthenticationOptions ya no tiene una propiedad de proveedor a partir de aspnet core 1.1.
Jeremy

27

Tuve la misma situación cuando OWIN siempre redirige la respuesta 401 a la página de inicio de sesión de WebApi. Nuestra API web no solo admite llamadas ajax desde Angular, sino también llamadas de formulario Win móvil. Por lo tanto, la solución para verificar si la solicitud es ajax no está realmente ordenada para nuestro caso.

He optado por otro enfoque es inyectar una nueva respuesta de encabezado: Suppress-Redirect si las respuestas provienen de webApi. La implementación está en el controlador:

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

Y registre este controlador en el nivel global de WebApi:

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

Entonces, en el inicio de OWIN puede verificar si el encabezado de respuesta tiene 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;
}

Gracias ! Nuestras API funcionaron en todas las plataformas, excepto Xamarin / Android. Utilizará esta solución
Jurion

17

En versiones anteriores de ASP.NET, tenía que hacer un montón de cosas para que esto funcionara.

La buena noticia es que está usando ASP.NET 4.5. puede deshabilitar la redirección de autenticación de formularios utilizando la nueva propiedad HttpResponse.SuppressFormsAuthenticationRedirect .

En Global.asax:

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

EDITAR : También es posible que desee echar un vistazo a este artículo de Sergey Zwezdin que tiene una forma más refinada de lograr lo que está tratando de hacer.

Fragmentos de código relevantes y narración del autor pegados a continuación. Autor original del código y la narración - Sergey Zwezdin .

Primero, determinemos si la solicitud HTTP actual es una solicitud AJAX. En caso afirmativo, debemos deshabilitar el reemplazo de HTTP 401 con 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: agreguemos una condición :: si el usuario está autenticado, entonces enviaremos HTTP 403; y HTTP 401 de lo contrario.

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

Bien hecho. Ahora deberíamos reemplazar todos los usos de AuthorizeAttribute estándar con este nuevo filtro. Es posible que no sea aplicable para los chicos de Sime, que es el esteta del código. Pero no sé de otra manera. Si es así, vamos a los comentarios, por favor.

Lo último, lo que debemos hacer: agregar el manejo de HTTP 401/403 en el lado del cliente. Podemos usar ajaxError en jQuery para evitar la duplicación 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.");
});

El resultado -

  • Si el usuario no está autenticado, será redirigido a una página de inicio de sesión después de cualquier llamada AJAX.
  • Si el usuario está autenticado, pero no tiene permisos suficientes, verá un mensaje de error fácil de usar.
  • Si el usuario está autenticado y tiene suficientes permisos, no hay ningún error y la solicitud HTTP se procederá como de costumbre.

Estoy usando el nuevo marco de identidad para la autenticación a través de mvc. ¿Esta configuración no evitaría que el inicio de sesión de mvc funcione tan bien como las llamadas webapi?
Tim

55
Cuando revisé este ejemplo, parece que el atributo Autorizar que se está utilizando es la versión MVC en lugar de la versión WebApi. sin embargo, la versión webapi no tiene opciones para suprimir la autenticación de formularios.
Tim

mi solicitud no tiene un método IsAjaxRequest.
Tim

1
Tim mire esto para IsAjaxRequest: brockallen.com/2013/10/27/… Si está utilizando AngularJs sin editar los encabezados, no tendrá "XMLHttpRequest" y lo agregará o buscará algo más.
Tim

10

Si está ejecutando su Web APIdesde su MVCproyecto, deberá crear una costumbre AuthorizeAttributepara aplicar a sus APImétodos. Dentro de lo IsAuthorized overrideque necesita para tomar la corriente HttpContextpara evitar la redirección, así:

    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 yo mismo la integración de Azure Active Directory, el enfoque usando el CookieAuthenticationmiddleware no funcionó para mí. Tuve que hacer lo siguiente:

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

Si la solicitud proviene del navegador en sí (y no de una llamada AJAX, por ejemplo), el encabezado Aceptar contendrá la cadena html en alguna parte. Solo cuando el cliente acepte HTML consideraré una redirección algo útil.

Mi aplicación cliente puede manejar el 401 informando al usuario que la aplicación no tiene más acceso y necesita volver a cargar para iniciar sesión nuevamente.


Esto es muy similar a la solución propuesta para una pregunta relacionada: stackoverflow.com/questions/34997674/…
Guillaume LaHaye

6

También tenía una aplicación MVC5 (System.Web) con WebApi (usando OWIN) y solo quería evitar que 401 respuestas de WebApi se cambiaran a 302 respuestas.

Lo que funcionó para mí fue crear una versión personalizada de WebApi AuthorizeAttribute como esta:

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

Y para usarlo en lugar del estándar WebApi AuthorizeAttribute. Usé el MVC AuthorizeAttribute estándar para mantener el comportamiento de MVC sin cambios.


Funciona, pero ahora tengo el problema de que el cliente recibe el estado -1 en lugar de 401
Sebastián Rojas

@ SebastiánRojas No estoy seguro de qué podría estar causando eso: al configurar la SuppressFormsAuthenticationRedirect bandera, me devolvió el 401 existente.
Jono Job

3

Simplemente instale el siguiente paquete NeGet

Paquete de instalación Microsoft.AspNet.WebApi.Owin

Escriba el siguiente código en el archivo 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"));
    }
}

Todo lo que tenía que hacer era poner este filtro y su trabajo config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));de otra forma User.Identity.IsAuthenticatedes siemprefalse
Ricardo Saracino

1

si desea capturar Content-Type == application / json puede usar ese 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;

    }

¡¡Saludos!!


1

Estaba teniendo dificultades para conseguir que tanto el código de estado como la respuesta de texto funcionaran en los métodos OnAuthorization / HandleUnauthorizedRequest. Esta resultó ser la mejor solución para mí:

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

1

Después de mucho alboroto tratando de evitar las redirecciones a la página de inicio de sesión, me di cuenta de que esto es realmente muy apropiado para el atributo Autorizar. Está diciendo ir y obtener autorización. En cambio, para las llamadas Api que no están autorizadas, solo quería no revelar ninguna información a los piratas informáticos. Este objetivo fue más fácil de lograr directamente al agregar un nuevo atributo derivado de Autorizar que en su lugar oculta el contenido como un error 404:

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

1

Mezclando MVC y WebAPI, si la solicitud no está autorizada, se redirigirá a la página de inicio de sesión incluso en la solicitud de WebAPI también. Para eso, podemos agregar el siguiente código para enviar una respuesta a la aplicación móvil

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

¡Gracias chicos!

En mi caso, combiné las respuestas de cuongle y Shiva , y obtuve algo como esto:

En el controlador OnException () del controlador para excepciones de API:

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

En el código de configuración de inicio de la aplicación:

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

En MVC 5 con Dot Net Framework 4.5.2 estamos obteniendo "application / json, texto simple .." bajo el encabezado "Aceptar" Será bueno usarlo de la siguiente manera:

isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.