c# - mvc - ¿Cómo se crea un AuthorizeAttribute personalizado en ASP.NET Core?
iauthorizationrequirement (7)
¿Cuál es el enfoque actual para hacer un AuthorizeAttribute personalizado?
Fácil: no cree su propio
AuthorizeAttribute
.
Para escenarios de autorización pura (como restringir el acceso solo a usuarios específicos), el enfoque recomendado es usar el nuevo bloque de autorización: https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#L84-L92
public enum PermissionItem
{
User,
Product,
Contact,
Review,
Client
}
public enum PermissionAction
{
Read,
Create,
}
public class AuthorizeAttribute : TypeFilterAttribute
{
public AuthorizeAttribute(PermissionItem item, PermissionAction action)
: base(typeof(AuthorizeActionFilter))
{
Arguments = new object[] { item, action };
}
}
public class AuthorizeActionFilter : IAuthorizationFilter
{
private readonly PermissionItem _item;
private readonly PermissionAction _action;
public AuthorizeActionFilter(PermissionItem item, PermissionAction action)
{
_item = item;
_action = action;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
bool isAuthorized = MumboJumboFunction(context.HttpContext.User, _item, _action); // :)
if (!isAuthorized)
{
context.Result = new ForbidResult();
}
}
}
public class UserController : BaseController
{
private readonly DbContext _context;
public UserController( DbContext context) :
base()
{
_logger = logger;
}
[Authorize(PermissionItem.User, PermissionAction.Read)]
public async Task<IActionResult> Index()
{
return View(await _context.User.ToListAsync());
}
}
Para la autenticación, se maneja mejor en el nivel de middleware.
¿Qué estás tratando de lograr exactamente?
Estoy tratando de hacer un atributo de autorización personalizado en ASP.NET Core.
En versiones anteriores, era posible anular
bool AuthorizeCore(HttpContextBase httpContext)
.
Pero esto ya no existe en
AuthorizeAttribute
.
¿Cuál es el enfoque actual para hacer un AuthorizeAttribute personalizado?
Lo que estoy tratando de lograr: estoy recibiendo una ID de sesión en la Autorización de encabezado. A partir de ese ID sabré si una acción en particular es válida.
Basado en la GRAN respuesta de Derek Greer, lo hice con enumeraciones.
Aquí hay un ejemplo de mi código:
[Permission("AccessCustomers")]
public class CustomersController
{
[Permission("AddCustomer")]
IActionResult AddCustomer([FromBody] Customer customer)
{
//Add customer
}
}
El enfoque recomendado por el equipo de ASP.Net Core es utilizar el nuevo diseño de política que está completamente documentado
here
.
La idea básica detrás del nuevo enfoque es utilizar el nuevo atributo [Autorizar] para designar una "política" (por ejemplo,
[Authorize( Policy = "YouNeedToBe18ToDoThis")]
donde la política se registra en Startup.cs de la aplicación para ejecutar algún bloque de código (es decir, asegurarse de que el usuario tenga un reclamo de edad cuando la edad sea 18 años o más)
El diseño de la política es una gran adición al marco y el equipo de ASP.Net Security Core debe ser elogiado por su introducción.
Dicho esto, no es adecuado para todos los casos.
La desventaja de este enfoque es que no proporciona una solución conveniente para la necesidad más común de simplemente afirmar que un controlador o acción determinada requiere un tipo de reclamo dado.
En el caso de que una aplicación pueda tener cientos de permisos discretos que rigen las operaciones CRUD en recursos REST individuales ("CanCreateOrder", "CanReadOrder", "CanUpdateOrder", "CanDeleteOrder", etc.), el nuevo enfoque requiere repetitivo one-to- una asignación entre un nombre de política y un nombre de reclamo (por ejemplo,
options.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));
), o escribir algún código para realizar estos registros en tiempo de ejecución ( por ejemplo, lea todos los tipos de reclamos de una base de datos y realice la llamada antes mencionada en un bucle. El problema con este enfoque para la mayoría de los casos es que es una sobrecarga innecesaria.
Si bien el equipo de ASP.Net Core Security recomienda nunca crear su propia solución, en algunos casos esta puede ser la opción más prudente para comenzar.
La siguiente es una implementación que utiliza IAuthorizationFilter para proporcionar una manera simple de expresar un requisito de reclamo para un controlador o acción determinada:
public class ClaimRequirementAttribute : TypeFilterAttribute
{
public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
{
Arguments = new object[] {new Claim(claimType, claimValue) };
}
}
public class ClaimRequirementFilter : IAuthorizationFilter
{
readonly Claim _claim;
public ClaimRequirementFilter(Claim claim)
{
_claim = claim;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
if (!hasClaim)
{
context.Result = new ForbidResult();
}
}
}
[Route("api/resource")]
public class MyController : Controller
{
[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
[HttpGet]
public IActionResult GetResource()
{
return Ok();
}
}
Parece que con ASP.NET Core 2, puede heredar nuevamente
AuthorizeAttribute
, solo necesita implementar
IAuthorizationFilter
(o
IAsyncAuthorizationFilter
):
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
private readonly string _someFilterParameter;
public CustomAuthorizeAttribute(string someFilterParameter)
{
_someFilterParameter = someFilterParameter;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
// it isn''t needed to set unauthorized result
// as the base class already requires the user to be authenticated
// this also makes redirect to a login page work properly
// context.Result = new UnauthorizedResult();
return;
}
// you can also use registered services
var someService = context.HttpContext.RequestServices.GetService<ISomeService>();
var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
if (!isAuthorized)
{
context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
return;
}
}
}
Puede crear su propio AuthorizationHandler que encontrará atributos personalizados en sus Controladores y Acciones, y pasarlos al método HandleRequirementAsync.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AuthorizationOptions>(options =>
{
options.AddPolicy("ManageStore", policy => policy.RequireClaim("Action", "ManageStore"));
});
}
}
public class StoreController : Controller
{
[Authorize(Policy = "ManageStore"), HttpGet]
public async Task<IActionResult> Manage() { ... }
}
Luego puede usarlo para cualquier atributo personalizado que necesite en sus controladores o acciones. Por ejemplo, para agregar requisitos de permisos. Simplemente cree su atributo personalizado.
public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
{
var attributes = new List<TAttribute>();
var action = (context.Resource as AuthorizationFilterContext)?.ActionDescriptor as ControllerActionDescriptor;
if (action != null)
{
attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
attributes.AddRange(GetAttributes(action.MethodInfo));
}
return HandleRequirementAsync(context, requirement, attributes);
}
protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);
private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
{
return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
}
}
Luego cree un Requisito para agregar a su Política
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : AuthorizeAttribute
{
public string Name { get; }
public PermissionAttribute(string name) : base("Permission")
{
Name = name;
}
}
Luego cree el AuthorizationHandler para su atributo personalizado, heredando el AttributeAuthorizationHandler que creamos anteriormente. Se pasará un IEnumerable para todos sus atributos personalizados en el método HandleRequirementsAsync, acumulado desde su Controlador y Acción.
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
//Add any custom requirement properties if you have them
}
Y, finalmente, en su método Startup.cs ConfigureServices, agregue su AuthorizationHandler personalizado a los servicios y agregue su Política.
public class PermissionAuthorizationHandler : AttributeAuthorizationHandler<PermissionAuthorizationRequirement, PermissionAttribute>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement, IEnumerable<PermissionAttribute> attributes)
{
foreach (var permissionAttribute in attributes)
{
if (!await AuthorizeAsync(context.User, permissionAttribute.Name))
{
return;
}
}
context.Succeed(requirement);
}
private Task<bool> AuthorizeAsync(ClaimsPrincipal user, string permission)
{
//Implement your custom user permission logic here
}
}
Ahora puede simplemente decorar sus Controladores y Acciones con su atributo personalizado.
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("Permission", policyBuilder =>
{
policyBuilder.Requirements.Add(new PermissionAuthorizationRequirement());
});
});
Si alguien solo quiere validar un token de portador en la fase de autorización utilizando las prácticas de seguridad actuales que puede,
agregue esto a su Startup / ConfigureServices
services.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(); services.AddAuthorization(options => options.AddPolicy("Bearer", policy => policy.AddRequirements(new BearerRequirement()) ) );
y esto en tu base de código,
public class BearerRequirement : IAuthorizationRequirement { public async Task<bool> IsTokenValid(SomeValidationContext context, string token) { // here you can check if the token received is valid return true; } } public class BearerAuthorizationHandler : AuthorizationHandler<BearerRequirement> { public BearerAuthorizationHandler(SomeValidationContext thatYouCanInject) { ... } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BearerRequirement requirement) { var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource; string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"]; if (authHeader != null && authHeader.Contains("Bearer")) { var token = authHeader.Replace("Bearer ", string.Empty); if (await requirement.IsTokenValid(thatYouCanInject, token)) { context.Succeed(requirement); } } } }
Si el código no alcanza el
context.Succeed(...)
tiene
context.Succeed(...)
fallará de todos modos (401).
Y luego en tus controladores puedes usar
[Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Soy la persona de seguridad de asp.net.
En primer lugar, me disculpo porque nada de esto está documentado aún fuera de la muestra de la tienda de música o las pruebas unitarias, y todo se está refinando en términos de API expuestas.
La documentación detallada está
here
.
No queremos que escriba atributos de autorización personalizados. Si necesita hacer eso, hemos hecho algo mal. En cambio, debe escribir los requisitos de autorización.
La autorización actúa sobre las identidades. Las identidades se crean por autenticación.
Usted dice en los comentarios que desea verificar una ID de sesión en un encabezado.
Su ID de sesión sería la base de la identidad.
Si desea utilizar el atributo
Authorize
, escribiría un middleware de autenticación para tomar ese encabezado y convertirlo en un
ClaimsPrincipal
autenticado.
Luego verificaría eso dentro de un requisito de autorización.
Los requisitos de autorización pueden ser tan complicados como desee, por ejemplo, aquí hay uno que toma una fecha de nacimiento de la identidad actual y autorizará si el usuario es mayor de 18 años;
public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
{
context.Fail();
return;
}
var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
int age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-age))
{
age--;
}
if (age >= 18)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
}
Luego, en su función
ConfigureServices()
, lo conectaría
services.AddAuthorization(options =>
{
options.AddPolicy("Over18",
policy => policy.Requirements.Add(new Authorization.Over18Requirement()));
});
Y finalmente, aplíquelo a un controlador o método de acción con
[Authorize(Policy = "Over18")]