Skip to content

Getting Started with Pragmatic.Authorization

This guide walks you through adding authorization to a Pragmatic.Design application from scratch.

  • A Pragmatic.Design host application using PragmaticApp.RunAsync
  • At least one module with entities and actions (e.g., Showcase.Booking)

The host project typically gets Pragmatic.Authorization transitively through modules or Pragmatic.Abstractions. The SG auto-detects it via FeatureDetector when any referenced assembly includes authorization types.

If you need a direct reference:

<PackageReference Include="Pragmatic.Authorization" />

Step 2: Configure Authorization in Program.cs

Section titled “Step 2: Configure Authorization in Program.cs”

The UseAuthorization method on IPragmaticBuilder is the entry point:

await PragmaticApp.RunAsync(args, app =>
{
app.UseAuthorization(authz =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
});
});

DefaultEndpointPolicy controls what happens when an endpoint has no [RequirePermission] or [RequirePolicy] attribute:

ValueBehavior
AllowAnonymousNo authorization required (the default)
RequireAuthenticatedUser must have a valid identity
RequirePermissionSG-derived CRUD permission based on HTTP verb and entity

Application roles live in the host project. Each role is a class implementing IRole:

using Pragmatic.Authorization;
namespace MyApp.Host.Authorization;
public sealed class AdminRole : IRole
{
public static string Name => "admin";
public static string? Description => "Full system access";
public static IReadOnlyList<string> DefaultPermissions => ["*"];
}

Then map it in Program.cs:

app.UseAuthorization(authz =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
authz.MapRole<AdminRole>();
});

When a user has the role claim set to "admin", the RoleExpansionProvider resolves it to ["*"], which WildcardMatcher matches against any required permission.

Annotate actions, mutations, or queries with [RequirePermission]:

using Pragmatic.Authorization;
[RequirePermission("booking.reservation.read")]
public sealed class SearchReservationsQuery : IQuery<PagedResult<ReservationDto>>
{
// query properties...
}

The SG generates permission constants per entity. After a build, you can use them:

[RequirePermission(BookingPermissions.Reservation.Read)]
public sealed class SearchReservationsQuery : IQuery<PagedResult<ReservationDto>>
{
// ...
}

Step 5: Define Module Permission Templates

Section titled “Step 5: Define Module Permission Templates”

Modules define IRoleDefinition to expose reusable permission bundles. These are NOT application roles — they are building blocks that the host composes into roles.

using Pragmatic.Authorization;
namespace MyModule.Authorization;
public sealed class BookingOperator : IRoleDefinition
{
public static string Name => "booking-operator";
public static string? Description => "Full CRUD on reservations and guests";
public static IReadOnlyList<string> Permissions =>
[
BookingPermissions.Reservation.All, // "booking.reservation.*"
BookingPermissions.Guest.All // "booking.guest.*"
];
}

In the host’s Program.cs, compose application roles by pulling in module definitions:

app.UseAuthorization(authz =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
authz.MapRole<AdminRole>();
authz.MapRole<ManagerRole>(r => r
.IncludeDefinition<BookingOperator>()
.IncludeDefinition<CatalogReader>());
authz.MapRole<ReceptionistRole>(r => r
.WithPermissions(
BookingPermissions.Reservation.Read,
BookingPermissions.Reservation.Create,
BookingPermissions.Reservation.Update)
.IncludeDefinition<CatalogReader>());
});

The ManagerRole class itself can have empty DefaultPermissions — all permissions come from the composition in Program.cs.

Groups aggregate roles. A user with a group claim gets all the roles (and their permissions) assigned to that group.

using Pragmatic.Authorization;
namespace MyApp.Host.Authorization;
public sealed class CustomerCareGroup : IGroup
{
public static string Name => "customer-care";
public static string? Description => "Customer care team";
public static IReadOnlyList<string> DefaultRoles =>
[
BookingManagerRole.Name,
CatalogViewerRole.Name
];
}

Then map it:

authz.MapGroup<CustomerCareGroup>();

For operations beyond standard CRUD, define IPermission:

using Pragmatic.Authorization;
public sealed class RefundInvoicePermission : IPermission
{
public static string Name => "billing.invoice.refund";
public static string? Description => "Refund a paid invoice";
public static string? Category => "Billing";
}

Or use Mode 2 on [RequirePermission] to let the SG derive the full name:

[RequirePermission("refund", Description = "Refund a paid invoice")]
public sealed class RefundInvoiceAction : IDomainAction<InvoiceId, VoidResult>
{
// SG generates: billing.invoice.refund (derived from boundary.entity.operation)
}

Step 9: Add a Resource Authorizer (Optional)

Section titled “Step 9: Add a Resource Authorizer (Optional)”

For instance-level access control (e.g., “can this user access this specific invoice?”):

using Pragmatic.Authorization;
using Pragmatic.Identity;
public sealed class InvoiceAuthorizer : IResourceAuthorizer<RefundInvoiceAction>
{
public ValueTask<bool> CanAccessAsync(
ICurrentUser user, RefundInvoiceAction resource, string action,
CancellationToken ct = default)
{
if (user.Authorization.HasPermission("billing.admin"))
return ValueTask.FromResult(true);
if (!user.IsAuthenticated)
return ValueTask.FromResult(false);
return ValueTask.FromResult(
user.Authorization.HasPermission("billing.invoice.refund"));
}
}

Register it:

authz.AddResourceAuthorizer<InvoiceAuthorizer>();

For cross-request permission caching via ICacheStack (using CacheCategories.Permissions):

authz.UsePermissionCache(TimeSpan.FromMinutes(5));

Cache keys include the user ID (and tenant ID if multi-tenant). Tags include user:{userId} for targeted invalidation.

Add roles.pragmatic.json to your host project:

{
"roles": {
"front-desk": {
"description": "Front desk staff",
"permissions": ["booking.reservation.read", "booking.reservation.create"]
}
},
"groups": {
"operations": {
"description": "All operational staff",
"roles": ["front-desk", "catalog-viewer"]
}
}
}

The SG generates SeedFromJson():

authz.SeedFromJson();

Putting it all together (from the Showcase application):

await PragmaticApp.RunAsync(args, app =>
{
app.UseAuthorization(authz =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
// Roles
authz.MapRole<ShowcaseAdmin>();
authz.MapRole<BookingManagerRole>(r => r
.IncludeDefinition<BookingOperator>()
.IncludeDefinition<CatalogReader>());
authz.MapRole<CatalogEditorRole>(r => r
.IncludeDefinition<CatalogEditor>());
authz.MapRole("auditor", r => r
.WithOperation<BookingBoundary>(CrudOperation.Read)
.WithOperation<BillingBoundary>(CrudOperation.Read)
.WithOperation<CatalogBoundary>(CrudOperation.Read));
// Groups
authz.MapGroup<CustomerCareGroup>();
// ABAC
authz.AddResourceAuthorizer<InvoiceAuthorizer>();
// JSON seeding
authz.SeedFromJson();
// Caching
authz.UsePermissionCache(TimeSpan.FromMinutes(5));
});
});

The authorization system reads claims from ICurrentUser.Claims:

Claim typeUsed by
permissionClaimsPermissionProvider (direct permissions)
roleRoleExpansionProvider (expanded to permissions)
groupGroupExpansionProvider (expanded to roles, then permissions)
scopeIUserAuthorization.Scopes

How these claims get populated depends on your authentication setup:

  • JWT: Claims from the token (standard JWT claims)
  • NoOp (development): Claims from HTTP headers (X-User-Id, X-User-Roles, X-User-Permissions, X-User-Groups)
  • External IdP: Claims from the identity provider’s token or userinfo endpoint