Permission-based access control with .NET

Permission-based access control can be used just as easily in .NET as role-based access control. To enable this, we need to replace one of the build-in services and create an extension method. Below is example code for .NET how to implement permission-based access control.

To demonstrate how this can be used, we have defined a single functionality for an Example application, which is “Manage Order,” and we have given it the authorization name “Example.ManageOrder” in Authway. For this, we define the following additions in the Constants class:

public static class Constants
{
    public static class Permissions
    {
        public const string ManageOrder = "Example.ManageOrder";
    }
}

Now we need to expand/change which scopes are to be retrieved from the IdP and simultaneously define claim actions in the AddOpenIdConnect configuration:

options.Scope.Add("perms"); //Authway permissions
options.ClaimActions.MapJsonKey("perm", "perm"); //Authway permission claim

To make it easy to check if a user has authorization for a function in the system, for example, the following extension method can be created:

public static class PrincipalExtensions
{
    public static bool Authorize(this ClaimsPrincipal principal, string permission)
    {
        return principal.Identity.IsAuthenticated && principal.HasClaim(c => c.Type == "perm" && c.Value == permission);
    }
}

With the extension method in place we can easily show/hide the link to Manage order page (in _Layout.cshtml) based on the user’s access:

@if (User.Authorize(Constants.Permissions.ManageOrder))
{ 
<li class="nav-item">
    <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="ManageOrder">ManageOrder</a>
</li>
}

Certainly, we can check if a user has access in other code in the same way. However, it would be nice if we could use the Authorize attribute as well, so we could do something like this in our OrderController:

[Authorize(Constants.Permissions.ManageOrder)]
public IActionResult Index()
{
    return View();
}

In .NET Core/.NET 5 and later, Microsoft has finally realized that role-based access control might not be ideal and, as a result, introduced an authorization system based on policies (read more here: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies). To make it super easy to use our permission-based access control, we will create a custom PolicyProvider that automatically generates a policy to check authorization for a functionality, assuming the application hasn’t already defined a policy with that name:

/// <summary>
/// A policy provider that translates a policy name to a required permission with the same name, if not 
/// <see cref="DefaultAuthorizationPolicyProvider"/> already has provided a policy.
/// </summary>
public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    /// <summary>
    /// Creates a new instance of <see cref="PermissionAuthorizationPolicyProvider" />.
    /// </summary>
    /// <param name="options">The options used to configure this instance.</param>
    public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
    {
    }

    /// <summary>Gets the default authorization policy or a permission based policy if no default is configured.</summary>
    /// <returns>The authorization policy.</returns>
    public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        var policy = await base.GetPolicyAsync(policyName);

        if (policy == null)
        {
            policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .RequireClaim("perm", policyName)
                .Build();
        }

        return policy;
    }
}

Finally we need to tell .NET to use our PolicyProvider instead of DefaultAuthorizationPolicyProvider, and we do this like any other configuration in Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    //Add policy provider that generates a policy for each permission (if a policy is not found)
    services.AddTransient<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
}

With that configuration, we can now use the Authorize attribute as mentioned above.