Skip to content

Repository files navigation

ClaimsPolicyBuilder

A fluent DSL for building ASP.NET Core claims-based authorization policies with compile-time safety.

.NET 10 License: MIT

Problem

Authorization in ASP.NET Core is powerful but ergonomically poor. AuthorizationPolicyBuilder requires 5–10 lines of repetitive policy.Require* calls per policy, uses magic strings for policy names, and offers no composition story.

Solution

ClaimsPolicyBuilder provides:

  • A fluent DSL that expresses policies in 1–2 readable lines
  • Compile-time policy name constants via a Roslyn source generator — typos become build errors, not runtime 403s
  • Policy composition with Extends, And, and Or
  • Zero runtime cost — produces standard AuthorizationPolicy instances

Installation

dotnet add package ClaimsPolicyBuilder

Quick Start

services.AddClaimsPolicies(policies =>
{
    policies.Add("CanEditInvoices")
        .RequireAuthenticated()
        .RequireScope("invoices:write")
        .RequireAnyRole("Accountant", "Admin")
        .RequireClaim("tenant", "acme");

    policies.Add("CanViewReports")
        .Extends("CanEditInvoices")
        .RequireClaim("feature", "reports");

    policies.Add("AdminOnly")
        .RequireRole("Admin")
        .Or(p => p.RequireClaim("override", "true"));
});

Using Generated Policy Name Constants

The source generator emits a Policies class with const string fields:

// Auto-generated at compile time
[Authorize(Policy = Policies.CanEditInvoices)]
public IResult EditInvoice() { ... }

[Authorize(Policy = Policies.AdminOnly)]
public IResult AdminAction() { ... }

Configure the Generated Namespace

Use an assembly attribute:

[assembly: ClaimsPolicyNamespace("MyApp.Auth")]

Or an MSBuild property in your .csproj:

<PropertyGroup>
  <ClaimsPolicyBuilder_Namespace>MyApp.Auth</ClaimsPolicyBuilder_Namespace>
</PropertyGroup>

Defaults to <RootNamespace>.Authorization.

API Reference

Policy Registration

Method Description
services.AddClaimsPolicies(Action<IPolicyRegistration>) Entry point for registering policies
policies.Add(string name) Creates a new named policy and returns a fluent builder

Policy Builder

Method Description
.RequireAuthenticated() Requires an authenticated user
.RequireRole(string) Requires a single role
.RequireAnyRole(params string[]) Requires any one of the specified roles
.RequireAllRoles(params string[]) Requires all of the specified roles
.RequireClaim(string type, params string[]) Requires a claim with optional allowed values
.RequireClaimMatching(string type, Func<string, bool>) Requires a claim matching a predicate
.RequireScope(params string[]) Requires OAuth2-style scope values (space-separated)
.RequireAssertion(Func<AuthorizationHandlerContext, bool>) Custom assertion escape hatch
.Extends(string policyName) Inherits all requirements from another policy (logical AND)
.And(Action<IPolicyBuilder>) Adds additional requirements (logical AND)
.Or(Action<IPolicyBuilder>) Adds alternative requirements (logical OR)

Source Generator Diagnostics

ID Severity Description
CPB001 Error Duplicate policy name detected
CPB002 Warning Policy name is not a valid C# identifier

Examples

1. Simple Role-Based Policy

services.AddClaimsPolicies(policies =>
{
    policies.Add("AdminOnly").RequireRole("Admin");
});

2. Multi-Tenant Policy

services.AddClaimsPolicies(policies =>
{
    policies.Add("TenantAccess")
        .RequireAuthenticated()
        .RequireClaim("tenant", "acme");
});

3. Composed Policies

services.AddClaimsPolicies(policies =>
{
    policies.Add("BaseTenant")
        .RequireAuthenticated()
        .RequireClaim("tenant", "acme");

    policies.Add("TenantAdmin")
        .Extends("BaseTenant")
        .RequireRole("Admin");
});

4. OR Composition

services.AddClaimsPolicies(policies =>
{
    policies.Add("CanAccess")
        .RequireRole("Admin")
        .Or(p => p.RequireClaim("bypass", "true"));
});

5. Scope-Based API Policy

services.AddClaimsPolicies(policies =>
{
    policies.Add("ReadWrite")
        .RequireAuthenticated()
        .RequireScope("api:read", "api:write");
});

Sample App

A runnable ASP.NET Core sample lives in samples/ClaimsPolicyBuilder.Sample. It wires up every policy above and protects minimal-API endpoints with the generated Policies.* constants. A header-driven test auth handler lets you flip a request between allowed and denied without an identity provider:

dotnet run --project samples/ClaimsPolicyBuilder.Sample
# 200 — all requirements met
curl -i http://localhost:5080/invoices/edit \
  -H "X-User: alice" -H "X-Roles: Accountant" \
  -H "X-Scope: invoices:write" -H "X-Claims: tenant=acme"
# 403 — drop the scope and the same request is forbidden
curl -i http://localhost:5080/invoices/edit \
  -H "X-User: alice" -H "X-Roles: Accountant" -H "X-Claims: tenant=acme"

See the sample README and sample.http for the full success/denied matrix of every policy.

Design Decisions

  • Extends semantics: Extending means logical AND of all requirements from the parent policy. Override semantics were considered but rejected as surprising in auth contexts.
  • Or implementation: ASP.NET Core's policy model is AND-only by default. Or is implemented as a single RequireAssertion wrapping both sub-policies. This loses requirement-level introspection but provides correct OR evaluation.
  • No replacement: The library produces standard AuthorizationPolicy instances. It integrates with existing [Authorize], minimal API .RequireAuthorization(), and Blazor <AuthorizeView>.

License

MIT

Releases

Contributors

Languages