22 Sep 2026
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.
// 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.
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).
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.
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.
var permissions = userRoles.SumRolesPermissions().ToHashSet();
AddPolicy for every permission combination. The provider generates them on demand.policy = new AuthorizationPolicyBuilder()
.AddRequirements(new PermissionAuthorizationRequirement(permissions))
.Build();
identity.AddClaim(new Claim(
$"{CustomClaimTypes.Permissions}{permission.Scope}",
permission.Sum.ToString()));
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.
I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.