FactorBasedPermissions 0.0.2

dotnet add package FactorBasedPermissions --version 0.0.2
                    
NuGet\Install-Package FactorBasedPermissions -Version 0.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FactorBasedPermissions" Version="0.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FactorBasedPermissions" Version="0.0.2" />
                    
Directory.Packages.props
<PackageReference Include="FactorBasedPermissions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FactorBasedPermissions --version 0.0.2
                    
#r "nuget: FactorBasedPermissions, 0.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FactorBasedPermissions@0.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FactorBasedPermissions&version=0.0.2
                    
Install as a Cake Addin
#tool nuget:?package=FactorBasedPermissions&version=0.0.2
                    
Install as a Cake Tool

Factor-Based Permissions (C#)

A flexible, attribute-driven authorization library that models permissions as combinations of factors. Designed for compact serialization to fit inside JWT tokens without bloating their size.

Why Factor-Based Permissions?

Traditional role-based access control (RBAC) often leads to role explosion. Factor-based permissions offer a more granular approach:

  • Factors represent conditions (e.g., "email verified", "subscription active", "IP whitelisted")
  • Permissions are granted when all required factors are satisfied
  • Roles can grant sets of permissions declaratively via attributes

JWT-Optimized Serialization

The primary design goal is minimal payload size for embedding in JWT tokens:

Technique Benefit
Base32 encoding 5 bits per character vs 3.3 bits in decimal
Permission grouping Permissions sharing same factors are merged
Short delimiters Single-character separators (!, #, &, +, ,)
No key names Positional format, no JSON overhead

Example: A policy with 10 permissions and 5 factors typically serializes to 30-50 characters — small enough to fit in a JWT claim without concern.

Installation

dotnet add package FactorBasedPermissions

Quick Start

1. Define Your Factors and Permissions

public enum Factor
{
    EmailVerified = 1,
    PhoneVerified = 2,
    SubscriptionActive = 3,
    TwoFactorEnabled = 4,
    AdminApproved = 5
}

public enum Permission
{
    [RequiresFactors(Factor.EmailVerified)]
    ViewDashboard = 1,

    [RequiresFactors(Factor.EmailVerified, Factor.SubscriptionActive)]
    DownloadReports = 2,

    [RequiresFactors(Factor.EmailVerified, Factor.TwoFactorEnabled)]
    ManageApiKeys = 3,

    [RequiresFactors(Factor.AdminApproved)]
    AccessAdminPanel = 4
}

2. Define Roles (Optional)

public enum Role
{
    [GrantsPermissions(
        Permission.ViewDashboard
    )]
    Guest = 1,

    [GrantsPermissions(
        Permission.ViewDashboard,
        Permission.DownloadReports,
        Permission.ManageApiKeys
    )]
    User = 2,

    [GrantsPermissions(
        Permission.ViewDashboard,
        Permission.DownloadReports,
        Permission.ManageApiKeys,
        Permission.AccessAdminPanel
    )]
    Admin = 3
}

3. Create and Serialize Permissions

// Factors satisfied by the current user (determined by your business logic)
var satisfiedFactors = new[] { Factor.EmailVerified, Factor.SubscriptionActive };

// Option A: From a role
var permissions = new FactorBasedPermissions<Factor, Permission>(satisfiedFactors, Role.User);

// Option B: From explicit permission list
var permissions = new FactorBasedPermissions<Factor, Permission>(
    satisfiedFactors,
    new[] { Permission.ViewDashboard, Permission.DownloadReports }
);

// Serialize for JWT
string serialized = permissions.Serialize();
// Example output: "!1,3#1+1&2+1,3"

4. Store in JWT

var claims = new[]
{
    new Claim("sub", userId),
    new Claim("ap", permissions.Serialize())  // "ap" = access policies
};

var token = new JwtSecurityToken(
    issuer: "your-app",
    audience: "your-app",
    claims: claims,
    expires: DateTime.UtcNow.AddHours(1),
    signingCredentials: credentials
);

5. Deserialize and Check (Server-Side)

// Extract from JWT claim
string serialized = User.FindFirst("ap")?.Value;

// Deserialize
var permissions = FactorBasedPermissions<Factor, Permission>.Deserialize(serialized);

// Check permissions
if (permissions.HasPermission(Permission.DownloadReports))
{
    // Access granted
}

// Check with detailed result
if (permissions.HasPermission(Permission.ManageApiKeys, out bool satisfied))
{
    // Permission exists in policy
    if (satisfied)
    {
        // All required factors are met
    }
    else
    {
        // Permission exists but factors not satisfied
    }
}
else
{
    // Permission not in policy at all
}

Serialization Format

The format is designed for minimal size while remaining debuggable:

[!<satisfied_factors>][#<permission_group_1>&<permission_group_2>&...]

Both sections are optional:

  • If no factors are satisfied, the !... section is omitted
  • If no permissions are defined, the #... section is omitted
  • An empty policy serializes to an empty string ""

Structure

Symbol Meaning
! Start of satisfied factors list (optional)
# Start of permissions block (optional)
, Item separator within a list
& Permission group separator
+ Separator between permissions and their required factors

Number Encoding

All numeric IDs are encoded in Base32 using characters 0-9 and a-v:

Decimal Base32
0-9 0-9
10-31 a-v
32 10
100 34
1000 v8

Example Breakdown

!1,3#1+1&2+1,3
  • !1,3 — Satisfied factors: 1 (EmailVerified), 3 (SubscriptionActive)
  • #1+1 — Permission 1 (ViewDashboard) requires factor 1
  • &2+1,3 — Permission 2 (DownloadReports) requires factors 1 and 3

Grouping Optimization

Permissions with identical required factors are grouped together:

// These three permissions all require factor 1:
Permission.A  // requires Factor.X
Permission.B  // requires Factor.X  
Permission.C  // requires Factor.X

// Serialized as: #1,2,3+1
// Instead of:    #1+1&2+1&3+1  (longer)

API Reference

FactorBasedPermissions<TFactorId, TPermissionId>

Constructors
// Empty instance
new FactorBasedPermissions<Factor, Permission>()

// From satisfied factors and permission lookup dictionary
new FactorBasedPermissions<Factor, Permission>(
    IEnumerable<Factor> satisfiedFactors,
    Dictionary<Permission, List<Factor>> permissionsLookup
)

// From satisfied factors and permissions (reads RequiresFactors attributes)
new FactorBasedPermissions<Factor, Permission>(
    IEnumerable<Factor> satisfiedFactors,
    IEnumerable<Permission> permissions
)

// From satisfied factors and role (reads GrantsPermissions attribute)
new FactorBasedPermissions<Factor, Permission>(
    IEnumerable<Factor> satisfiedFactors,
    Enum role
)
Properties
Property Type Description
FactorsLookup Dictionary<TFactorId, bool> All factors with their satisfaction status
PermissionsLookup Dictionary<TPermissionId, List<TFactorId>> Permission to required factors mapping
HasPermissionLookup Dictionary<TPermissionId, bool> Cached permission check results
Methods
Method Returns Description
HasPermission(id, out satisfied) bool Returns true if permission exists; satisfied indicates if factors are met
HasPermission(id, satisfiedFilter) bool Check with filter: true = only satisfied, false = only unsatisfied, null = any
FactorsSatisfied(factors) bool Check if all given factors are satisfied
Same(other) bool Deep equality comparison
Serialize() string Convert to compact string format
Static Methods
Method Returns Description
Deserialize(value, factorConverter?, permissionConverter?) FactorBasedPermissions<...> Parse from serialized string

Attributes

RequiresFactorsAttribute

Apply to permission enum members to declare required factors:

// Generic version (type-safe)
[RequiresFactors<Factor>(Factor.A, Factor.B)]
SomePermission = 1

// Non-generic version (for dynamic scenarios)
[RequiresFactors(Factor.A, Factor.B)]
SomePermission = 1
GrantsPermissionsAttribute

Apply to role enum members to declare granted permissions:

// Generic version (type-safe)
[GrantsPermissions<Permission>(Permission.X, Permission.Y)]
SomeRole = 1

// Non-generic version
[GrantsPermissions(Permission.X, Permission.Y)]
SomeRole = 1

Advanced Usage

Custom Type Converters

You can provide custom converters when deserializing. This is especially useful for enum types to avoid boxing overhead and improve performance:

// Explicit converters are MUCH faster than implicit boxing conversion
var permissions = FactorBasedPermissions<AuthFactor, Permission>.Deserialize(
    serialized,
    factorConverter: id => (AuthFactor)id,
    permissionConverter: id => (Permission)id
);

For non-enum numeric types:

var permissions = FactorBasedPermissions<int, int>.Deserialize(
    serialized,
    factorConverter: id => (int)id,
    permissionConverter: id => (int)id
);

Comparing Permission Sets

var permissions1 = new FactorBasedPermissions<Factor, Permission>(factors1, Role.User);
var permissions2 = new FactorBasedPermissions<Factor, Permission>(factors2, Role.User);

if (permissions1.Same(permissions2))
{
    // Identical factors and permissions
}

Extension Methods

// Get required factors for any enum member
var factors = Permission.DownloadReports.GetRequiredFactors<Factor>();

// Get granted permissions for any role
var permissions = Role.Admin.GetGrantedPermissions<Permission>();

Best Practices

1. Consider Your Use of Value 0

// Option A: Start at 1 to distinguish from default/uninitialized
public enum Permission
{
    ViewDashboard = 1,
    DownloadReports = 2,
    ...
}

// Option B: Use 0 intentionally when it has semantic meaning
public enum AuthFactor
{
    AnyMethod = 0,      // Valid: "any authentication method"
    Password = 10,
    Google = 11,
    ...
}

Starting at 1 helps catch uninitialized values, but 0 is perfectly valid when it represents a meaningful concept.

Keep factor IDs grouped logically for easier debugging of serialized strings:

public enum Factor
{
    // Verification factors: 1-10
    EmailVerified = 1,
    PhoneVerified = 2,
    
    // Subscription factors: 11-20
    SubscriptionActive = 11,
    SubscriptionPremium = 12,
    
    // Security factors: 21-30
    TwoFactorEnabled = 21,
    ...
}

3. Use a DI Service for Permission Checks

Create a scoped service to lazily deserialize and cache permissions per request:

public class UserPermissions : IUserPermissions
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private FactorBasedPermissions<Factor, Permission>? _permissions;
    private bool _initialized;

    public UserPermissions(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public FactorBasedPermissions<Factor, Permission>? Current
    {
        get
        {
            if (!_initialized)
            {
                _permissions = Deserialize();
                _initialized = true;
            }
            return _permissions;
        }
    }

    public bool HasPermission(Permission permission, bool? satisfiedFilter = true)
    {
        return Current?.HasPermission(permission, satisfiedFilter) ?? false;
    }

    private FactorBasedPermissions<Factor, Permission>? Deserialize()
    {
        var serialized = _httpContextAccessor.HttpContext?.User.FindFirst("ap")?.Value;

        if (serialized is null)
            return null;

        return FactorBasedPermissions<Factor, Permission>.Deserialize(serialized, ToFactor, ToPermission);

        static Factor ToFactor(uint x) => (Factor)x;
        static Permission ToPermission(uint x) => (Permission)x;
    }
}

public interface IUserPermissions
{
    FactorBasedPermissions<Factor, Permission> Current { get; }
    bool HasPermission(Permission permission, bool? satisfiedFilter = true);
}

// Registration
services.AddScoped<IUserPermissions, UserPermissions>();

Size Comparison

Approach Example Size Notes
JSON array of permission names ~200 bytes ["ViewDashboard","DownloadReports",...]
JSON object with factors ~300 bytes Full permission-to-factors mapping
This library ~40 bytes !1,3#1+1&2+1,3&3+1,4&4+5

For JWTs that are sent with every HTTP request, this 5-8x size reduction matters.

Requirements

  • .NET Standard 2.1+
  • C# 11+

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.2 105 1/31/2026
0.0.1 946 12/7/2025