49

I am trying to implement permission based access control with aspnet core. For dynamically managing user roles and permissions(create_product, delete_product etc.), they are stored in the database. Data Model is like http://i.stack.imgur.com/CHMPE.png

Before aspnet core (in MVC 5) i was using custom AuthorizeAttribute like below to handle the issue:

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    private readonly string _permissionName { get; set; }
    [Inject]
    public IAccessControlService _accessControlService { get; set; }

    public CustomAuthorizeAttribute(string permissionName = "")
    {
        _permissionName = permissionName;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);
        var user = _accessControlService.GetUser();
        if (PermissionName != "" && !user.HasPermission(_permissionName))
        {
            // set error result
            filterContext.HttpContext.Response.StatusCode = 403;
            return;
        }
        filterContext.HttpContext.Items["CUSTOM_USER"] = user;
    }
}

Then i was using it in action method like below:

[HttpGet]
[CustomAuthorize(PermissionEnum.PERSON_LIST)]
public ActionResult Index(PersonListQuery query){ }

Additionally, i was using HttpContext.Items["CUSTOM_USER"] in views to show or hide html part:

@if (CurrentUser.HasPermission("<Permission Name>"))
{

}

When i decided to switch aspnet core, all my plan was failed. Because there was no virtual OnAuthorization method in the AuthorizeAttribute. I tried some ways to solve problem. Those are below:

  • Using new policy based authorization(i think it is not suitable for my scenerio)

  • Using custom AuthorizeAttribute and AuthorizationFilter(i read this post https://stackoverflow.com/a/35863514/5426333 but i couldn’t change it properly)

  • Using custom middleware(how to get AuthorizeAttribute of current action?)

  • Using ActionFilter(is it correct for security purpose?)

I couldn’t decide which way is the best for my scenerio and how to implement it.

First question: Is MVC5 implementation bad practice?

Second question: Do you have any suggest to implement aspnet core?

Community
  • 1
  • 1
adem caglin
  • 17,749
  • 8
  • 44
  • 67
  • Why do you think policy based authorization is not suitable for your case? You can still create `PermissionRequirement` implementing `IAuthorizationRequirement` and a handler, then add it as `options.AddPolicy("PersonList", policy => policy.Requirements.Add(new PermissionRequirement("PersonList")));` – Tseng Apr 06 '16 at 09:02
  • because, i want to get user permissions from database. – adem caglin Apr 06 '16 at 09:10
  • Nothing prevents you from fetching them inside the handler. Just inject your context/repository/service/whatever you need in your requirement handler: http://docs.asp.net/en/latest/security/authorization/dependencyinjection.html – Tseng Apr 06 '16 at 09:12
  • if the application has 500 permissions, do i need to create 500 requirements and handlers? – adem caglin Apr 06 '16 at 09:17
  • 2
    No, it's 1 requirement and 1 handler (though a requirement can be handled by more than 1 handler). You'd just need 500 policy registrations, which you can just add in a loop – Tseng Apr 06 '16 at 09:23
  • wow, that's great solution for me! i never thought this, thanks. – adem caglin Apr 06 '16 at 09:32
  • 1
    I've added a small update to the answer. the `services.AddSingleton();` should be `services.AddScope();` if it uses `DbContext`, because `DbContext` is and should be resolved per request, not for the lifetime of the application – Tseng Apr 06 '16 at 23:17
  • 1
    "You'd just need 500 policy registrations". That seems onerous to me (imagine pre-existing .NET MVC full framework app that is multi-tenant environment with multiple back-end databases that are only accessed when first request comes in for that tenant). Things could be changed to use Policy-based approach, but it seems very roundabout for something that was very simple with custom Authorization attributes in the old world. – Caleb Doise Mar 26 '18 at 20:50
  • I have some problem, can you show me some of code , EX: PermissionEnum. Please! – vankhanhpr Jul 11 '20 at 08:32

3 Answers3

68

Based on the comments, here an example on how to use the policy based authorization:

public class PermissionRequirement : IAuthorizationRequirement
{
    public PermissionRequirement(PermissionEnum permission)
    {
         Permission = permission;
    }

    public PermissionEnum Permission { get; }
}

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    private readonly IUserPermissionsRepository permissionRepository;

    public PermissionHandler(IUserPermissionsRepository permissionRepository)
    {
        if(permissionRepository == null)
            throw new ArgumentNullException(nameof(permissionRepository));

        this.permissionRepository = permissionRepository;
    }

    protected override void Handle(AuthorizationContext context, PermissionRequirement requirement)
    {
        if(context.User == null)
        {
            // no user authorizedd. Alternatively call context.Fail() to ensure a failure 
            // as another handler for this requirement may succeed
            return null;
        }

        bool hasPermission = permissionRepository.CheckPermissionForUser(context.User, requirement.Permission);
        if (hasPermission)
        {
            context.Succeed(requirement);
        }
    }
}

And register it in your Startup class:

services.AddAuthorization(options =>
{
    UserDbContext context = ...;
    foreach(var permission in context.Permissions) 
    {
        // assuming .Permission is enum
        options.AddPolicy(permission.Permission.ToString(),
            policy => policy.Requirements.Add(new PermissionRequirement(permission.Permission)));
    }
});

// Register it as scope, because it uses Repository that probably uses dbcontext
services.AddScope<IAuthorizationHandler, PermissionHandler>();

And finally in the controller

[HttpGet]
[Authorize(Policy = PermissionEnum.PERSON_LIST.ToString())]
public ActionResult Index(PersonListQuery query)
{
    ...
}

The advantage of this solution is that you can also have multiple handlers for a requirement, i.e. if first one succeed the second handler can determine it's a fail and you can use it with resource based authorization with little extra effort.

The policy based approach is the preferred way to do it by the ASP.NET Core team.

From blowdart:

We don't want you writing custom authorize attributes. If you need to do that we've done something wrong. Instead you should be writing authorization requirements.

neizan
  • 2,155
  • 2
  • 31
  • 48
Tseng
  • 52,202
  • 10
  • 166
  • 183
  • 8
    Using asp.net core rc1 I could not use [Authorize(Policy = PermissionEnum.PERSON_LIST.ToString())] beacuse the compiler require to use only "constants" in Attribute declaration. The turn-arround was to use the attribute like this: [Authorize(Policy = nameof(PermissionEnum.PERSON_LIST))] – Alan Araya May 30 '16 at 18:52
  • How would you modify this solution to cope with resource-based rules as well? Eg, I need to check that the user has permission based on an enum value AND some information available only when a controller action is executing? Is that where IAuthorizationService has to be injected? – Sam Jul 12 '16 at 02:00
  • @Sam Wouldn't you just do your additional checks in the `Handle` method? – The Muffin Man Jul 22 '16 at 21:42
  • I can't check information that isn't passed to the Handle method directly. If I need to check additional information that is only scoped at controller action level and I want this to be available to a handler's Handle method as a parameter, I have to invoke IAuthorizationService directly and pass in the additional information as an object so that it gets passed to the handler as well - this can't be done using declarative policies, that's what I was asking about. – Sam Jul 26 '16 at 03:13
  • I like that you used `enum.ToString()`; That saves the type safety. – Mohammed Noureldin Nov 11 '17 at 14:18
  • 1
    @AlanAraya Another very simple solution is to implement your own `MyAuthorizeAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute`. The only code in the class needs to be a constructor like so: public `MyAuthorizeAttribute(PermissionEnum perm) => Policy = perm.ToString();` I hadn't thought of using nameof, but I do like that this way is a little easier to read at a glance. – pbarranis Aug 21 '18 at 13:46
  • How to get dbContext in UserDbContext context = ...; ?? – mzain Mar 02 '19 at 19:26
  • @Sam Don't know about earlier versions, but asp.net core 2.1 provides the Resource property on the AuthorizationHandlerContext argument. It's all abstract classes in the type system, but at runtime you have access to every facet of the request -- route data, http context, etc. You just have to convert the resource to its runtime type to be able to get at it. For example: `(context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).RouteData.` – bvoyelr Apr 15 '19 at 18:15
  • old question but to build on this you can write a custom policy provider that will allow you to avoid using the dbcontext in configureservices, where I believe we should avoid relying on instances. – Craig Mar 27 '20 at 11:59
  • @vankhanhpr: An arbitrary [enum](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/enum) type – Tseng Jul 12 '20 at 10:43
4

I had same requirement and i have done it as below and it works fine for me. I am using .Net Core 2.0 Webapi

[AttributeUsage(AttributeTargets.Class | 
                         AttributeTargets.Method
                       , AllowMultiple = true
                       , Inherited = true)]
public class CheckAccessAttribute : AuthorizeAttribute, IAuthorizationFilter
{
  private string[] _permission;
  public CheckAccessAttribute(params string[] permission)
  {
      _permission = permission;
  }

  public void OnAuthorization(AuthorizationFilterContext context)
  {
     var user = context.HttpContext.User;

     if (!user.Identity.IsAuthenticated)
     {
        return;
     }

     IRepository service = 
     (IRepositoryWrapper)context.HttpContext.RequestServices.GetService(typeof(IRepository));
     var success = service.CheckAccess(userName, _permission.ToList());
     if (!success)
     {
        context.Result = JsonFormatter.GetErrorJsonObject(
                               CommonResource.error_unauthorized,
                               StatusCodeEnum.Forbidden);
        return;
     }
     return;
   }
}

In Controller use it like below

[HttpPost]
[CheckAccess(Permission.CreateGroup)]
public JsonResult POST([FromBody]Group group)
{
   // your code api code here.
}
surfmuggle
  • 4,724
  • 6
  • 38
  • 68
  • 1
    Note that if your `OnAuthorization` implementation needs to await an async method, you should implement `IAsyncAuthorizationFilter` instead of `IAuthorizationFilter` otherwise your filter will execute synchronously and your controller action will execute regardless of the outcome of the filter. – Codemunkie Mar 23 '19 at 20:44
  • What happens in service.checkAccess? – kabuto178 Mar 21 '21 at 01:38
1

For a solution that doesn't require you to add a policy for each permission see my answer for another question.

It lets you decorate your Controllers and Actions with any custom attributes you wish, and access them in your AuthorizationHandler.

Community
  • 1
  • 1
Shawn
  • 661
  • 8
  • 9