Authorization: How to handle mutiple (dozen or more) requirements - asp.net-core

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.

Related

Does IdentityServer4 still allow ResourceAuthorize?

I'm looking at upgrading from IdentityServer3 to IdentityServer4, specifically because we're upgrading existing projects from .NET 4.5 to .NET Core 3.1.
The biggest issue I see right now is that we use the ResourceAuthorize attribute to check if the user has permission against a resource
[ResourceAuthorize("Read","urn://someresource")]
But looking through the ID4 documentation and the code base, it doesn't look like ResourceAuthorize exists. The documentation does show examples of using Authorize, but I'm not seeing anything that lets me check for a permission against a resource.
Has the paradigm changed or is there another way to get this type of check done with ID4?
You can add policies:
Startup.cs
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
"SomePolicy",
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(
new SomePolicyRequirement());
});
});
SomePolicyRequirement.cs
public class SomePolicyRequirement : IAuthorizationRequirement
{
public SomePolicyRequirement()
{
}
}
SomePolicyHandler.cs
public class SomePolicyHandler : AuthorizationHandler<SomePolicyRequirement>
{
public SomePolicyHandler()
{
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SomePolicyRequirement requirement)
{
var endpoint = context.Resource as Endpoint;
if (endpoint == null)
{
context.Fail();
return Task.CompletedTask;
}
/*
//RouteData can be controller, action or id
var imageId = filterContext.RouteData.Values["id"].ToString();
if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
{
context.Fail();
return Task.CompletedTask;
}*/
/*
//Repository check can go here
var ownerId = context.User.Claims.FirstOrDefault(c => c.Type == "sub").Value;
if (!_someRepository.IsImageOwner(imageIdAsGuid, ownerId))
{
context.Fail();
return Task.CompletedTask;
}*/
// all checks out
context.Succeed(requirement);
return Task.CompletedTask;
}
}
Dotnet core added some great features for authorization. Resource base authorization can implemented very easy with policy based authorization.
Policy-based authorization

Authorise the localhost in ASP.NET Core

I am newbie in ASP.NET Core, and I have a controller I need to authorise it only on my machine, for the test purposes, however, deny on other...
I have the following config:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.DateFormatString= "yyyy-MM-ddTHH:mm:ssZ";
});
services.AddAuthentication("Cookie")
.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>("Cookie", null);
services.AddLogging(builder => { builder.AddSerilog(dispose: true); });
And on the test controlled I enabled the [Authorise] attrubute
[Authorize]
public class OrderController : Controller
Is there a way to allow my local machine to be autorised to acces the controller's actions? Something like [Authorize(Allow=localhost)]
You can create an action filter like so:
public class LocalhostAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var ip = context.HttpContext.Connection.RemoteIpAddress;
if (!IPAddress.IsLoopback(ip)) {
context.Result = new UnauthorizedResult();
return;
}
base.OnActionExecuting(context);
}
}
And then use the tag Localhost:
//[Authorize]
[Localhost]
public class OrderController : Controller
I believe this will work, restricting the access to the machine where it's executed.
This is more whitelisting than authorization. Authorization means checking whether a user has permission to do something. To do that, the user must be identified first, ie authenticated.
The article Client IP Safelist in the docs shows how you can implement IP safelists through middleware, an action filter or a Razor Pages filter.
App-wide Middleware
The middleware option applies to the entire application. The sample code retrieves the request's endpoint IP, checks it against a list of safe IDs and allows the call to proceed only if it comes from a "safe" list. Otherwise it returns a predetermined error code, in this case 401:
public async Task Invoke(HttpContext context)
{
if (context.Request.Method != "GET")
{
var remoteIp = context.Connection.RemoteIpAddress;
_logger.LogDebug("Request from Remote IP address: {RemoteIp}", remoteIp);
string[] ip = _adminSafeList.Split(';');
var bytes = remoteIp.GetAddressBytes();
var badIp = true;
foreach (var address in ip)
{
var testIp = IPAddress.Parse(address);
if(testIp.GetAddressBytes().SequenceEqual(bytes))
{
badIp = false;
break;
}
}
if(badIp)
{
_logger.LogInformation(
"Forbidden Request from Remote IP address: {RemoteIp}", remoteIp);
context.Response.StatusCode = 401;
return;
}
}
await _next.Invoke(context);
}
The article shows registering it before UseMvc() which means the request will be rejected before reaching the MVC middleware :
app.UseMiddleware<AdminSafeListMiddleware>(Configuration["AdminSafeList"]);
app.UseMvc();
This way we don't waste CPU time routing and processing a request that's going to be rejected anyway. The middleware option is a good choice for implementing a blacklist too.
Action Filter
The filtering code is essentially the same, this time defined in a class derived from ActionFilterAttribute. The filter is defined as a scoped service :
services.AddScoped<ClientIpCheckFilter>();
services.AddMvc(options =>
{
options.Filters.Add
(new ClientIpCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
In this case the request will reach the MVC infrastructure before it's accepted or rejected.
Razor Pages Filter
The code is once more the same, this time deriving from IPageFilter

.NET Core Identity API with permission based auth

I'm new at Identity API but in my web application: Institution users creates other users for own institution and and they want to decide who see this page or not.My controller methods like this ;
[Authorize]
public IActionResult Privacy()
{
return View();
}
But also user's have permissions to do any actions like this enum and enum is bigger than 50;
public enum PermissionTypes
{
UserCreate = 1,
UserEdit = 2,
UserDelete = 3,
....
}
And i do some research and found policy based authorization but when you create a new policy you must declare at Startup.cs and its not good for me because when you do that you always publish new codes in production.What i need is something like that ;
[CustomAuth(PermissionTypes.UserCreate)]
public IActionResult Privacy()
{
return View();
}
Is there any solution for this situation ?
There is many ways to do this. A lot of people recommend claims and policy based security... I personally found this approach a little "stiff".
So instead I do this a little different:
First create a class like this:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Bamboo.Web.CoreWebsite.Membership
{
public class PermissionHandler : AuthorizationHandler<RolesAuthorizationRequirement>
{
private readonly IUserStore<CustomUser> _userStore;
public PermissionHandler(IUserStore<CustomeUser> userStore)
{
_userStore = userStore;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
{
if(context == null || context.User == null)
return;
var userId = context.User.FindFirst(c => string.CompareOrdinal(c.Type, ClaimTypes.NameIdentifier) == 0);//according to msdn this method returns null if not found
if(userId == null)
return;
// for simplicity, I use only one role at a time in the attribute
//but you can use multiple values
var permissions = requirement.AllowedRoles.ToList();
var hasPermissions = //here is your logic to check the database for the actual permissions for this user.
// hasPermissions is just a boolean which is the result of your logic....
if(hasPermissions)
context.Succeed(requirement);//the user met your custom criteria
else
context.Fail();//the user lacks permissions.
}
}
}
Now inject the PermissionHandler in your startup.cs file like this:
public void ConfigureServices(IServiceCollection services)
{
// Custom Identity Services
........
// custom role checks, to check the roles in DB
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
//the rest of your injection logic omitted for brevity.......
}
Now use it in your actions like this:
[Authorize(Roles = PermissionTypes.UserCreate)]
public IActionResult Privacy()
{
return View();
}
Notice I did not create a custom attribute... Like I said there is many ways to do this.
I prefer this way because is less code and there is no hard-coded policies or claims or any other complexities and you can make it 100% data driven.
This is a complex subject so there might be extra tweaks necessary for it work.
Also I use ASP.NET Core 2.2 which might be different than 3.0.
But it should give you a way to do permission based Authorization.
You need to use Roles within your action.
ASP .NET Core Identity Roles

Multiple Access Denied Pages

I'm creating an application that has two different authorization scenarios: admin and site.
If you try to access a /admin route without the policy succeeding the user should be redirected to an access denied page. At this point there's no action the user can take. They can't access the resource and there's nothing for them to do.
If you try to access a /site/joes-super-awesome-site route without the policy suceeding the user should be redirected to a different access denied. At this point the user should be able to request access. There is an action they can take.
What's the best way to achieve this? I know I can override the default OnRedirectToAccessDenied action but that will require some ugly string parsing (untested example below).
.AddCookie(options => {
options.Events.OnRedirectToAccessDenied = context => {
// parsing this kinda sucks.
var pathParts = context.Request.Path.Value.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathParts?[0] == "site") {
context.Response.Redirect($"/{pathParts[0]}/request-access");
} else {
context.Response.Redirect("/account/access-denied");
}
return Task.CompletedTask;
};
})
Doing some searching, I found the following information:
Someone with the same question on this GitHub issue
Tracking of authorization-related improvements in this GitHub issue
Unfortunately these improvements didn't make it to ASP.NET Core 2.1.
It seems that at this point, another option (apart from your suggestion of parsing the request URL) is to imperatively invoke the authorization service in your MVC actions.
It could go from:
// Highly imaginary current implementation
public class ImaginaryController : Controller
{
[HttpGet("site/{siteName}")]
[Authorize("SitePolicy")]
public IActionResult Site(string siteName)
{
return View();
}
[HttpGet("admin")]
[Authorize("AdminPolicy")]
public IActionResult Admin()
{
return View();
}
}
to:
public class ImaginaryController : Controller
{
private readonly IAuthorizationService _authorization;
public ImaginaryController(IAuthorizationService authorization)
{
_authorization = authorization;
}
[HttpGet("site/{siteName}")]
public Task<IActionResult> Site(string siteName)
{
var sitePolicyAuthorizationResult = await _authorization.AuthorizeAsync(User, "SitePolicy");
if (!sitePolicyAuthorizationResult.Success)
{
return Redirect($"/site/{siteName}/request-access");
}
return View();
}
[HttpGet("admin")]
public Task<IActionResult> Admin()
{
var adminPolicyAuthorizationResult = await _authorization.AuthorizeAsync(User, "AdminPolicy");
if (!adminPolicyAuthorizationResult.Success)
{
return Redirect("/account/access-denied");
}
return View();
}
}

IP based authorization policy with Attributes

I'm trying to secure a REST API based on the client IP.
Imagine a blog application with these request examples:
/post/list // Everyone should see the posts
/post/create // Only Authors should create a post
/post/update/42 // Only Authors should update a post
/post/delete/42 // Only Admins should delete a post
/comment/42/list // Everyone should see a post's comments
/comment/42/create // Everyone should create a comment
/comment/42/delete/1337 // Only Admins should delete a comment
IP whitelists defined in appsettings.json:
"IpSecurity": {
"Author": "123.456.789.43,123.456.789.44",
"Admin": "123.456.789.42"
}
Here are action examples with the according RequireRole attributes I'd like to implement:
[HttpGet("post/list")]
public List<Post> List()
// ...
[RequireRole("Author")]
[HttpGet("post/create")]
public StandardResponse Create([FromBody]Post post)
// ...
[RequireRole("Admin")]
[HttpGet("post/delete/{id}")]
public StandardResponse Delete(int id)
// ...
Defined injectable from Startup
var IpSecurity = Configuration.GetSection("IpSecurity");
services.Configure<IpSecurityConfig>(IpSecurity);
Does it sound like a good idea ?
Should I do a custom authorization policy, a middleware and/or a filter for that ?
How would I implement the RequireRole attribute ?
This gives an idea of how to implement an IP whitelist but since a middleware does not have access to the contextual action, I can't use attributes to define my requirements.
Yes, that looks good not least because it looks easy understand at a glance.
One comment I would offer is that using the term "Role" for this might confuse your successors. Call it "MachineRole" instead? (And, for the same reason, don't use the [Authorize(Roles="..."])
Implementation in AspNetCore looks to me a little more complex that it was under MVC4, something like this in the usual methods in Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
//after services.AddMvc() :
services.AddAuthorization(o => { o.AddPolicy(MachineRole.AuthorMachine, p => p.RequireClaim(nameof(MachineRole), MachineRole.AuthorMachine)); });
services.AddAuthorization(o => { o.AddPolicy(MachineRole.AdminMachine, p => p.RequireClaim(nameof(MachineRole), MachineRole.AdminMachine)); });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// ...
app.UseClaimsTransformation( AddMachineRoleClaims );
// app.UseMvc( ... );
// ...etc...
}
public Task<ClaimsPrincipal> AddMachineRoleClaims(ClaimsTransformationContext ctx)
{
var connectionRemoteIpAddress = ctx.Context.Connection.RemoteIpAddress.MapToIPv4();
if (Configuration.GetSection("IpSecurity")["Author"].Contains(connectionRemoteIpAddress.ToString()))
{
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim(nameof(MachineRole), MachineRole.AuthorMachine) }));
}
if (Configuration.GetSection("IpSecurity")["Admin"].Contains(connectionRemoteIpAddress.ToString()))
{
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim( nameof(MachineRole), MachineRole.AdminMachine) }));
}
return Task.FromResult(ctx.Principal);
}
public static class MachineRole
{
public const string AuthorMachine = "AuthorMachine";
public const string AdminMachine = "AdminMachine";
}
and then you can use
[Authorize(Policy = MachineRole.AdminMachine)]
I was sufficiently irritated by the fact that this is not simple, and in particular not a simple as it was in MVC4 that I've done https://github.com/chrisfcarroll/RequireClaimAttributeAspNetCore to make it possible to write:
[RequireClaim("ClaimType",Value = "RequiredValue")]
public IActionResult Action(){}
Assuming you've thought about the implications of IP based authorization -- such that they can be spoofed, and requests make it very deep into your stack before being rejected...
I'd suggest creating a middleware that assigns claims, or at the very least sets the identity (so the user is authenticated). And then use either claims (which you've assigned to the identity in the middleware) or authorization policies (https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies). You could then reject each request based on the IPs associated with a policy:
[Authorize(Policy="AuthorIp")]
[HttpGet("post/create")]
public StandardResponse Create([FromBody]Post post)