Permission-Based Authorization: Bitmask Claims and Dynamic Policies

Permission-Based Authorization: Bitmask Claims and Dynamic Policies

22 Sep 2026

Permission-Based Authorization: Bitmask Claims and Dynamic Policies

Authorization in ASP.NET Core typically means [Authorize] attributes with role strings or policy names. It works, but it scales poorly. When you have dozens of modules, each with their own set of permissions, managing static policies becomes a maintenance nightmare. Brand Website V3 solves this with a custom authorization system built on bitmask-based permissions and dynamic policy generation.

The idea is simple: each module defines a [Flags] enum of permissions. At login, a claims principal factory aggregates all of a user's role permissions using bitwise OR and stores them as claims. At endpoint time, a .RequirePermission() extension method checks whether the user's bitmask includes the required permission. Policies are generated on-the-fly. No static policy registration needed.

The Permissions Enum

// Blog._Shared/_Shared.cs
[Flags]
public enum Permissions
{
    None = 0,
    ManagePosts = 1,
    All = ~None
}

Each module defines its own Permissions enum. The [Flags] attribute enables bitwise operations. All is the bitwise complement of None, meaning every permission is set.

Claims Principal Factory

At login, the factory aggregates role permissions into claims:

// Auth.Api/CustomUserClaimsPrincipalFactory.cs
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user)
{
    var identity = await base.GenerateClaimsAsync(user);
    var userRoleNames = await UserManager.GetRolesAsync(user);
    var userRoles = await RoleManager.Roles
        .Include(r => r.Permissions)
        .Where(r => userRoleNames.Contains(r.Name!))
        .ProjectToType<RoleResponse>()
        .ToListAsync();

    var permissions = userRoles.SumRolesPermissions().ToHashSet();

    foreach (var permission in permissions)
    {
        identity.AddClaim(new Claim(
            $"{CustomClaimTypes.Permissions}{permission.Scope}",
            permission.Sum.ToString()));
    }
    return identity;
}

The factory loads all of the user's roles, aggregates their permissions using bitwise OR, and stores the result as claims. Each claim is scoped to a module. A user might have Blog:3 (ManagePosts | ManageCategories) and Reviews:1 (ManageReviews).

Dynamic Policy Provider

The FlexibleAuthorizationPolicyProvider generates authorization policies on-the-fly:

// Auth.Api/FlexibleAuthorizationPolicyProvider.cs
public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
    var policy = await base.GetPolicyAsync(policyName);
    if (policy == null && PolicyNameHelper.IsValidPolicyName(policyName))
    {
        var permissions = PolicyNameHelper.GetPermissionsFrom(policyName);
        policy = new AuthorizationPolicyBuilder()
            .AddRequirements(new PermissionAuthorizationRequirement(permissions))
            .Build();
        _options.AddPolicy(policyName, policy);
    }
    return policy;
}

When .RequirePermission(nameof(Blog), (int)Permissions.ManagePosts) is called, the policy provider parses the policy name, extracts the scope and bitmask, and creates an authorization policy dynamically. No need to register every possible combination of permissions in Startup.

Endpoint-Level Authorization

builder.MapGet("/", GetReviewsAsync)
    .RequirePermission(nameof(Reviews), (int)Permissions.ManageReviews);

The check uses bitwise AND: (userPermissions & requiredPermissions) != 0. If the user has any of the required bits set, they are authorized.

Why It Works

  • Bitmask operations are fast and composable. Adding a new permission is one enum value. Combining permissions is one bitwise OR. Checking permissions is one bitwise AND.
var permissions = userRoles.SumRolesPermissions().ToHashSet();
  • Dynamic policies eliminate registration boilerplate. You never need to call AddPolicy for every permission combination. The provider generates them on demand.
policy = new AuthorizationPolicyBuilder()
    .AddRequirements(new PermissionAuthorizationRequirement(permissions))
    .Build();
  • Per-module scoping prevents cross-module permission leaks. A Blog permission never accidentally grants access to Reviews.
identity.AddClaim(new Claim(
    $"{CustomClaimTypes.Permissions}{permission.Scope}",
    permission.Sum.ToString()));
  • Endpoint-level checks are clean and readable. One extension method, no attributes, no magic strings.
builder.MapGet("/", GetReviewsAsync)
    .RequirePermission(nameof(Reviews), (int)Permissions.ManageReviews);

Brand Website V3 implements permission-based authorization with bitmask claims and dynamic policies across all modules. See the complete implementation at kiss-code.com.


Join the community

I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.

Newsletter

Allow me to share what I learn, with you.

Share

Support

An error has occurred. 🗙