c# - tables - mvc 5 identity authentication
Llamada webapi no autorizada que devuelve la página de inicio de sesión en lugar de 401 (13)
¿Cómo configuro mi proyecto mvc / webapi para que un método webapi llamado desde una vista razor 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);
});
}]
);
Por lo tanto, no estoy conectado 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, qué obtendría en mvc si solicitó una acción de controlador que estaba marcada con [Autorizar] y no ha iniciado sesión.
Quiero que devuelva un 401 no autorizado, de modo que pueda mostrar diferentes datos para los usuarios en función de si están conectados o no. Idealmente, si el usuario está conectado, quiero poder acceder a la propiedad del Usuario del Controlador para que pueda devolver datos específicos a ese Miembro.
ACTUALIZACIÓN: dado que ninguna de las siguientes sugerencias parece funcionar más (cambios a Identity o WebAPI) he creado un ejemplo sin github en github que debería ilustrar el problema.
Brock Allen tiene una buena publicación de blog sobre cómo devolver 401 para llamadas ajax cuando usa autenticación de cookies y OWIN. http://brockallen.com/2013/10/27/using-cookie-authentication-middleware-with-web-api-and-401-response-codes/
Pon 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"));
}
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 bastante apropiado para el atributo Autorizar. Se trata de ir y obtener autorización. En cambio, para las llamadas Api que no están autorizadas, solo quería que no se revelara 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 Authorize que en cambio 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");
}
}
En MVC 5 con Dot Net Framework 4.5.2 estamos obteniendo "application / json, reclaint text .." bajo el encabezado "Accept". Será agradable usar lo siguiente:
isJson = headers["Content-Type"] == "application/json" || headers["Accept"].IndexOf("application/json", System.StringComparison.CurrentCultureIgnoreCase) >= 0;
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ás usando ASP.NET 4.5. puede deshabilitar la redirección de autenticación de formularios con 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 : Quizás también quieras echar un vistazo a este artículo de Sergey Zwezdin, que tiene una forma más refinada de lograr lo que estás tratando de hacer.
Los fragmentos de código relevantes y la narración del autor se pegan a continuación. Autor original de código y narración - Sergey Zwezdin .
Primero, determinemos si la solicitud HTTP actual es solicitud AJAX. En caso afirmativo, deberíamos desactivar la sustitución de HTTP 401 por 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 se autentica, entonces le 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 debemos reemplazar todos los usos de AuthorizeAttribute estándar con este nuevo filtro. Puede no ser aplicable para chicos Sime, que es esteta de código. Pero no sé de otra manera. Si tiene, vamos a los comentarios, por favor.
Lo último, lo que deberíamos 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, se le redirigirá a una página de inicio de sesión después de cualquier llamada AJAX.
- Si el usuario está autenticado, pero no tiene suficientes permisos, verá un mensaje erorr fácil de usar.
- Si el usuario está autenticado y tiene suficientes permisos, no hay ningún error y la solicitud HTTP se realizará de la forma habitual.
Estaba teniendo problemas para obtener el código de estado y una respuesta de texto que funcionan 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)
};
Hay dos implementaciones de AuthorizeAttribute y debe asegurarse de que está haciendo referencia a la correcta para API Web. Existe System.Web.Http.AuthorizeAttribute que se utiliza para las API web y System.Web.Mvc.AuthorizeAttribute que se utiliza para los controladores con vistas. Http.AuthorizeAttribute devolverá un error 401 si la autorización falla y Mvc.AuthorizeAttribute se 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.
Traté de 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 recibía antes. Tampoco funcionó una HttpException, ya que simplemente se cambió a un estado de error de 500.
Probé la solución de Brock Allen y funcionó cuando estaba usando una llamada jQuery ajax. Si no funciona para usted, supongo que es porque está 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 te permite insertar tus propios valores de encabezado, agrégalo a tus solicitudes de Ajax y probablemente empiece a funcionar.
Obtuve 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 admite no solo llamadas ajax desde llamadas angulares, sino también móviles, llamadas a formularios. 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;
}
Si está agregando asp.net WebApi dentro del sitio web asp.net MVC, probablemente quiera responder sin autorización a algunas solicitudes. Pero entonces la infraestructura ASP.NET entra en juego y cuando intentas configurar el código de estado de respuesta a HttpStatusCode.Unauthorized obtendrás 302 redirigir 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);
}
Si está ejecutando su Web API
desde su proyecto MVC
, deberá crear un AuthorizeAttribute
personalizado para aplicar a sus métodos API
. Dentro de la override
IsAuthorized
, debe obtener el HttpContext
actual para evitar la redirección, como esta:
protected override bool IsAuthorized(HttpActionContext actionContext)
{
if (Thread.CurrentPrincipal.Identity.Name.Length == 0)
{
// If using Basic Authentication, do that here
}
if (Thread.CurrentPrincipal.Identity.Name.Length == 0)
{
var response = HttpContext.Current.Response;
response.SuppressFormsAuthenticationRedirect = true;
response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
response.End();
}
return base.IsAuthorized(actionContext);
}
Simplemente instale el siguiente paquete NeGet
Install-Package 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"));
}
}
También tenía una aplicación MVC5 (System.Web) con WebApi (con 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 de esta manera:
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.
si quieres capturar Content-Type == application / json, puedes 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!!
CookieAuthentication
la integración de Azure Active Directory, el enfoque con el middleware CookieAuthentication
no funcionó. Tenía 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 mismo (y no de una llamada AJAX, por ejemplo), el encabezado Aceptar contendrá la cadena html
en algún lugar. Solo cuando el cliente acepte HTML consideraré redirigir 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 de nuevo.