Octubre de 2017
Volumen 32, número 10
Tecnología de vanguardia: autorización basada en directivas en ASP.NET Core
Por Dino Esposito | Octubre de 2017
El nivel de autorización de una aplicación de software garantiza que se permite al usuario actual acceder a un recurso determinado, realizar una operación específica o realizar una operación sobre un recurso concreto. En ASP.NET Core, hay dos maneras de configurar un nivel de autorización. Puede hacerlo mediante roles o directivas. El primer enfoque, la autorización basada en roles, se ha mantenido desde versiones anteriores de la plataforma ASP.NET, mientras que la autorización basada en directivas es una novedad de ASP.NET Core.
El atributo Authorize
Siempre se han usado roles en aplicaciones ASP.NET. Técnicamente, un rol es una cadena sin formato. Sin embargo, el nivel de seguridad lo trata como metainformación (se comprueba la presencia en el objeto IPrincipal) y las aplicaciones lo usan para asignar un conjunto de permisos a un usuario autenticado concreto. En ASP.NET, el usuario que inició sesión se identifica mediante un objeto IPrincipal y, en ASP.NET Core, la clase real es Claims Principal. Esta clase expone una colección de identidades y cada identidad se representa mediante objetos IIdentity; en concreto, objetos Claims Identity. Esto significa que cualquier usuario que inicie sesión tiene una lista de notificaciones, que son, básicamente, instrucciones sobre su estado. El nombre de usuario y el rol son dos notificaciones comunes de los usuarios de aplicaciones ASP.NET Core. Sin embargo, la presencia de roles depende del almacén de identidades auxiliar. Por ejemplo, si usa la autenticación social, nunca verá los roles.
La autorización va un paso más allá de la autenticación. La finalidad de la autenticación es descubrir la identidad de un usuario, mientras que la de la autorización es definir los requisitos para que los usuarios puedan llamar a puntos de conexión de la aplicación. Los roles de usuario se suelen almacenar en la base de datos y se suelen recuperar cuando se validan las credenciales del usuario. En ese momento, la información sobre el rol se conecta de alguna forma a la cuenta de usuario. La interfaz de IIdentity cuenta con un método Is In Role que se debe implementar. Para hacerlo, la clase Claims Identity comprueba que la notificación Role está disponible en la colección de notificaciones resultante del proceso de autenticación. En cualquier caso, cuando el usuario intenta llamar a un método de controlador seguro, su rol debería estar disponible para la comprobación. En caso contrario, no se permite al usuario llamar a ningún método protegido.
El atributo Authorize es la forma declarativa de proteger un controlador o alguno de sus métodos:
[Authorize]
public class CustomerController : Controller
{
...
}
Si se especifica sin argumentos, el atributo solo comprueba que el usuario se ha autenticado. Sin embargo, el atributo admite otros atributos, como Roles. La propiedad Roles indica que se permite el acceso a los usuarios que tengan alguno de los roles indicados. Para requerir varios roles, puede aplicar el atributo Authorize varias veces o escribir su propio filtro.
[Authorize(Roles="admin, system"]
public class BackofficeController : Controller
{
...
}
Si lo prefiere, el atributo Authorize también puede aceptar uno o más esquemas de autenticación a través de la propiedad Active Authentication Schemes.
[Authorize(Roles="admin, system", ActiveAuthenticationSchemes="Cookie"]
public class BackofficeController : Controller
{
...
}
La propiedad Active Authentication Schemes es una cadena separada por comas que enumera los componentes de middleware de autenticación en que confiará el nivel de autorización en el contexto actual. En otras palabras, indica que el acceso a la clase Backoffice Controller solo está permitido si el usuario se autentica mediante el esquema Cookies y tiene alguno de los roles enumerados. Como ya se mencionó, los valores de cadena que se pasan a la propiedad Active Authentication Schemes deben coincidir con el middleware de autenticación registrado al iniciar la aplicación.
Tenga en cuenta que, en ASP.NET 2.0, el middleware de autenticación se sustituye por un servicio con varios controladores. Como resultado, un esquema de autenticación es una etiqueta que selecciona un controlador. Para obtener más información sobre la autenticación en ASP.NET Core, consulte mi columna de septiembre de 2017 "Cookies, Claims and Authentication in ASP.NET Core" (Cookies, notificaciones y autenticación en ASP.NET Core) en msdn.com/magazine/mt842501.
Filtros de autorización
El filtro de autorización que proporciona el sistema consume la información que proporciona el atributo Authorize. Dado que tiene la responsabilidad de comprobar si el usuario puede realizar la operación solicitada, este filtro se ejecuta antes que cualquier otro filtro de ASP.NET Core. Si no se autoriza al usuario, el filtro provoca un cortocircuito en la canalización y cancela la solicitud.
Los filtros de autorización se pueden crear, pero, la mayor parte del tiempo, no es necesario hacerlo. De hecho, es preferible configurar el nivel de autorización existente del que depende el filtro predeterminado.
Roles, permisos y anulaciones
Los roles son una forma sencilla de agrupar a los usuarios de una aplicación según lo que pueden o no pueden hacer. Pero no son muy expresivos. Al menos, no lo suficiente para satisfacer las necesidades de la mayoría de las aplicaciones modernas. Por ejemplo, considere una arquitectura de autorización relativamente sencilla al servicio de los usuarios normales del sitio web y los usuarios avanzados con autorización para acceder al software del área de operaciones, y actualizar el contenido. Un nivel de autorización basado en roles se puede crear alrededor de dos roles (usuario y administrador) que definen los controladores y métodos a que puede acceder cada grupo.
Los problemas aparecen con las distinciones sutiles en las anulaciones que describen lo que los usuarios pueden o no pueden hacer desde un rol determinado. Por ejemplo, puede que tenga usuarios con acceso a sistemas del área de operaciones. De dichos usuarios, algunos tienen autorización para editar datos del cliente; otros, solo para trabajar en el contenido; y otros, para editar datos del cliente y trabajar en el contenido (consulte la Figura 1).
Figura 1 Jerarquía de roles
Los roles son conceptos básicamente sencillos. ¿Cómo simplificaría una jerarquía sencilla como la de la Figura 1? Podría crear cuatro roles distintos: User, Admin, CustomerAdmin y ContentsAdmin. Pero, cuando la cantidad de anulaciones aumenta, la cantidad de roles necesarios también crece significativamente. Incluso un sencillo ejercicio como este muestra que los roles podrían no ser la forma más efectiva de controlar las autorizaciones, excepto en el caso de escenarios sencillos e instancias en que la compatibilidad con versiones anteriores es una prioridad. En cualquier otro caso, hay un requisito distinto. Especifique una autorización basada en directivas.
Pero, ¿qué es una directiva?
En ASP.NET, el marco de autorización basado en directivas se diseñó para separar la lógica de aplicación y la autorización. En otras palabras, una directiva es una entidad concebida como una colección de requisitos que, a su vez, son condiciones que el usuario actual debe cumplir.
La directiva más sencilla es que el usuario se autentica, mientras que un requisito común es que el usuario esté asociado con un rol determinado. Otro requisito común es que el usuario tenga una notificación concreta o una notificación concreta con un valor particular. En términos generales, un requisito es una aserción acerca de la identidad del usuario que intenta acceder a un método que contiene el valor true. Para crear un objeto de directiva, se usa el código siguiente:
var policy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Cookie, Bearer")
.RequireAuthenticatedUser()
.RequireRole("Admin")
.RequireClaim("editor", "contents") .RequireClaim("level", "senior")
.Build();
El objeto builder recopila requisitos mediante una amplia variedad de métodos de extensión y, a continuación, crea la instancia de directiva. Como puede ver, los requisitos actúan sobre los esquemas y el estado de autenticación, el rol y cualquier combinación de notificaciones leídas mediante la cookie de autenticación o el token de portador.
Si ninguno de los métodos de extensión predefinidos para definir los requisitos funciona en su caso, siempre puede recurrir a definir un requisito nuevo mediante su propia aserción. Para hacerlo, siga estos pasos:
var policy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Cookie, Bearer")
.RequireAuthenticatedUser()
.RequireRole("Admin")
.RequireAssertion(ctx =>
{
return ctx.User.HasClaim("editor", "contents") ||
ctx.User.HasClaim("level", "senior");
})
.Build();
El método Require Assertion usa un elemento lambda que recibe el objeto Http Context y devuelve un valor booleano. Por lo tanto, la aserción es, simplemente, una instrucción condicional. Tenga en cuenta que, si concatena el elemento Require Role varias veces, el usuario deberá tener asociados todos los roles. Si prefiere expresar una condición OR, puede recurrir a una aserción. En este ejemplo, de hecho, la directiva permite usuarios que sean editores de contenido o usuarios sénior.
Registro de directivas
Definir las directivas no es suficiente. También debe registrarlas con el middleware de autorización. Para hacerlo, debe agregar el middleware de autorización como servicio en el método Configure Services de la clase inicial, como se indica a continuación:
services.AddAuthorization(options=>
{
options.AddPolicy("ContentsEditor", policy =>
{
policy.AddAuthenticationSchemes("Cookie, Bearer");
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin");
policy.RequireClaim("editor", "contents");
});
}
Cada directiva agregada al middleware tiene un nombre, que se usa para hacer referencia a la directiva desde el atributo Authorize de la clase de controlador:
[Authorize(Policy = "ContentsEditor")]
public IActionResult Save(Article article)
{
// ...
}
El atributo Authorize le permite definir una directiva de modo declarativo, pero las directivas también se pueden invocar mediante programación desde un método de acción, como se muestra en la Figura 2.
Figura 2 Comprobación de directivas mediante programación
public class AdminController : Controller
{
private IAuthorizationService _authorization;
public AdminController(IAuthorizationService authorizationService)
{
_authorization = authorizationService;
}
public async Task<IActionResult> Save(Article article)
{ var allowed = await _authorization.AuthorizeAsync( User, "ContentsEditor"));
if (!allowed)
return new ForbiddenResult();
// Proceed with the method implementation
...
}
}
Si la comprobación de permisos mediante programación no se completa correctamente, puede devolver un objeto Forbidden Result. Otra opción sería devolver un objeto Challenge Result. En ASP.NET Core 1.x, al devolver un desafío, se indica al middleware de autorización que devuelva el código de estado 401 o que redirija al usuario a una página de inicio de sesión, según la configuración. El redireccionamiento no tendrá lugar en ASP.NET Core 2.0. Sin embargo, incluso en ASP.NET Core 1.x, el reto genera un resultado Forbidden Result si el usuario ya inició sesión. Al final, el mejor enfoque consiste en devolver Forbidden Result si la comprobación de permiso no obtiene un resultado correcto.
Tenga en cuenta que puede realizar una comprobación de las directivas mediante programación desde una vista de Razor, como se muestra en el código siguiente:
@{
var authorized = await Authorization.AuthorizeAsync(
User, "ContentsEditor"))}
@if (!authorized)
{
<div class="alert alert-error">
You’re not authorized to access this page.
</div>
}
Sin embargo, para que el código funcione, en primer lugar, debe insertar la dependencia en el servicio de autorización como se indica a continuación:
@inject IAuthorizationService Authorization
Usar el servicio de autorización en una vista puede ayudar a ocultar elementos de la UI que no deberían estar al alcance del usuario actual en un contexto dado. Recuerde que ocultar las opciones de la vista no es suficiente. También es necesario aplicar directivas en el controlador.
Requisitos personalizados
Básicamente, los requisitos de existencias cubren las notificaciones y la autenticación, y proporcionan un mecanismo general para la personalización basado en aserciones. Si lo prefiere, también puede crear requisitos personalizados. Un requisito de directiva está formado por dos elementos: una clase de requisito que contiene solo datos y un controlador de autorización que valida los datos respecto al usuario. Los requisitos personalizados amplían su capacidad de expresar directivas concretas. Si, por ejemplo, quisiera ampliar la directiva Contents Editor agregando el requisito de que el usuario debe tener, al menos, tres años de experiencia, podría hacerlo así:
public class ExperienceRequirement : IAuthorizationRequirement
{
public int Years { get; private set; }
public ExperienceRequirement(int minimumYears)
{
Years = minimumYears;
}
}
Un requisito debe tener al menos un controlador de autorización. Un controlador tiene el tipo Authorization Handler<T>, donde T representa el tipo de requisito. La Figura 3 ilustra un controlador de ejemplo para el tipo Experience Requirement.
Figura 3 Controlador de autorización de ejemplo
public class ExperienceHandler :
AuthorizationHandler<ExperienceRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ExperienceRequirement requirement)
{
// Save User object to access claims
var user = context.User; if (!user.HasClaim(c => c.Type == "EditorSince")) return Task.CompletedTask;
var since = user.FindFirst("EditorSince").Value.ToInt();
if (since >= requirement.Years)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
El controlador de autorización de ejemplo lee las notificaciones asociadas con el usuario y comprueba si existe una notificación Editor Since personalizada. Si no se encuentra, el controlador devuelve un mensaje que lo indica. Solo se devuelve un mensaje que indica que se completó la acción correctamente si existe la notificación y contiene un valor entero no inferior a la cantidad de años especificada.
Se espera que la notificación personalizada sea una información vinculada de alguna forma al usuario (por ejemplo, una columna de la tabla Users) guardado en la cookie de autenticación. Sin embargo, si contiene una referencia al usuario, siempre puede buscar el nombre de usuario a partir de las notificaciones y ejecutar una consulta en cualquier base de datos o servicio externo para obtener los años de experiencia y usar dicha información en el controlador. (Admito que este ejemplo sería más realista si el valor Editor Since tuviera un valor Date Time y se pudiera calcular si ha transcurrido cierta cantidad de años desde que el usuario obtuvo el rol Editor).
Un controlador de autorización invoca el método Succeed y se aprueba el requisito actual para notificar que el requisito se validó correctamente. Si el requisito no se pasó, el controlador no necesita hacer nada y puede volver. Sin embargo, si el controlador quiere determinar el incumplimiento de un requisito, independientemente de que otros controladores para el mismo requisito puedan devolver un estado correcto, invoca el método Fail en el objeto del contexto de autorización.
A continuación se indica cómo se agrega un requisito personalizado a la directiva (recuerde que, dado que se trata de un requisito personalizado, no tiene ningún método de extensión, sino que debe continuar con la colección de requisitos del objeto de directiva):
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast3Years",
policy => policy
.Requirements
.Add(new ExperienceRequirement(3)));
});
Además, debe registrar el nuevo controlador con el sistema de DI dentro del ámbito del tipo IAuthorization Handler:
services.AddSingleton<IAuthorizationHandler, ExperienceHandler>();
Como ya se ha mencionado, un requisito puede tener varios controladores. Cuando se registran varios controladores con el sistema de DI para el mismo requisito del nivel de autorización, basta con que uno de ellos devuelva un estado correcto.
Acceso al contexto HTTP actual
En la implementación del controlador de autorización, puede que necesite inspeccionar las propiedades de la solicitud o los datos de ruta como se indica a continuación:
if (context.Resource is AuthorizationFilterContext mvc)
{
var url = mvc.HttpContext.Request.GetDisplayUrl(); ...
}
En ASP.NET Core, el contexto del controlador de autorización expone una propiedad Resource definida en el objeto de contexto de filtro. El objeto de contexto es distinto en función del marco implicado. Por ejemplo, MVC y SignalR envían su propio objeto específico. Si necesita realizar una transmisión o no depende de a qué quiera acceder. Por ejemplo, la información de usuario siempre está presente, de modo que no es necesaria ninguna transmisión para eso. Sin embargo, si quiere obtener detalles específicos de MVC, como datos de enrutamiento, deberá realizar una transmisión.
Resumen
En ASP.NET Core, la autorización puede tener dos formas. Una es la autorización tradicional basada en roles, que funciona de la misma forma que el clásico patrón MVC de ASP.NET, pero tiene la limitación estructural de ser bastante simple y no resulta ideal para expresar una lógica de autorización sofisticada. La autenticación basada en directivas es un nuevo enfoque que proporciona un modelo más rico y expresivo. Esto se debe a que una directiva es una colección de requisitos basados en notificaciones y lógica personalizada basada en cualquier otra información que se pueda insertar desde el contexto HTTP u otros orígenes externos. Cada uno de estos requisitos está asociado con uno o más controladores, que son los responsables de la evaluación real del requisito.
Dino Esposito es el autor de "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014) y "Modern Web Applications with ASP.NET" (Microsoft Press, 2016). Como experto técnico para las plataformas .NET y Android en JetBrains y orador frecuente en eventos mundiales de la industria, Esposito comparte su visión sobre el software en software2cents@wordpress.com y en su Twitter @despos.
Gracias a los siguientes expertos técnicos por revisar este artículo: Barry Dorrans (Microsoft) y Steve Smith