How to Implement Permission Based Access Control with ASP.NET Core

How to implement permission based authorization in ASP.net core Identity?

I find an approach which is using claim and policy for creating a permission-based authorization in this link.

I create a custom claim type such as Application.Permission and then create some classes as following to define my permissions:

public class CustomClaimTypes
{
public const string Permission = "Application.Permission";
}

public static class UserPermissions
{
public const string Add = "users.add";
public const string Edit = "users.edit";
public const string EditRole = "users.edit.role";
}

and then I create My roles and then Assign these permissions as claims to the roles with key ApplicationPermission.

await roleManager.CreateAsync(new ApplicationRole("User"));
var userRole = await roleManager.FindByNameAsync("User");
await roleManager.AddClaimAsync(userRole, new Claim(CustomClaimTypes.Permission, Permissions.User.View));
await roleManager.AddClaimAsync(userRole, new Claim(CustomClaimTypes.Permission, Permissions.Team.View));

in the next step, I add these claims to my token when the user is trying to login to system:

var roles = await _userManager.GetRolesAsync(user);
var userRoles = roles.Select(r => new Claim(ClaimTypes.Role, r)).ToArray();
var userClaims = await _userManager.GetClaimsAsync(user).ConfigureAwait(false);
var roleClaims = await GetRoleClaimsAsync(roles).ConfigureAwait(false);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.UserName)
}.Union(userClaims).Union(roleClaims).Union(userRoles);

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SigningKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddYears(1),
signingCredentials: creds);

then I create my policies this way:

public static class PolicyTypes
{
public static class Users
{
public const string Manage = "users.manage.policy";
public const string EditRole = "users.edit.role.policy";
}
}

then I set up my authorization service inside startup.cs file in ConfigureServiceSection:

services.AddAuthorization(options =>
{
options.AddPolicy(PolicyTypes.Users.Manage, policy => { policy.RequireClaim(CustomClaimTypes.Permission, Permissions.Users.Add); });
options.AddPolicy(PolicyTypes.Users.EditRole, policy => { policy.RequireClaim(CustomClaimTypes.Permission, Permissions.Users.EditRole); });
}

finally, I set policies on my routes and finish:

[Authorize(Policy = PolicyTypes.Users.Manage)]
public async Task<IEnumerable<TeamDto>> GetSubTeams(int parentId)
{
var teams = await _teamService.GetSubTeamsAsync(parentId);
return teams;
}

How to implement Permission Based Access Control with Asp.Net Core

Based on the comments, here an example on how to use the policy based authorization:

public class PermissionRequirement : IAuthorizationRequirement
{
public PermissionRequirement(PermissionEnum permission)
{
Permission = permission;
}

public PermissionEnum Permission { get; }
}

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IUserPermissionsRepository permissionRepository;

public PermissionHandler(IUserPermissionsRepository permissionRepository)
{
if(permissionRepository == null)
throw new ArgumentNullException(nameof(permissionRepository));

this.permissionRepository = permissionRepository;
}

protected override void Handle(AuthorizationContext context, PermissionRequirement requirement)
{
if(context.User == null)
{
// no user authorizedd. Alternatively call context.Fail() to ensure a failure
// as another handler for this requirement may succeed
return null;
}

bool hasPermission = permissionRepository.CheckPermissionForUser(context.User, requirement.Permission);
if (hasPermission)
{
context.Succeed(requirement);
}
}
}

And register it in your Startup class:

services.AddAuthorization(options =>
{
UserDbContext context = ...;
foreach(var permission in context.Permissions)
{
// assuming .Permission is enum
options.AddPolicy(permission.Permission.ToString(),
policy => policy.Requirements.Add(new PermissionRequirement(permission.Permission)));
}
});

// Register it as scope, because it uses Repository that probably uses dbcontext
services.AddScope<IAuthorizationHandler, PermissionHandler>();

And finally in the controller

[HttpGet]
[Authorize(Policy = PermissionEnum.PERSON_LIST.ToString())]
public ActionResult Index(PersonListQuery query)
{
...
}

The advantage of this solution is that you can also have multiple handlers for a requirement, i.e. if first one succeed the second handler can determine it's a fail and you can use it with resource based authorization with little extra effort.

The policy based approach is the preferred way to do it by the ASP.NET Core team.

From blowdart:

We don't want you writing custom authorize attributes. If you need to do that we've done something wrong. Instead you should be writing authorization requirements.

how to manage user roles and permission in a full stack application made up of angular and .net core web api

The easiest way is to add roles to your claims with a loop. This is a complete method for creating jwt tokens.

    public string GenarateToken(User user)
{
var claims =new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
};

foreach (var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_conf.GetSection("AppSettings:secret").Value));

var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = cred
};

var tokenHandler = new JwtSecurityTokenHandler();

var token = tokenHandler.CreateToken(tokenDescriptor);

return tokenHandler.WriteToken(token);
}

Permission based Authorization for different user roles

I figured few ways to solve the conundrum. One way to do it was through Custom Authorization Policy Providers using IAuthorizationPolicyProvider

In the above method I would have created custom policy attributes like

[MinimumAccessAuthorize("Read","ModuleName1")]

[MinimumAccessAuthorize("Write","ModuleName2")]

on top of my Web API action methods and the IAuthorizationPolicyProvider would have taken care of the rest. The only problem to this approach was that I needed to hard code attributes for Read/Write access available for a module for every god dam action method in every controller here after.

Another approach to this problem was to come up with a custom middleware that authenticated user before the request could hit the controller.

So I created a custom middleware: Resource for custom middleware

Inside my middleware I had a function called invokeAsync

public async Task InvokeAsync(HttpContext context, [FromServices] IRoleService roleService, [FromServices] IPermissionService permissionService)
{
var roles = context.User.Identities.SelectMany(s => s.Claims).Where(s => s.Type.Contains("role")).Select(s => s.Value).FirstOrDefault();

if (roles == null)
{
context.Response.StatusCode = 401;
return;
}

var requestType = context.Request.Method;
var controller = context.Request.Path.Value.Replace("/api/v1/", "").Split('/')[0];

PermissionType typeRequired = requestType == "GET" ? PermissionType.Read : PermissionType.Write;

var (IsSuccessRole, roleResult) = await roleService.GetRoleAsync();
if (!IsSuccessRole)
{
context.Response.StatusCode = 500;
return;
}
int _roleId = roleResult.Where(r => r.RoleName == roles).Select(r => r.Id).FirstOrDefault();

var (IsSuccess, permissionResult) = await permissionService.GetPermissionAsync(_roleId);
if (!IsSuccess)
{
context.Response.StatusCode = 500;
return;
}
var validRoles = from p in permissionResult
join k in GetControllerModuleMaps()
on p.ProjectModule.ModuleName equals k.ModuleName
where k.ControllerName == controller && p.PermissionType >= typeRequired
select p.RoleId;

if (!validRoles.Any())
{
context.Response.StatusCode = 401;
return;
}
await next(context);

}

I am passing the HTTP context and 2 HTTP client services to get the permission and Roles data from the database.

I now check for the role of the user which is present in the jwt token through context and basic linq query.

then basic validation to see if the role is not null.

I check for the request type then to see if it is a GET, PUT or Patch request. Based on the request I store the value as enum in PermissionType.

I store the Controller details which will be hit by REST request in controller variable.

Now using the role service I query the database for the roleId associated with my current jwt token role. I then fetch the permissions associated with that roleID from Database using permission service.

Now based on the Successful query of permission from database, I write a Linq Query syntax where I do a join of the permission result data from database with a list of C# record containing the controller name and the module name as pair values(not key pair value).

Sample list of record looks like this:

    var toRet = new List<ControllerRecord>();

toRet.Add(new ControllerRecord("Controller1", "Module 1"));
toRet.Add(new ControllerRecord("Controller1", "Module 2"));

I check if my record of controllers match with the Request controller path and the permission associated with for that role in the database is greater than what is requested for in the Rest API ( this works because I have enums so NoAccess = 0 < GET request = Only Read = 1 < Put/Patch/Post request = Write = 2 )

If there is any valid role, I let the flow to continue as usual.

That's how I pulled this over...

Using Authorization filters to Implement permission based authorization in asp.net core

The recommend way is to use policy based approach , generate the policies dynamically with a custom AuthorizationPolicyProvider using custom authorization attribute .

From this reply :

We don't want you writing custom authorize attributes. If you need to do that we've done something wrong. Instead you should be writing authorization requirements.

Similar discussion here is also for your reference .

.NET Core Identity API with permission based auth

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.

About authorization in ASP.NET Core

The solution seems to work... but let me raise up a caution a bit...

  • Performance issue

Since you have to check the permission on database for every client request (which will have a real burden to the system). I know it seems like you're building classic mono app. But the server will still suffer from coming back and forth database hard.

  • The user doesn't know what's happening.

Imagine you display a report section that user usually access it frequently, but on some nice day... the browser blank out, or pop-up some dialog that she doesn't have permission to using this anymore. That's could cause real issue cause user only use what they need at the very moment. What'll happen if it's 10 minutes to the meeting and an assistance needed to print out some report and that pop-up ? (from my experienced lesson (XD)).

So I highly suggest that, on app deployemnt and user login, take all their role and claims from database once and cache them somewhere (like IMemoryCache, since we are targeting classic mono app), then check the claim on caches afterward.

Everytime user permission changed, update the cache, and log the user out right at that moment. If something bad happen. User would yelling at the person who setting the permission, not us as developers.

Seems like you have spend a few continuously hours to complete your own decent solution since last time.

Good work mate



Related Topics



Leave a reply



Submit