.Net Core Authorization with Customized Requirement/Handler

3.6k Views Asked by At

I asked similar question before and with others help I made some progress, but still not sure what I did wrong or missed here.

My application is kind of simple: Domain users get authenticated. Authenticated user(author) creates a request, save in database. Other authenticated users can view the request only. Author and admin users can edit/delete the request.

This is one of the example I followed: Different API functionality for different roles

And the other one PoliciesAuthApp: https://github.com/aspnet/Docs/tree/master/aspnetcore/security/authorization/policies/samples/PoliciesAuthApp1 I am not sure how the PermissionHandler is used/registered/invoked in here.

Here is my code:

Startup.cs

//  Add Authentication
//  Global filter
services.AddMvc(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .RequireRole("Role - Domain Users")
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});


//  Add Authorization Handlers
services.AddAuthorization(options =>
{
    options.AddPolicy("EditPolicy", policy => policy.Requirements.Add(new EditRequirement()));
});

services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

EditRequirement.cs

public class EditRequirement : IAuthorizationRequirement
{
}

PermissionHandler.cs

public class PermissionHandler : IAuthorizationHandler
{

    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadRequirement)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsAdmin(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditRequirement ||
                     requirement is DeleteRequirement)
            {
                if (IsOwner(context.User, context.Resource) || IsAdmin(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }
        return Task.CompletedTask;
    }

    private bool IsAdmin(ClaimsPrincipal user, object resource)
    {
        if (user.IsInRole("Role - Administrator"))
        {
            return true;
        }
        return false;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        if (resource is CreateRequestViewModel)
        {
            var ctx = (CreateRequestViewModel)resource;

            if (ctx.RequestEnteredBy.Equals(user.Identity.Name))
            {
                return true;
            }
        }
        else if (resource is AuthorizationFilterContext)
        {
            var afc = (AuthorizationFilterContext)resource;

            //  This is not right, but I don't know how to deal with AuthorizationFilterContext 
            //  type passed into resource parameter when I click Edit button trying to edit the request
            if (afc.HttpContext.User.Identity.Name.Equals(user.Identity.Name))
            {
                return true;
            }
        }
        else if (resource is Request)
        {
            var r = (Request)resource;

            if (r.RequestEnteredBy.Equals(user.Identity.Name))
            {
                return true;
            }
        }


        return false;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

RequestsController.cs

private IAuthorizationService _authorizationService;

public RequestsController(ApplicationModelContext context, IAuthorizationService authorizationService)
{
    _context = context; 
    _authorizationService = authorizationService;
}

[Authorize(Policy = "EditPolicy")]
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    CreateRequestViewModel crvm = new CreateRequestViewModel();

    var request = await _context.Request
                .SingleOrDefaultAsync(m => m.RequestId == id);

    if (request == null)
    {
        return NotFound();
    }

    var authorizationResult = await _authorizationService.AuthorizeAsync(User, request, "EditPolicy");

    if (authorizationResult.Succeeded)
    {   
        //  Load request contents and return to the view

        return View(crvm);
    }

    //  This needs to be changed to redirect to a message screen saying no permission
    return RedirectToAction("Details", new { id = request.RequestId });
}

When I was debugging the application, I found that:

  • The first time when the page loads requests from database, it passes a CreateRequestViewModel type object to the resource parameter of IsOwner method in PermissionHandler.
  • After click Edit button of the request item on page, it passes AuthorizationFilterContext type
  • Then goes inside the Edit action in ReuqestController, which sends a Request type(in _authorizationService.AuthorizeAsync).

Not sure if I did something duplicated or completely mixed with different approaches.

Any advise would be really appreciated.

1

There are 1 best solutions below

0
On

It looks like you are mixing policy and resource based authorization. Resource-based sounds like what you want, since you may not want to create policies for each CRUD operation, i.e "CreateUserPolicy", "UpdateUserPolicy", and then pass the different requirements to each one. See this tutorial for resource-based authorization: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-2.2

For authorizing against a user resource, I created a UserAuthorizationHandler:

Startup.cs:

services.AddScoped<IAuthorizationHandler, UserAuthorizationHandler>();

UserAuthorizationHandler.cs

public class UserAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, User>
{
    private readonly IPermissionRepository _permissionRepository;

    public UserAuthorizationHandler(IPermissionRepository permissionRepository)
    {
        _permissionRepository = permissionRepository;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, User resource)
    {
        var authUserId = int.Parse(context.User.FindFirstValue(ClaimTypes.NameIdentifier));

        if (requirement == AuthorizationOperations.Create)
        {
            if (await CanCreate(authUserId))
            {
                context.Succeed(requirement);
            }
        }
        else if (requirement == AuthorizationOperations.Read)
        {
            if (await CanRead(authUserId, resource))
            {
                context.Succeed(requirement);
            }
        }
    }

    /// <summary>
    /// User can create if they have 'create' 'users' permission.
    /// </summary>
    /// <param name="authUserId">The requesting user</param>
    /// <returns></returns>
    public async Task<bool> CanCreate(int authUserId)
    {
        return await _permissionRepository.UserHasPermission(authUserId, "create", "users");
    }

    /// <summary>
    /// User can read if reading themselves or they have the 'read' 'users' permission.
    /// </summary>
    /// <param name="authUserId">The requesting user</param>
    /// <param name="user">The requested resource</param>
    /// <returns></returns>
    public async Task<bool> CanRead(int authUserId, User user)
    {
        return authUserId == user.Id || await _permissionRepository.UserHasPermission(authUserId, "read", "users");
    }
}

Controller:

  var authorized = await _authorizationService.AuthorizeAsync(User, new User { Id = id}, AuthorizationOperations.Read);
  if (!authorized.Succeeded)
  {
    return Unauthorized();
  }