Authentication & Authorization
Practical entry point for common questions.
Use this document when you need to answer “how do I do X?” quickly.
Read This First
Section titled “Read This First”If you are completely new:
../../Pragmatic.Authorization/README.md- This How To
- The module documentation:
../../Pragmatic.Authorization/docs/
Setting Up Authentication
Section titled “Setting Up Authentication”How do I set up authentication for development?
Section titled “How do I set up authentication for development?”Use NoOpAuthenticationHandler and HeaderUserMiddleware:
await PragmaticApp.RunAsync(args, app =>{ app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");});In your IStartupStep.ConfigurePipeline, add:
app.UseMiddleware<HeaderUserMiddleware>();app.UseAuthentication();app.UseAuthorization();Then send identity via HTTP headers:
X-User-Id: user-123X-User-Name: John DoeX-User-Roles: booking-manager,catalog-viewerX-User-Permissions: billing.invoice.refundX-User-Groups: customer-careX-Tenant-Id: acmeHow do I set up JWT authentication for production?
Section titled “How do I set up JWT authentication for production?”Add the Pragmatic.Identity.Local.Jwt package and configure:
await PragmaticApp.RunAsync(args, app =>{ app.UseJwtAuthentication(jwt => { jwt.SigningKey = config["Jwt:SigningKey"]!; jwt.Issuer = "my-app"; jwt.Audience = "my-app"; jwt.TokenExpiration = TimeSpan.FromHours(8); });});The JwtAuthenticationHandler validates the token and populates ClaimsPrincipal.
LoginUser action returns a JWT token containing roles, permissions, and tenant claims.
How do I switch between dev and prod auth?
Section titled “How do I switch between dev and prod auth?”Check configuration and pick the handler:
var jwtKey = config["Jwt:SigningKey"];if (!string.IsNullOrEmpty(jwtKey)) app.UseJwtAuthentication(jwt => { jwt.SigningKey = jwtKey; /* ... */ });else app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");This is exactly what the Showcase does in Program.cs.
Permission Basics
Section titled “Permission Basics”How do I require authentication only (no permissions)?
Section titled “How do I require authentication only (no permissions)?”Set the default policy at host level:
app.UseAuthorization(authz =>{ // Endpoints require authentication by default (PragmaticEndpointsOptions.RequireAuthorizationByDefault); // opt a single endpoint out with [AllowAnonymous].});Use this when you want authenticated users but not fine-grained permissions yet.
How do I require one permission?
Section titled “How do I require one permission?”Annotate the operation:
[RequirePermission("booking.reservation.read")]public partial class GetReservationEndpoint : Endpoint<ReservationDto>;Also valid on domain actions, mutations, and queries.
Use the SG-generated strongly-typed constants:
[RequirePermission(BookingPermissions.Reservation.Read)]How do I require ALL of multiple permissions?
Section titled “How do I require ALL of multiple permissions?”[RequirePermission("catalog.property.read", "catalog.amenity.read")]RequirePermission means the user must have all listed permissions.
How do I require ANY of multiple permissions?
Section titled “How do I require ANY of multiple permissions?”[RequireAnyPermission("catalog.property.read", "catalog.property.search")]RequireAnyPermission means the user must have at least one.
How are CRUD permissions generated?
Section titled “How are CRUD permissions generated?”The SG auto-generates permission constants from entities with [Repository] or mutations:
{boundary}.{entity}.read ← from [Repository] or [Query]{boundary}.{entity}.create ← from Create mutation{boundary}.{entity}.update ← from Update mutation{boundary}.{entity}.delete ← from SoftDelete or Delete mutationExample: booking.reservation.read, catalog.amenity.create.
Access them via generated constants: BookingPermissions.Reservation.Read.
Roles, Groups, and Composition
Section titled “Roles, Groups, and Composition”How do I define a role?
Section titled “How do I define a role?”Implement IRole in your host project:
public sealed class BookingManagerRole : IRole{ public static string Name => "booking-manager"; public static string? Description => "Full booking operations with catalog context"; public static IReadOnlyList<string> DefaultPermissions => [];}How do I compose roles from module definitions?
Section titled “How do I compose roles from module definitions?”Modules define reusable permission templates with IRoleDefinition.
The host builds the actual application roles:
app.UseAuthorization(authz =>{ authz.MapRole<BookingManagerRole>(r => r .IncludeDefinition<BookingOperator>() // reservation.*, guest.*, guestprefs.* .IncludeDefinition<CatalogReader>()); // amenity.read, property.read, ...});How do I use wildcards?
Section titled “How do I use wildcards?”authz.MapRole<AdminRole>(r => r.WithAllPermissions()); // → "*"authz.MapRole<CatalogEditor>(r => r.WithAllPermissions<CatalogBoundary>()); // → "catalog.*"authz.MapRole("auditor", r => r .WithOperation<BookingBoundary>(CrudOperation.Read) // → "booking.*.read" .WithOperation<BillingBoundary>(CrudOperation.Read)); // → "billing.*.read"Wildcards are expanded at runtime by WildcardMatcher:
*matches everythingbooking.*matches all booking permissionsbooking.*.readmatches read on all booking entities
How do I remove permissions from a role?
Section titled “How do I remove permissions from a role?”Use WithoutPermissions with explicit permissions only:
authz.MapRole("receptionist", r => r .WithPermissions( BookingPermissions.Reservation.Read, BookingPermissions.Reservation.Create, BookingPermissions.Reservation.Update) .WithoutPermissions(BookingPermissions.Reservation.Update));Do not combine WithoutPermissions with wildcard grants.
The builder fails fast at startup when an exclusion conflicts with a wildcard.
How do I define a group?
Section titled “How do I define a group?”public sealed class CustomerCareGroup : IGroup{ public static string Name => "customer-care"; public static string? Description => "All customer-facing staff"; public static IReadOnlyList<string> DefaultRoles => ["booking-manager", "catalog-viewer"];}Register: authz.MapGroup<CustomerCareGroup>();
Groups expand to roles, which expand to permissions. The chain is:
Group → Roles → Permissions → WildcardMatcher → resolved set.
How do I seed roles and groups from JSON?
Section titled “How do I seed roles and groups from JSON?”Add roles.pragmatic.json to your project and call SeedFromJson():
authz.SeedFromJson();{ "roles": { "front-desk": { "description": "Front desk staff", "permissions": ["booking.reservation.read", "booking.reservation.create"], "inherits": ["catalog-reader"] }, "catalog-reader": { "permissions": ["catalog.amenity.read", "catalog.property.read"] } }, "groups": { "operations": { "roles": ["front-desk"] } }}Notes:
inheritsis expanded recursively at compile time (cycle-safe)descriptionappears as comments in the generated code- JSON is a static baseline, not a replacement for runtime management
Advanced Authorization
Section titled “Advanced Authorization”How do I add a resource-level authorization check?
Section titled “How do I add a resource-level authorization check?”Use IResourceAuthorizer<TResource> when the decision depends on the specific instance:
public sealed class InvoiceAuthorizer : IResourceAuthorizer<RefundInvoiceAction>{ public ValueTask<bool> CanAccessAsync( ICurrentUser user, RefundInvoiceAction action, string operation, CancellationToken ct) => ValueTask.FromResult( user.Authorization.HasPermission("billing.invoice.refund"));}Register: authz.AddResourceAuthorizer<InvoiceAuthorizer>();
Runs at Order 250 in the pipeline (after permission check at 200 and policy at 210).
How do I add a composable policy?
Section titled “How do I add a composable policy?”Use ResourcePolicy for complex rules combining multiple conditions:
public sealed class ReservationManagementPolicy : ResourcePolicy{ public override bool Evaluate(ICurrentUser user) { var isService = HasPrincipalKind(PrincipalKind.Service); var hasPermission = IsAuthenticated() & RequirePermission("booking.reservation.create"); return (isService | hasPermission).Evaluate(user); }}Apply: [RequirePolicy<ReservationManagementPolicy>] on the action.
Operators: & (AND), | (OR), ! (NOT). Like Specification but for authorization.
How do I add row-level security?
Section titled “How do I add row-level security?”Use IPermissionBasedFilter<T>:
public sealed class OwnReservationsFilter(ICurrentUser user) : IPermissionBasedFilter<Reservation>{ public string BypassPermission => "booking.reservations.view-all"; public int Priority => 300;
public Expression<Func<Reservation, bool>> GetFilter() => r => r.CreatedByUserId == user.Id;}Register: services.AddScoped<IQueryFilter<Reservation>, OwnReservationsFilter>();
Important behavior:
- Permission-based filters are skipped for unauthenticated users (no user context = no scope to evaluate)
- Skipped in admin/background filter modes
- Users with
BypassPermissionsee everything
How do internal calls skip authorization?
Section titled “How do internal calls skip authorization?”The SG-generated I{Boundary}InternalActions facade (the one a module injects to call its own operations) wraps invoker calls in EnterInternalCall():
// Generated in BoundaryInterfaceTemplate, for I{Boundary}InternalActionspublic async Task<Result<...>> PlaceOrder(PlaceOrderAction action, CancellationToken ct = default){ using var __scope = _callContext?.EnterInternalCall(); return await _placeOrder.InvokeAsync(action, ct).ConfigureAwait(false);}The public I{Boundary}Actions facade does not: another module calling through it is held to the permission of the operation it invokes, unless the caller is already inside an internal call, such as an event handler or an operation that declares [AbsorbsChildPermissions].
When IsInternalCall == true:
PermissionAuthorizationFilter(Order 200) skipsPolicyEvaluationFilter(Order 210) skipsResourceAuthorizationFilter(Order 250) still runs (instance checks apply)- Data-level filters still apply
This is the intended path for composite flows, event handlers, and intra-boundary orchestration.
Permission Caching
Section titled “Permission Caching”How does permission caching work?
Section titled “How does permission caching work?”Two levels:
-
Request-scoped (always active):
CachedPermissionResolverresolves permissions lazily on first access per request and caches the result for the remainder of the request. -
Cross-request (opt-in):
HybridCachecaches the resolved permission set across requests.
authz.UsePermissionCache(TimeSpan.FromMinutes(5));// orauthz.UsePermissionCache(options =>{ options.Expiration = TimeSpan.FromMinutes(5); options.KeyPrefix = "perms"; // multi-tenant safe});How does the permission resolution chain work?
Section titled “How does the permission resolution chain work?”1. ClaimsPermissionProvider (Order 0) ← reads "permission" claims from JWT2. RoleExpansionProvider (Order 100) ← expands roles → permissions via IRolePermissionStore3. GroupExpansionProvider (Order 200) ← expands groups → roles → permissions via IGroupRoleStore
All results merge into a single HashSet<string>.WildcardMatcher resolves patterns (booking.*, *) against the required permission.Pipeline Order Reference
Section titled “Pipeline Order Reference”Request └─ Authentication (L1) ← middleware: JWT or NoOp handler └─ Endpoint Authorization ← ASP.NET Core: PragmaticPermissionRequirement └─ Action Pipeline ├─ Validation (Order 100) ├─ PermissionAuthorizationFilter (Order 200) ← [RequirePermission] ├─ PolicyEvaluationFilter (Order 210) ← [RequirePolicy<T>] ├─ ResourceAuthorizationFilter (Order 250) ← IResourceAuthorizer<T> ├─ Transaction (Order 300) └─ Action Execution └─ Data-Level Filtering (L4) ← IQueryFilter<T> applied at query timeTroubleshooting
Section titled “Troubleshooting”I get 401 but I’m sending credentials
Section titled “I get 401 but I’m sending credentials”Check:
- Are you using
HeaderUserMiddleware? It requiresX-User-Idheader. - Is
UseAuthentication()called beforeUseAuthorization()in the pipeline? - For JWT: is the token expired? Is the signing key correct?
I get 403 but I think I have the right permission
Section titled “I get 403 but I think I have the right permission”Check:
- Is the permission string exactly right? Use
BookingPermissions.Reservation.Readinstead of typing the string. - Does your role actually include that permission? Check the role mapping in
Program.cs. - Is the permission check happening at endpoint level or action level? Both are independent.
- For wildcards:
booking.*coversbooking.reservation.readbutbooking.reservation.*does not coverbooking.guest.read.
Internal calls still get 403
Section titled “Internal calls still get 403”Check:
- Are you calling through
I{Boundary}InternalActions? The publicI{Boundary}Actionsand direct invoker calls don’t enter internal mode: they enforce the callee’s permission, which is what a 403 here says. - Is
ICallContextregistered in DI? It’s registered automatically when usingPragmatic.Actions.
Permission-based filters don’t seem to work on public endpoints
Section titled “Permission-based filters don’t seem to work on public endpoints”By design: permission-based filters are skipped when no authenticated user is present.
If your endpoint is [AllowAnonymous] and also needs data filtering, use a regular IQueryFilter<T> (not IPermissionBasedFilter<T>).
What is stable and what is evolving?
Section titled “What is stable and what is evolving?”| Area | Status | Notes |
|---|---|---|
| Permission gate (L2) | Stable | [RequirePermission], role/group expansion, wildcards |
| ResourcePolicy (L2b) | Stable | Composable policies, [RequirePolicy<T>] |
| Resource authorization (L3) | Stable | IResourceAuthorizer<T> |
| Data-level filters (L4) | Stable | IPermissionBasedFilter<T>, IQueryFilter<T> |
| JWT authentication | Stable | Identity.Local.Jwt package |
| JSON seeding | Stable | roles.pragmatic.json with inherits |
| Permission caching | Stable | HybridCache cross-request |
| Internal call bypass | Stable | EnterInternalCall() in the I{Boundary}InternalActions facade |
| Dynamic permissions (DB-backed policies) | Future | Designed, not yet implemented |
| RBAC admin UI | Future | Package exists, endpoints not yet exposed |
| Multi-tenant RBAC | Future | Forward-compat interfaces exist |
