Skip to content

Authentication & Authorization

Practical entry point for common questions.

Use this document when you need to answer “how do I do X?” quickly. Use the architecture documents when you need the full model or internal behavior.

If you are completely new:

  1. ../../Pragmatic.Authorization/README.md
  2. This How To
  3. ../architecture/authentication-authorization.md
  4. ../architecture/resource-permissions.md
  5. ../architecture/data-level-authorization.md

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-123
X-User-Name: John Doe
X-User-Roles: booking-manager,catalog-viewer
X-User-Permissions: billing.invoice.refund
X-User-Groups: customer-care
X-Tenant-Id: acme

How 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.


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 =>
{
authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
});

Use this when you want authenticated users but not fine-grained permissions yet.

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.

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 mutation

Example: booking.reservation.read, catalog.amenity.create.

Access them via generated constants: BookingPermissions.Reservation.Read.


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, ...
});
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 everything
  • booking.* matches all booking permissions
  • booking.*.read matches read on all booking entities

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.

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.

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:

  • inherits is expanded recursively at compile time (cycle-safe)
  • description appears as comments in the generated code
  • JSON is a static baseline, not a replacement for runtime management

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).

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.

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 BypassPermission see everything

The SG-generated boundary facade wraps invoker calls in EnterInternalCall():

// Generated in BoundaryInterfaceTemplate
public async Task<Result<...>> PlaceOrder(PlaceOrderAction action, CancellationToken ct = default)
{
using var __scope = _callContext?.EnterInternalCall();
return await _placeOrder.InvokeAsync(action, ct).ConfigureAwait(false);
}

When IsInternalCall == true:

  • PermissionAuthorizationFilter (Order 200) skips
  • PolicyEvaluationFilter (Order 210) skips
  • ResourceAuthorizationFilter (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.


Two levels:

  1. Request-scoped (always active): CachedPermissionResolver resolves permissions lazily on first access per request and caches the result for the remainder of the request.

  2. Cross-request (opt-in): HybridCache caches the resolved permission set across requests.

authz.UsePermissionCache(TimeSpan.FromMinutes(5));
// or
authz.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 JWT
2. RoleExpansionProvider (Order 100) ← expands roles → permissions via IRolePermissionStore
3. 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.

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 time

Check:

  1. Are you using HeaderUserMiddleware? It requires X-User-Id header.
  2. Is UseAuthentication() called before UseAuthorization() in the pipeline?
  3. 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:

  1. Is the permission string exactly right? Use BookingPermissions.Reservation.Read instead of typing the string.
  2. Does your role actually include that permission? Check the role mapping in Program.cs.
  3. Is the permission check happening at endpoint level or action level? Both are independent.
  4. For wildcards: booking.* covers booking.reservation.read but booking.reservation.* does not cover booking.guest.read.

Check:

  1. Are you calling through the boundary interface? Direct invoker calls don’t auto-enter internal mode.
  2. Is ICallContext registered in DI? It’s registered automatically when using Pragmatic.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>).


AreaStatusNotes
Permission gate (L2)Stable[RequirePermission], role/group expansion, wildcards
ResourcePolicy (L2b)StableComposable policies, [RequirePolicy<T>]
Resource authorization (L3)StableIResourceAuthorizer<T>
Data-level filters (L4)StableIPermissionBasedFilter<T>, IQueryFilter<T>
JWT authenticationStableIdentity.Local.Jwt package
JSON seedingStableroles.pragmatic.json with inherits
Permission cachingStableHybridCache cross-request
Internal call bypassStableEnterInternalCall() in boundary facades
Dynamic permissions (DB-backed policies)FutureDesigned, not yet implemented
RBAC admin UIFuturePackage exists, endpoints not yet exposed
Multi-tenant RBACFutureForward-compat interfaces exist
NeedDocument
current mental model../architecture/authentication-authorization.md
permission naming and role DX../architecture/resource-permissions.md
row-level patterns../architecture/data-level-authorization.md
internal reasoning and tradeoffs../ragionamenti/authorization-identity-stato-arte.md
future work and ongoing fixes../in-progress/authorization-evolution.md