Activity based Authorization in ASP.NET Core
We have an asp.net mvc application which I'm porting to aspnet core mvc. In the old solution authentication is done using Windows authentication.
On top of that we have an "activity based authentication" (like http://ryankirkman.com/2013/01/31/activity-based-authorization.html); a user is connected to roles and the roles are connected to rights. The users roles and corresponding rights is stored in a separate application that serves as authorization service for our application and handful of other systems.
A query to the authorization service api for the rights of user "Jon Doe" would get a response like this:
{
Email:"Jon.Doe@acme.com",
FirstName:"Jon",
LastName:"Doe",
Resources:
[
"CanAccessWebApplication",
"CanCopyAppointment",
"CanEditAppointment",
"CanEditContact",
"CanSaveContact"
...
]
Alias:"1234567",
UserId:"1234"
}
In our current application these rights are checked using attributes (that we have implemented our selves) on the controller methods:
public ContactController
{
[ActionUserAccess("CanSaveContact")]
public ActionResult SaveContact
{
...
}
}
The current legacy implementation of the ActionUserAccessAttribute
filter looks like this:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class ActionUserAccessAttribute : ActionFilterAttribute
{
private readonly string _accessRight;
public ActionUserAccessAttribute(string accessRight)
{
_accessRight = accessRight;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
throw new InvalidOperationException("ActionUserAccessAttribute can not be used for controllers or actions configured for anonymous access");
}
base.OnActionExecuting(filterContext);
var securityService = ContainerResolver.Container.GetInstance<ISecurityService>();
var hasResource = securityService.HasAccess(_accessRight);
if (!hasResource)
{
filterContext.Result =
new HttpStatusCodeResult(
403,
string.Format(
"User {0} is not authorized to access the resource:'{1}' ",
filterContext.HttpContext.User.Identity.Name,
_accessRight));
}
}
}
}
Porting the attribute/filter to aspnetcore seems quite straightforward, but according to this answer https://stackoverflow.com/a/31465227/1257728 by "asp.net security person" @blowdart we shouldn't.
If not porting the custom filter to aspnetcore, what would be the best fit to implement here? Maybe we could use the Role based authentication https://docs.microsoft.com/en-us/aspnet/core/security/authorization/roles? We could create a middleware that populates the users access rights from the authorization service api and flatten the rights and add them as ClaimTypes.Role
to the users' ClaimsIdentity
? Then we would use on the method above like:
[Authorize(Roles = "CanSaveContact")]
public ActionResult Save()
The misfit of this approach is that this is not really about roles, but more about the access rights.
I've also looked at the Policy based authorization:
https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies
Which could look like this in the controller:
[Authorize(Policy = "CanSaveContact")]
public ActionResult Save()
But as I read the code in microsoft's policy based example above I would then have to add all available access rights that exists in the security service api as policies in the ConfigureService
method of the Startup
class to be able to use them. I think seems awkward (pseudo code):
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
IEnumerable<string> allAccessRights = _securtiyService.GetAllAccessRights();
services.AddAuthorization(options =>
{
foreach(var accessRight in allAccessRights)
{
options.AddPolicy(accessRight, policy => policy.Requirements.Add(new AccessRightRequirement(accessRight));
}
});
services.AddSingleton<IAuthorizationHandler, AccessRightHandler>();
}
The AccessRightHandler
would then be resposible to validate the access right for the user. Writing an AccessRightHandler is ok, but it seems unnecessary to have to add all the rights as policies.
What would be the best approach to implement this kind of authorization in our aspnetcore application?
Great question, and I think a number of people would have the same problem upgrading to ASP.NET Core.
Barry Dorrans (@blowdart) is absolutely correct, you shouldn't write your own custom authorize attributes - Authorization in ASP.NET Core has been greatly improved, and you can definitely mould it to your needs.
It would of course greatly depend on your current application, and what roles do you have, so I'll make some assumptions based on the snippets you provided above.
Before I start, I REALLY recommend you read through the new Authorization docs for ASP.NET Core, as well as Barry Dorran's Authorization workshop on GitHub. I highly recommend you go through the latter, and he has a .NET Core 2.0 branch there as well.
Depending how you want to implement it, you could either go with Claims based authorization, or go resource based.
Looking at your roles, it seems like Resource based auth could actually work great in your case!
For example:
Identify possible operations (the operation Name
is to be picked up from your Resources):
public static class Operations
{
public static OperationAuthorizationRequirement Access = new OperationAuthorizationRequirement { Name = "Access" };
public static OperationAuthorizationRequirement Copy = new OperationAuthorizationRequirement { Name = "Copy" };
public static OperationAuthorizationRequirement Edit = new OperationAuthorizationRequirement { Name = "Edit" };
public static OperationAuthorizationRequirement Save = new OperationAuthorizationRequirement { Name = "Save" };
public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" };
}
Create a base resource authorization handler:
public abstract class BaseResourceAuthorizationHandler<TResource> : AuthorizationHandler<OperationAuthorizationRequirement, TResource>
{
private readonly string _resourceType;
public BaseResourceAuthorizationHandler(string resourceType)
{
_resourceType = resourceType;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, TResource resource)
{
if (context.User.HasClaim("Resources", $"Can{requirement.Name}{_resourceType}"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Implement specific resource based handlers. The resources are binding objects in your application to entities in your Resources. This class will be the glue between your current resource roles, the Operations, and the authorization system in ASP.NET Core. These can also be extended to add extra logic for any specific resource types/operations
For example, for Appointments:
public class AppointmentAuthorizationHandler : BaseResourceAuthorizationHandler<Appointment>
{
public AppointmentAuthorizationHandler() : base("Appointment") { }
}
Which you then register:
services.AddSingleton<IAuthorizationHandler, AppointmentAuthorizationHandler>();
Then in your controllers:
public class AppointmentsController : Controller
{
IAppointmentsRepository _appointmentsRepository;
IAuthorizationService _authorizationService;
public AppointmentsController(IAppointmentsRepository appointmentsRepository,
IAuthorizationService authorizationService)
{
_appointmentsRepository = appointmentsRepository;
_authorizationService = authorizationService;
}
public IActionResult Edit(int id)
{
var appointment = _appointmentsRepository.Get(id);
if (appointment == null)
{
return new NotFoundResult();
}
if (!(await _authorizationService.AuthorizeAsync(User, appointment, Operations.Edit)))
{
return new ChallengeResult();
}
return View(appointment);
}
}
You can also do the same in views, to check whether the user is allowed to see the Edit button, for example:
@using Microsoft.AspNetCore.Authorization
@model IEnumerable<Appointment>
@inject IAuthorizationService AuthorizationService
<h1>Document Library</h1>
@foreach (var appointment in Model)
{
if (await AuthorizationService.AuthorizeAsync(User, appointment, Operations.Edit))
{
<p>@Html.ActionLink("Appointment #" + appointment.Id, "Edit", new { id = appointment.Id })</p>
}
}
PS Just to add a note - yes, you lose the ability to filter by attributes, but in the end it's better this way. First and foremost - you move away from String based roles, you request permissions based on an operation type and resource type. Secondly, you can handle permissions in a much better (and intelligent way), as well as combine multiple permission checks.
It looks more complex, but it's also MUCH more powerful :)
Going to play the devil's advocate here, and suggest an alternative to my other answer - this could be a simpler option based on @mortb's request, and could fit some people that are migrating from their current systems.
Based on your situation, the Policy based auth really wouldn't fit your usecase - it's a more powerful option, you're not really using any of it, other than checking for the existence of a Resource
string from your API.
On the other hand, I wouldn't discard the Roles approach. The resource list you get from the external API isn't strictly resources, but at the same time it maps quite perfectly to your needs. At the end of the day, all you're trying to do is to check whether the user has one (or more) Resource access permissions for a specific request.
Like you mentioned on your post, you'd have to extend your authorization to populate the roles from your external API. Don't forget that your ClaimsIdentity
has a RoleClaimType
property, which marks the type of the claim used to store the roles. It'll usually be set to ClaimTypes.Role
, but not always.
You could even go ahead, and create custom auth attributes, not unlike this:
public class AuthorizeAccessAttribute : AuthorizeAttribute
{
public AuthorizeAccessAttribute(string entity)
{
Roles = "CanAccess" + entity;
}
}
public class AuthorizeEditAttribute : AuthorizeAttribute
{
public AuthorizeEditAttribute(string entity)
{
Roles = "CanEdit" + entity;
}
}
So you could use it as follows:
[AuthorizeEdit("Appointment")]
public IActionResult Edit(int id)
{
return View();
}
链接地址: http://www.djcxy.com/p/15972.html
上一篇: 如何在代码中动态添加表格到EF4上下文中