How to authorize subset of resource in .Net Core AuthorizationHandler - asp.net-core

I have an implementation of AuthorizationHandler that takes a List<Guid> as its resource. I check each guid to see if the current user has access to this particular resource. My question is, what if I discover they can access some of the guids but not others? Is there some way to reflect this case back to the controller by modifying the following code, or do I need to separately authorize each of the guids in the list?
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyRequirement requirement, List<Guid> resource)
{
Guid myId = Guid.Parse(context.User.Claims.FirstOrDefault(c => c.Type == "SomeClaim").Value);
var allAccessableGuids = GetTheGuids(myId);
foreach(var id in resource)
{
if(allAccessableGuids.FirstOrDefault(u => u.Id == id) == null)
{
return Task.CompletedTask;
}
}
context.Succeed(requirement);
return Task.CompletedTask;
}
The code will currently fail the whole list if one guid is found to be unauthorized.
Note that variable names and methods have been changed for simplicity.

Related

Authorization: How to handle mutiple (dozen or more) requirements

I have a set of tables in our database with users, permissions, and a join that maps which users have what permissions.
Looking at the docs, the following is an example of how policies and the requirement(s) are set up on Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
}
And here is an example of a handler for multiple requirements:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
public class PermissionHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is ReadPermission)
{
if (IsOwner(context.User, context.Resource) ||
IsSponsor(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
else if (requirement is EditPermission ||
requirement is DeletePermission)
{
if (IsOwner(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
}
//TODO: Use the following if targeting a version of
//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
private bool IsOwner(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
private bool IsSponsor(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
}
My intention is to check my database tables within the handler to validate that the user has a setting that corresponds to the policy. To check if a user can upload files, the policy might look like this:
services.AddAuthorization(config =>
{
config.AddPolicy("CanUploadFiles", policy => policy.Requirements.Add(new CanDoRequirement("CanUploadFiles")));
});
Using an [Authorize] attribute for a given policy, I can check that within the handler. I have that much working.
Question: Given that I might have 10-20 separate "CanDo…" permissions in our table, is there a better way to set these up rather than have separate lines in AddAuthorization()?
Well, I'm not aware of any shortcuts when configuring the 20-ish requirements and policies that would remove the separate lines in startup, but you could consider implementing a sort of custom resource based authorization rather than a policy based one, policy-based being a declarative one. Declarative meaning the policy is pre-configured. Like so: [Authorize("policy")].
By using imperative authorization, rather than declarative, you would remove the need for x amount of policies to be configured. Instead of saying "Authorize this method", you let the framework take care of the authorization itself.
Consider the following requirements
A user must be authenticated.
That user can only upload a file if they satisfy the CanUploadFiles which is a boolean on the user's record in the database.
Now consider the following example
You have created your own ICustomAuthorizationHandler, somewhat similar to the the ASP.NET Core's IAuthorizationHandler, with the exception that you won't be satisfying a policy, but instead you will feed it a 'CanDoPermission' and it will return true or false if that user has that specific 'flag'.
public class FileController : Controller
{
private ICustomAuthorizationService _authService
public FileController(ICustomAuthorizationService authService)
{
_authService = authService;
}
[Authorize]
public async Task<IActionResult> Upload(IFormFile file)
{
var authResult = await _authService.AuthorizeAsync(User, "CanDoUpload");
if (!authResult.Succeeded)
{
return new ForbidResult();
}
// Process upload
return View();
}
}
This way, there wouldn't have to be policies nor requirements configured for checking if the user can upload a file. But, you would need to take care of a lot of the stuff that you get for 'free' by simply going for policies and configuring them in AddAuthorization.

ASP.NET Core WEB API Problem with GET parameters in API method

I have this controller
[ApiController]
[Route("api/[controller]")]
public class FamiliesController : ControllerBase
{
readonly FamilyFinanceContext db;
public FamiliesController(FamilyFinanceContext context)
{
db = context;
}
[HttpDelete("deletefamily")]
public async Task<ActionResult<Family>> DeleteFamilly(int id)
{
Family user = db.Families.FirstOrDefault(x => x.Id == id);
if (user == null)
{
return NotFound();
}
db.Families.Remove(user);
await db.SaveChangesAsync();
return Ok(user);
}
}
after call https://localhost:44373/api/families/deletefamily?id=2 i have this error - HTTP ERROR 405
In theory this GET parameters must work. What i done not correctly?
As ESG stated, your trying to do a DELETE, so you need to use the DELETE verb, not the GET verb. You're getting a 405 Method Not Allowed for this reason (you cannot use GET on a DELETE action). You should use a tool like PostMan (https://www.postman.com/) to create your DELETE requests, since you can't really do it easily just in a browser.
To fall more in line with REST convention, you should consider changing your DELETE method slightly:
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteFamily([FromRoute] int id)
{
Family user = db.Families.FirstOrDefault(x => x.Id == id);
if (user == null)
{
return NotFound();
}
db.Families.Remove(user);
await db.SaveChangesAsync();
return NoContent();
}
You would then call this as DELETE https://localhost:44373/api/families/2
By using the [HttpDelete("{id}"] attribute, your moving the id from the query string to the URI, which is more in line with REST convention (the URI represents an object). Query string parameters are more typically used for optional capabilities, such as filtering, sorting, etc and not the endpoint representing the object itself.
Typically DELETE actions do not return content, but that is up to you. If you really want to return the user object, then stick with Ok(user), but NoContent is more typical of the DELETE verb.

Asp.Net Core 2.2 - How to return the Current user ID when using Two AuthorizationSchemes, JWT and cookies

Some times authenticating using Login razor page, and sometimes authenticating using Postman, I would like to get the same userId to use in any kind of controller method in the project.
Here is the static class helper I am using currently:
public static class Utilities
{
public static Guid GetUserId(ClaimsPrincipal user)
{
var userId = user.FindFirst(JwtClaimTypes.Subject)?.Value?.Trim();
return Guid.Parse(userId);
}
public static string[] GetRoles(ClaimsPrincipal identity)
{
return identity.Claims
.Where(c => c.Type == JwtClaimTypes.Role)
.Select(c => c.Value)
.ToArray();
}
}
Placing a breakpoint at GetUserId method above,
when using Cookie scheme, the correct Id is returned, but when using JWT scheme GetUserId gets null value.
I've tested previously other ways that either didn't work for both. So what is the shortest implementation that works for both ?
Edit:
Following #ChrisPratt suggestion, changed GetUserId method as follows:
public static Guid GetUserId(ClaimsPrincipal user)
{
var userId = Guid.Parse(user.FindFirstValue(ClaimTypes.NameIdentifier));
return userId;
}
But when hover the mouse on user -> Identity -> Claims, I get Count = 0
So now as no Claims can be used following this answer, how can I write the above class with User property exposed by ControllerBase ?
Not sure what you've tried, but regardless of auth scheme, the end result should be a ClaimsPrincipal instance on your HttpContext. The way you'd usually get the user id is to simply get the NameIdentifier claim:
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);

ASP.NET Core Entity changing history

I have many controllers like this:
public class EntityController : Controller
{
private readonly IEntityRepository _entity;
public EntityController(IEntityRepository entity)
{
_entity = entity;
}
[Authorize]
[HttpPut("{id}")]
public async ValueTask<IActionResult> Put(int id, [FromBody] Entity entity)
{
if (entity == null || entity.Id != id) return BadRequest();
var updated = await _entity.Update(entity);
if (updated == null) return NotFound();
return Ok(updated);
}
}
I need to implement entities editing (audit) history.
And, since the method is marked as [Authorize], I need to log by which user it was edited.
I'm looking at Audit.NET, but I didn't find a way to do it.
The Audit.NET EF Provider allows to customize the audit entity before saving it. This has to be done at the startup with a so-called AuditEntity Action: an action that is triggered for each entity being modified.
So, you can make this action retrieve the user name from the current HttpContext and store it in a UserName property on your audit entities.
On your asp net startup code, setup a way to obtain the current HttpContext and configure the action to retrieve the username from the context:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Add the HttpContextAccessor if needed.
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Get the service provider to access the http context
var svcProvider = services.BuildServiceProvider();
// Configure Audit.NET
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeNameMapper(typeName => "Audit_" + typeName)
.AuditEntityAction((evt, ent, auditEntity) =>
{
// Get the current HttpContext
var httpContext = svcProvider.GetService<IHttpContextAccessor>().HttpContext;
// Store the identity name on the "UserName" property of the audit entity
((dynamic)auditEntity).UserName = httpContext.User?.Identity.Name;
}));
}
}
This is assuming your audit entities have a common UserName property.
If your Audit Entities already inherits from an interface or base class including the UserName, you can use the generic AuditEntityAction<T> instead.
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeNameMapper(typeName => "Audit_" + typeName)
.AuditEntityAction<IUserName>((evt, ent, auditEntity) =>
{
var httpContext = svcProvider.GetService<IHttpContextAccessor>().HttpContext;
auditEntity.UserName = httpContext.User?.Identity.Name;
}));
To get UserID in IOC :
var userId = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value
how-get-current-user-in-asp-net-core

authorize logged in user against url

My goal is to authorize users only if the current logged on user's customerId matches the customerId on the url/controller.
I'm using ASP.NET Core 2.0 with entity framework. I have extended ApplicationUser with a CustomerId int that has a FK to a CustomerIdentity table.
I am able to retrieve the customerId from the logged in user, using this method here: http://rion.io/2016/01/04/accessing-identity-info-using-dependency-injection-in-net-5/
I have made a routing:
routes.MapRoute(
name: "customers",
template: "{customerId?}/{controller=Home}/{action=Index}/{id?}");
And then in my controllers I have a customerId parameter.
Global admins have the rights to use any customerId they like, but for everyone else they can only use their customerId belonging to their logged in user.
I was thinking of using this approach to check if customerId from url, e.g. /23/Computers/Approve matches currentCustomerId
But I'm not sure how to adapt it to a policy and claim that asp.net core uses.
What I have sofar:
public class CustomerRequirement : IAuthorizationRequirement
{
public bool IsMatchingLoggedInUser { get; private set; }
public CustomerRequirement(bool isMatchingLoggedInUser)
{
IsMatchingLoggedInUser = IsMatchingLoggedInUser;
}
}
Not sure how to make this one:
public class CustomerRequirementHandler : AuthorizationHandler<CustomerRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomerRequirement requirement)
{
//GetDate.CurrentCustomerId is a static int property set after user has logged in
if (GetData.CurrentCustomerId == /*Get httpContext customerId ??? "")*/ 42)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Not sure if I'm totally off here or it can be done differently? I am open for suggestions.
Update (what I ended up with)
public class ValidateCustomerAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (GetData.CurrentCustomerId != (int)context.ActionArguments["customerId"])
{
context.Result = new UnauthorizedResult();
}
}
}
Then on my controllers I decorated it with this attribute. I still need to exclude admins in this, but that will come later. (and some more errorhandling :))
I'm not familiar with how ASP.NET Core does this (is AuthorizationHandler similar to an action filter?), but this seems like something you can handle using roles.
I have a custom Auth action filter where I put custom validation logic like this. In the controller, you could check the roles of the logged in user. If they don't have role "Admin" and their customer ID does not match the customer ID in the URL, return a 403. Otherwise, continue as normal.
If getting the value of the customer ID is a problem for you and you don't have access to the action parameters, then you can use the current request's Uri, split the path apart and pull it out that way. But that's a fragile implementation.