net mvc example asp asp.net asp.net-mvc security asp.net-web-api

asp.net - example - authorize roles mvc 5



Cómo personalizar la API web ASP.NET AuthorizeAttribute para requisitos inusuales (3)

Estoy heredando de System.Web.Http.AuthorizeAttribute para crear una rutina de autorización / autenticación personalizada que cumpla con algunos requisitos inusuales para una aplicación web desarrollada con ASP.NET MVC 4. Esto agrega seguridad a la API web utilizada para llamadas Ajax desde la web. cliente. Los requisitos son:

  1. El usuario debe iniciar sesión cada vez que realice una transacción para verificar que alguien más no haya caminado hasta la estación de trabajo después de que alguien haya iniciado sesión y se haya marchado.
  2. Los roles no se pueden asignar a los métodos del servicio web en el momento del programa. Deben asignarse en tiempo de ejecución para que un administrador pueda configurar esto. Esta información se almacena en la base de datos del sistema.

El cliente web es una aplicación de una sola página (SPA), por lo que la autenticación típica de formularios no funciona tan bien, pero estoy tratando de reutilizar la mayor parte del marco de seguridad ASP.NET que pueda para cumplir con los requisitos. El atributo personalizado AuthorizeAttribute funciona muy bien para el requisito 2 sobre la determinación de los roles asociados con un método de servicio web. Acepto tres parámetros, nombre de la aplicación, nombre del recurso y operación para determinar qué roles están asociados con un método.

public class DoThisController : ApiController { [Authorize(Application = "MyApp", Resource = "DoThis", Operation = "read")] public string GetData() { return "We did this."; } }

Anulo el método OnAuthorization para obtener los roles y autenticar al usuario. Dado que el usuario debe ser autenticado para cada transacción, reduzco el parloteo hacia adelante y hacia atrás mediante la autenticación y la autorización en el mismo paso. Obtengo las credenciales de los usuarios del cliente web mediante el uso de autenticación básica que pasa las credenciales encriptadas en el encabezado HTTP. Entonces mi método OnAuthorization se ve así:

public override void OnAuthorization(HttpActionContext actionContext) { string username; string password; if (GetUserNameAndPassword(actionContext, out username, out password)) { if (Membership.ValidateUser(username, password)) { FormsAuthentication.SetAuthCookie(username, false); base.Roles = GetResourceOperationRoles(); } else { FormsAuthentication.SignOut(); base.Roles = ""; } } else { FormsAuthentication.SignOut(); base.Roles = ""; } base.OnAuthorization(actionContext); }

GetUserNameAndPassword recupera las credenciales del encabezado HTTP. Luego uso Membership.ValidateUser para validar las credenciales. Tengo un proveedor de membresía personalizado y un proveedor de roles conectado para acceder a una base de datos personalizada. Si el usuario está autenticado, entonces recupero los roles para el recurso y la operación. A partir de ahí, uso la base OnAuthorization para completar el proceso de autorización. Aquí es donde se descompone.

Si el usuario está autenticado, utilizo los métodos de autenticación de formularios estándar para registrar al usuario (FormsAuthentication.SetAuthCookie) y, si fallan, los cierro (FormsAuthentication.SignOut). Pero el problema parece ser que la clase base OnAuthorization no tiene acceso a Principal que se actualiza para que IsAuthenticated se establezca en el valor correcto. Siempre está un paso atrás. Y mi suposición es que está usando algún valor almacenado en caché que no se actualiza hasta que haya un viaje de ida y vuelta al cliente web.

Entonces, todo esto conduce a mi pregunta específica, que es, ¿hay alguna otra manera de establecer IsAuthenticated al valor correcto para el Principal actual sin usar cookies? Me parece que las cookies realmente no se aplican en este escenario específico en el que tengo que autenticar cada vez. El motivo por el que sé que IsAuthenticated no está establecido en el valor correcto es que también anulo el método HandleUnauthorizedRequest para esto:

protected override void HandleUnauthorizedRequest(HttpActionContext filterContext) { if (((System.Web.HttpContext.Current.User).Identity).IsAuthenticated) { filterContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden); } else { base.HandleUnauthorizedRequest(filterContext); } }

Esto me permite devolver un código de estado de Prohibido al cliente web si el error se debió a la autorización en lugar de la autenticación y puede responder en consecuencia.

Entonces, ¿cuál es la forma correcta de establecer IsAuthenticated para el Principio actual en este escenario?


La mejor solución para mi escenario parece ser pasar por alto la base OnAuthorization por completo. Como tengo que autenticar cada vez, las cookies y el almacenamiento en caché del principio no son de mucha utilidad. Así que aquí está la solución que se me ocurrió:

public override void OnAuthorization(HttpActionContext actionContext) { string username; string password; if (GetUserNameAndPassword(actionContext, out username, out password)) { if (Membership.ValidateUser(username, password)) { if (!isUserAuthorized(username)) actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden); } else { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); } } else { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); } }

Desarrollé mi propio método para validar los roles llamados isUserAuthorized y ya no estoy usando la base OnAuthorization ya que verifica el Principio actual para ver si está autenticado . IsAuthenticated solo permite get así que no estoy seguro de cómo configurarlo, y no parece necesitar el Principio actual. Probado esto y funciona bien.

Sigue interesado si alguien tiene una mejor solución o puede ver cualquier problema con este este.


Para agregar a la respuesta absolutamente correcta de Kevin, me gustaría decir que puedo modificarlo ligeramente para aprovechar la ruta existente de .NET Framework para el objeto de respuesta para asegurar que el código de flujo descendente en el marco (u otros consumidores) no se vea afectado negativamente. por alguna extraña idiosincrasia que no puede predecirse.

Específicamente esto significa usar este código:

actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, REQUEST_NOT_AUTHORIZED);

más bien que:

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

Donde REQUEST_NOT_AUTHORIZED es:

private const string REQUEST_NOT_AUTHORIZED = "Authorization has been denied for this request.";

Saqué esa string de la definición SRResources.RequestNotAuthorized en .NET framework.

¡Gran respuesta, Kevin! Implementé el mío de la misma manera porque ejecutar OnAuthorization en la clase base no tenía sentido porque estaba verificando un encabezado HTTP que era personalizado para nuestra aplicación y realmente no quería verificar el Principal porque no había ninguno.


Para agregar a la respuesta ya aceptada: Comprobando el código fuente actual (aspnetwebstack.codeplex.com) para System.Web.Http.AuthorizeAttribute , parece que la documentación está desactualizada. Base OnAuthorization() solo llama / verifica la SkipAuthorization() privada SkipAuthorization() (que simplemente verifica si AllowAnonymousAttribute se usa en contexto para eludir el resto de la verificación de autenticación). Luego, si no se omite, OnAuthorization() llama a IsAuthorized() y si esa llamada falla, llama a la HandleUnauthorizedRequest() virtual protegida HandleUnauthorizedRequest() . Y eso es todo lo que hace ...

public override void OnAuthorization(HttpActionContext actionContext) { if (actionContext == null) { throw Error.ArgumentNull("actionContext"); } if (SkipAuthorization(actionContext)) { return; } if (!IsAuthorized(actionContext)) { HandleUnauthorizedRequest(actionContext); } }

Mirando dentro de IsAuthorized() , ahí es donde se IsAuthorized() el Principio con los roles y los usuarios. Por lo tanto, anular IsAuthorized() con lo que tienes arriba en lugar de OnAuthorization() sería el camino a seguir. Por otra parte, aún tendrá que anular OnAuthorization() o HandleUnauthorizedRequest() para decidir cuándo devolver una respuesta 401 frente a 403.