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.
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();
}
}
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.