ASP.NET Core provides simple, role-based, and policy-based authorization mechanisms. In this article, I will show you how to create a flexible permission-based authorization system using the policy-based model.
Basic Authorization
Basic authorization can be implemented by applying the AuthorizeAttribute
to a controller or action. [AllowAnonymous]
attribute can be used to allow access to unauthenticated users. It's also possible to specify user roles and policies to evaluate for the authorization to succeed. Code below shows some examples:
[Authorize]
public class SomeController : Controller
{
[Authorize]
public ActionResult ActionOne()
{
// Only authenticated users.
}
[Authorize(Roles = "Administrators")]
public ActionResult ActionTwo()
{
// Only Administrators.
}
[Authorize(Policy = "ContributorsOnly")]
public ActionResult ActionThree()
{
// Only those who meet the requirements of the
// ContributorsOnly policy.
}
[AllowAnonymous]
public ActionResult AnotherAction()
{
// Everyone can access.
}
}
Policy-based Authorization
Policy-based authorization offers a rich model where we can define authorization policies with one or more requirements, and authorization handlers that evaluate these requirements to determine if access is allowed.
You can learn more about authorization in ASP.NET Core from the links given at the end of this article.
Permission-based Authorization
Typically, applications require more than just authenticated users. We would like to have users with different sets of permissions. The easiest way to achieve this is with the role-based authorization where we allow users to perform certain actions depending on their membership in a role.
For small applications, it might be perfectly fine to use role-based authorization, but this has some drawbacks. For instance, it would be difficult to add or remove roles, because we would have to check every AuthorizeAttribute
with role specified in our code whenever we changed roles.
More flexible authorization could be implemented using claims. Instead of checking role membership, we check if a user has a permission to perform a certain action. Permission in this case is represented as a claim.
In order to make it easier to manage claims, we can group them in roles. Latest versions of ASP.NET Core make this possible with role claims.
This solution has the following benefits:
- It allows us to add/remove/delete roles.
- We can re-define a role by changing its permissions.
- Administration UI can be implemented to easily edit roles and permissions.
Let's get started.
Permissions
First, we define permissions for each action grouped by a feature area. In this example, we are defining two feature areas with CRUD permissions. We are using constants because we will use these later in attributes, which require constant expressions.
public static class Permissions
{
public static class Dashboards
{
public const string View = "Permissions.Dashboards.View";
public const string Create = "Permissions.Dashboards.Create";
public const string Edit = "Permissions.Dashboards.Edit";
public const string Delete = "Permissions.Dashboards.Delete";
}
public static class Users
{
public const string View = "Permissions.Users.View";
public const string Create = "Permissions.Users.Create";
public const string Edit = "Permissions.Users.Edit";
public const string Delete = "Permissions.Users.Delete";
}
}
Permissions will be assigned to roles with custom claim type:
public class CustomClaimTypes
{
public const string Permission = "permission";
}
Create roles and assign permissions as desired. For example:
await _roleManager.CreateAsync(new IdentityRole("Administrators"));
var adminRole = await _roleManager.FindByNameAsync("Administrators");
await _roleManager.AddClaimAsync(adminRole, new Claim(CustomClaimTypes.Permission, Permissions.Dashboards.View));
await _roleManager.AddClaimAsync(adminRole, new Claim(CustomClaimTypes.Permission, Permissions.Dashboards.Create));
Also, create one or more users and assign them to roles:
var testUser = new IdentityUser("TestUser");
await _userManager.CreateAsync(testUser);
await _userManager.AddToRoleAsync(testUser, "Administrators");
Permission Requirement
Second, we create a class that will hold the permission to be evaluated.
internal class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; private set; }
public PermissionRequirement(string permission)
{
Permission = permission;
}
}
Permission Authorization Handler
Third, we create an authorization handler that checks whether a user has the required permission, and if so, access is allowed.
internal class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
UserManager<IdentityUser> _userManager;
RoleManager<IdentityRole> _roleManager;
public PermissionAuthorizationHandler(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
{
_userManager = userManager;
_roleManager = roleManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
if (context.User == null)
{
return;
}
// Get all the roles the user belongs to and check if any of the roles has the permission required
// for the authorization to succeed.
var user = await _userManager.GetUserAsync(context.User);
var userRoleNames = await _userManager.GetRolesAsync(user);
var userRoles = _roleManager.Roles.Where(x => userRoleNames.Contains(x.Name));
foreach (var role in userRoles)
{
var roleClaims = await _roleManager.GetClaimsAsync(role);
var permissions = roleClaims.Where(x => x.Type == CustomClaimTypes.Permission &&
x.Value == requirement.Permission &&
x.Issuer == "LOCAL AUTHORITY")
.Select(x => x.Value);
if (permissions.Any())
{
context.Succeed(requirement);
return;
}
}
}
}
Register the handler in Startup.ConfigureServices
:
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
Permission Policies
Next, we need to create a policy for each permission:
services.AddAuthorization(options =>
{
options.AddPolicy(Permissions.Dashboards.View, builder =>
{
builder.AddRequirements(new PermissionRequirement(Permissions.Dashboards.View));
});
options.AddPolicy(Permissions.Dashboards.Create, builder =>
{
builder.AddRequirements(new PermissionRequirement(Permissions.Dashboards.Create));
});
// The rest omitted for brevity.
});
As you can see, we are using the permission as the name for the policy. Each policy has a single requirement that specifies the required permission.
Now, we can use the AuthorizeAttribute
like this:
[Authorize]
public class DashboardsController : Controller
{
[Authorize(Permissions.Dashboards.View)]
public ActionResult View()
{
}
[Authorize(Permissions.Dashboards.Create)]
public ActionResult Create(object params)
{
}
}
When a user tries to access the View()
action, the AuthorizeAttribute
specifies the Permissions.Dashboards.View
policy to be applied. This policy has a single requirement that contains a permission with the same name as the policy. The authorization handler we registered for this type of requirement will authorize access if one of the roles the user belongs to has the specified permission.
Permission Policy Provider
As it stands, the solution works but we can improve further. As we add more permissions, we also need to create policies for those permissions. We can automate this process with IAuthorizationPolicyProvider
. We will create a custom policy provider that will dynamically create a policy with the appropriate requirement as it's needed during runtime.
internal class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
{
// There can only be one policy provider in ASP.NET Core.
// We only handle permissions related policies, for the rest
/// we will use the default provider.
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
// Dynamically creates a policy with a requirement that contains the permission.
// The policy name must match the permission that is needed.
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith("Permissions", StringComparison.OrdinalIgnoreCase))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return Task.FromResult(policy.Build());
}
// Policy is not for permissions, try the default provider.
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
}
Register the policy provider in Startup.ConfigureServices
:
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
We no longer need the policies we defined earlier for the permissions, so we can remove them.
All done! We now have a flexible authorization mechanism where we can manage roles and permissions easily. Feel free to modify the system we have created here according to your needs. For example, you might need multiple permissions instead of only one per action.
After a lot of research regarding policy based authorization on .net core, this post actually met my exact needs. Thanks for the excellent insights.
Hi, Thanks for writing such a nice post in details.
I have implemented it, it works like charm but can you help me how to use it in razor view
[Authorize(Permissions.Users.View)]
what is equivalent to this in razor view ?
Thank you for your kind words. I’m glad you found it helpful.
For Razor view, please take a look at the information below:
View-based authorization in ASP.NET Core MVC
Razor Pages authorization conventions in ASP.NET Core
Thank you so much for this post! This was exactly the information I needed – and it’s written in super understandable way.
I went through a lot of documentations and examples, but they tend to get oversimplified to keep content short.
Now I can go back with confidence and redo all the hacky stuff I wrote to get this to work in my application 🙂
Thank you Elena. Good luck on your projects.