10

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?

Nkosi
  • 191,971
  • 29
  • 311
  • 378
mortb
  • 7,874
  • 3
  • 21
  • 38
  • 1
    You don't need quotes around _"asp .net security person"_. [This is him](https://channel9.msdn.com/Events/dotnetConf/2017/T324). He also has an [authorization lab if you're interesting in playing around with it.](https://github.com/blowdart/AspNetAuthorizationWorkshop) – MDMoore313 Nov 18 '17 at 16:56

2 Answers2

9

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>
    }
}

P.S. 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 :)

Artiom Chilaru
  • 10,949
  • 4
  • 39
  • 51
  • Thanks for your well written and thought out reply! – mortb Sep 21 '17 at 08:15
  • I'd like to point out to Microsoft (if they are reading this) that I am a bit disappointed that there is no built in filter that can handle this. I like to have the controller methods as clean as possible, in my scenario authorization is simple and only dependent on however a user has a right or not. I think it would be a great fit for a filter. Maybe there could have been a CustomAuthorizationFilter where T for example could be a string or an enum. It could be wired to some authorization service in the Configure services of the Startup class. (I should maybe put a feature request... :) – mortb Sep 21 '17 at 08:17
  • Well, I think the new system is actually quite a bit more powerful that you give it credit :) After all, you can do things in more complex or simple ways. One of the issues with the old system was that it was WAY too easy to do security in the wrong way, so they are trying to make sure that they'll point you in the right direction, and give you the power to make the security work **your** way, without making it easy to make your app insecure. – Artiom Chilaru Sep 21 '17 at 12:01
  • On that note, I've added an alternative solution, that is much more simple, and maps well to your requirements, while also being concise and easy to move to from your current approach. Based on what you said, that would pretty much achieve what you wanted in the simplest way possible, while keeping it simple and "strict" :) – Artiom Chilaru Sep 21 '17 at 12:02
  • yeah, you know maybe I'm a bit grumpy... :) We really took ourselves time to implement attributes in the old solution and they worked well, but as said it took time to understand should be doing – mortb Sep 21 '17 at 12:04
  • it's worse this way because there's a lot of boilerplate – Konrad Dec 04 '18 at 14:50
  • In this way, all user rights (permissions) needed to be loaded and appended to the claims! What if the user has hundreds or thousands of permissions in a large system? It's not a good idea to store this size of data in a JWT – Majid Dec 10 '20 at 00:35
  • Even if we don't store them in the JWT and adding them dynamically after authenticating, it's not a good idea to load all permissions and then check if one item exists in the list! Why not delegate that checking to the authorization service (or any external storage like Redis) that just return a `true` or `false` as a result? – Majid Dec 10 '20 at 00:39
  • So what solution do you suggest in such scenarios? – Majid Dec 10 '20 at 00:39
1

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();
}
Artiom Chilaru
  • 10,949
  • 4
  • 39
  • 51