Authorisation Policy in Identity 3 MVC 6

2019-05-15 14:58发布

问题:

I have done a lot of research but am still not sure if I am doing this correctly. The best resource I found was here

http://leastprivilege.com/2015/10/12/the-state-of-security-in-asp-net-5-and-mvc-6-authorization/

Given an ApplicationUser class extended to include a list of Authorized account numbers I want to restrict the user to only view statements (and other actions based) on their authorized accounts). I would think this is a very common design however most of the articles on the net refer to previous versions of identity.

(PS I am injecting UserManager in the Controller constructor)

Here is my action

public IActionResult GetStatement(int accountNo,DateTime startDate,DateTime endDate)
{
    var user = userManager.Users
        .Include(u => u.AuthorisedAccounts)
        .Where(u => u.Id == User.GetUserId())
        .FirstOrDefault();
    if (user.AuthorisedAccounts != null)
    {
        foreach (var account in user.AuthorisedAccounts)
        {
            if (account.AccountNo == accountNo)
                return View(statementService.GetStatement(accountNo, startDate, endDate, 0));
        }
    }
    return HttpUnauthorized();
}

I cant help feeling there is a better way? Basically I want to authorize based on the action parameter."accountNo"

Any hints on what approach to take.

回答1:

In this case you'd use resource based, with the account being the resource. The documentation for this is at https://docs.asp.net/en/latest/security/authorization/resourcebased.html

To start with you'd define an operation of Read,

public static class Operations
{
    public static OperationAuthorizationRequirement Read =
        new OperationAuthorizationRequirement   { Name = "Read" };
}

Now you'd have a policy for AccountAccess

public class AccountAuthorizationHandler : AuthorizationHandler<
    OperationAuthorizationRequirement, Account>
{
    IUserManager _userManager;

    public AccountAuthorizationHandler(IUserManager userManager)
    {
        _userManager = userManager;
    }

    protected override void Handle(AuthorizationContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Account resource)
    {
        // Pull the user ID claim out from the context.User
        var userId = context.User.....
        // Get the current user's account numbers.       
        var user = userManager.Users
            .Include(u => u.AuthorisedAccounts)
            .Where(u => u.Id == userId)
            .FirstOrDefault();
    }

    // Now check if the user's account numbers match the resource accountNumber, and 
    // also check the operation type, in case you want to vary based on create, view etc.
    if (user.AuthorisedAccounts.Contains(resource.AccountId &&
        requirement.Name == "View")
   {
      context.Succeed(requirement);
   } 
}

After that register your policy in the DI container, within configure services;

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthorization();

    services.AddSingleton<IAuthorizationHandler,
                          AccountAuthorizationHandler>();
}

In your controller you inject the AuthorizationService;

public class AccountController : Controller
{
    IAuthorizationService _authorizationService;

    public AccountController(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }
}

Then, within your controller, after you've loaded the account resource you'd do something like

public async Task<IActionResult> View(int accountId)
{
    Account account = accountManager.Find(accountId);

    if (account == null)
    {
        return new HttpNotFoundResult();
    }

    if (await _authorizationService.AuthorizeAsync(User, account, Operations.Read))
    {
        return View(account);
    }
    else
    {
        return new ChallengeResult();
    }
}


回答2:

If you wanted to define your authorization policy in a way that lets you use it for an [Authorize] attribute, you could alter the approach seen in this article:

services.AddAuthorization(options =>
{
    // inline policies
    options.AddPolicy("AccessByAccountNumber", policy =>
    {
        policy.RequireDelegate((context, requirement) =>
        {
            var httpContext = (context as dynamic).HttpContext;

            // Proceed to grab the account number from the request values
            // and compare it against the user object stored in 'context.User'
        });
    });
});

The downside of this is that you would need to ensure that all of the actions where you use the attribute do so in a consistent way, i.e. using the same name for the action parameter or route parameter every time.

@blowdart's example of using the IAuthorizationService in the controller action demonstrates a way of using and re-using a policy which allows for the parameter names to be decoupled from the policy itself. Though we can see that the IAuthorizationService does not offer a strongly-typed generic method, it would seem to offer a less brittle approach to implementing resource-based policies.