Troubleshooting
Practical problem/solution guide for Pragmatic.Authorization. Each section covers a common issue, the likely causes, and the fix.
User Gets 403 Forbidden on Every Request
Section titled “User Gets 403 Forbidden on Every Request”Authenticated users are denied access to all endpoints, even those they should have permission for.
Checklist
Section titled “Checklist”-
Is
UseAuthorizationcalled inProgram.cs? Without it, no role-to-permission mappings exist. Users withroleclaims have no mechanism to resolve those roles into permissions. -
Are roles mapped correctly? Check that the role names in
MapRolematch theroleclaim values in the user’s token. Role names are case-insensitive.// If the JWT has role claim "booking-manager":authz.MapRole<BookingManagerRole>(); // BookingManagerRole.Name must return "booking-manager" -
Does the role have the right permissions? Check
IRole.DefaultPermissionsand theIncludeDefinition/WithPermissionscalls in the builder:authz.MapRole<BookingManagerRole>(r => r.IncludeDefinition<BookingOperator>()); // What permissions does BookingOperator define? -
Is the
[RequirePermission]string correct? Compare the permission string on the action with the permissions granted to the role. A typo like"booking.reservations.read"(plural) will not match"booking.reservation.read"(singular). Use SG-generated constants. -
Is
DefaultEndpointPolicyset too restrictively? IfDefaultPolicy = DefaultEndpointPolicy.RequirePermissionis set, endpoints without[RequirePermission]get auto-derived permission requirements. Verify these match the user’s permissions. -
Check the permission claim type. By default, direct permissions come from the
"permission"claim. If your token uses a different claim type (e.g.,"permissions"or"scp"), configureAuthorizationOptions.PermissionClaimType. -
Is
ActionCallContext.IsInternalCallalways false? Internal calls skip permission and policy checks. If authorization is being enforced on intra-boundary calls, check thatActionCallContextis properly scoped.
Permissions Not Resolving from Roles
Section titled “Permissions Not Resolving from Roles”The user has a role claim, but IUserAuthorization.HasPermission returns false for permissions that should be in that role.
Checklist
Section titled “Checklist”-
Is the
RoleExpansionProviderregistered? It is only registered whenMapRoleis called orUseRolePermissionStore<T>()is used. If neither is called,roleclaims are not expanded. -
Does the
InMemoryRolePermissionStorecontain the role? Add a breakpoint or log inRoleExpansionProvider.ResolvePermissionsAsyncto verify the store returns permissions for the role name. -
Wildcard matching order: If the role grants
"booking.*", the user gets this string in their resolved set.HasPermission("booking.reservation.read")first checks exact match (no hit for"booking.*"), then checks wildcards (hit). This should work. If it does not, check thatWildcardMatcheris reached (add a breakpoint onCachedPermissionResolver.MatchesPermission). -
Case mismatch in role name: Role names are case-insensitive in the
InMemoryRolePermissionStore(usesStringComparer.OrdinalIgnoreCase). But verify your custom store (if used) also handles case-insensitivity.
Permissions Not Resolving from Groups
Section titled “Permissions Not Resolving from Groups”The user has a group claim, but permissions from group membership are not resolved.
Checklist
Section titled “Checklist”-
Is
MapGroupcalled? TheGroupExpansionProvideris only registered whenMapGroupis called orUseGroupRoleStore<T>()is used. -
Are the group’s roles also mapped?
GroupExpansionProviderresolves groups to roles (viaIGroupRoleStore), then roles to permissions (viaIRolePermissionStore). Both stores must be populated.// Group maps to roles:authz.MapGroup<CustomerCareGroup>(); // DefaultRoles = ["booking-manager", "catalog-viewer"]// Those roles must also be mapped:authz.MapRole<BookingManagerRole>(r => r.IncludeDefinition<BookingOperator>());authz.MapRole<CatalogViewerRole>(r => r.IncludeDefinition<CatalogReader>()); -
Is the group claim name correct? The
GroupExpansionProviderreads fromICurrentUser.Claims["group"]. If your token uses a different claim type (e.g.,"groups"), the provider will not find it. Ensure your authentication handler maps the claim type to"group".
ResourcePolicy Evaluation Returns False Unexpectedly
Section titled “ResourcePolicy Evaluation Returns False Unexpectedly”A [RequirePolicy<T>] attribute is present, but the policy always denies.
Checklist
Section titled “Checklist”-
Does the policy have a parameterless constructor?
PolicyEvaluationFiltercreates instances viaActivator.CreateInstance. Thenew()constraint onRequirePolicyAttribute<T>catches this at compile time, but verify no runtime factory is bypassing it. -
Is the user authenticated?
PolicyEvaluationFilterreturnsUnauthorizedError(401) ifCurrentUser.IsAuthenticatedis false, before evaluating the policy. If you see 401 instead of 403, the issue is authentication, not the policy. -
Debug the policy directly. Policies are pure functions on
ICurrentUser. Unit test them:var policy = new ReservationManagementPolicy();var mockUser = CreateMockCurrentUser(permissions: ["booking.reservation.create"]);var result = policy.Evaluate(mockUser);Assert.True(result); -
Check operator precedence.
&(AND) binds tighter than|(OR) in C#:// This is (A & B) | C, not A & (B | C)var policy = ResourcePolicy.IsAuthenticated()& ResourcePolicy.RequirePermission("booking.reservation.create")| ResourcePolicy.HasPrincipalKind(PrincipalKind.Service);Use parentheses for clarity.
-
Internal calls skip policy evaluation.
PolicyEvaluationFilterchecksCallContext.IsInternalCalland skips evaluation for intra-boundary calls. If you expect the policy to run on an internal call, this is by design.
IResourceAuthorizer Not Being Called
Section titled “IResourceAuthorizer Not Being Called”The IResourceAuthorizer<T> is registered but CanAccessAsync never executes.
Checklist
Section titled “Checklist”-
Is the authorizer registered with the correct type parameter? The
ResourceAuthorizationFilterresolvesIResourceAuthorizer<TAction>whereTActionis the concrete action type. If you registeredIResourceAuthorizer<BaseAction>but the action isRefundInvoiceAction, the filter will not find it (unlessBaseActionisRefundInvoiceAction). -
Was
AddResourceAuthorizer<T>()called? CheckProgram.cs. The method discovers allIResourceAuthorizer<T>interfaces on the type via reflection and registers each. -
Is the action invoked through the pipeline?
ResourceAuthorizationFilterruns in the action pipeline (Order 250). If the action is called directly (not throughIDomainActionInvoker), the filter does not run. -
Is the call internal?
ResourceAuthorizationFilterdoes not checkIsInternalCall, butPolicyEvaluationFilterat Order 210 does. Verify which filter is skipping. Resource authorization runs at Order 250 and does not skip internal calls.
SG Not Generating Permission Constants
Section titled “SG Not Generating Permission Constants”Expected BookingPermissions.Reservation.Read but the constant does not exist after build.
Checklist
Section titled “Checklist”-
Is
Pragmatic.Authorizationreferenced? The SG generates permission constants only whenFeatureDetector.HasAuthorizationis true. This flag is set when the assembly referencesPragmatic.Authorization. -
Are there
IPermissionimplementations in the assembly? The SG scans for classes implementingIPermissionto generate constants. If noIPermissiontypes exist, no constants are generated. -
Is
IPermission.Namea non-empty string literal? The SG reads theNameproperty at compile time. If it is computed from a variable or method call, the SG may not resolve it. Use a string literal:public static string Name => "booking.reservation.read"; // OK -
Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator in Solution Explorer. Look for files named
_Infra.Identity.*.g.cs. -
Is the naming convention correct? Constants are grouped by the first segment (boundary). If
IPermission.Namedoes not contain a dot, the SG cannot extract a category and may skip generation.
Cross-Request Cache Not Working
Section titled “Cross-Request Cache Not Working”Permissions are resolved fresh on every request despite UsePermissionCache being configured.
Checklist
Section titled “Checklist”-
Is
ICacheStackregistered? Cross-request caching requiresPragmatic.Cachingto be referenced andICacheStackto be available in DI. Without it,CachedPermissionResolverfalls back to request-scoped caching only. -
Is the user authenticated?
CachedPermissionResolver.CanUseCrossRequestCache()checks three conditions:_cache is not null,_cacheOptions is not null, and_currentUser.IsAuthenticated. Anonymous users are never cached cross-request. -
Check the cache key. The key is
{prefix}:{userId}(or{prefix}:{tenantId}:{userId}for multi-tenant). If the user ID changes between requests (e.g., different header values in development), each request gets a different cache key. -
Check the expiration. Default TTL is 5 minutes. If you are testing with short intervals, the cache may have expired.
Diagnostics Reference
Section titled “Diagnostics Reference”| ID | Severity | Cause | Fix |
|---|---|---|---|
| PRAG1000 | Error | IPermission.Name is empty | Ensure the static abstract Name property returns a non-empty string literal |
| PRAG1001 | Error | Duplicate permission name across types | Each IPermission type must have a unique Name |
| PRAG1002 | Warning | Permission name does not follow convention | Use the format boundary.entity.operation |
| PRAG1003 | Error | IRole.Name is empty | Ensure the static abstract Name property returns a non-empty string literal |
Runtime Exceptions
Section titled “Runtime Exceptions”| Exception | Thrown by | Cause | Fix |
|---|---|---|---|
InvalidOperationException (“Cannot exclude…covered by wildcard”) | RoleBuilder.Resolve() | WithoutPermissions conflicts with a wildcard grant | Use explicit permissions instead of wildcards when using WithoutPermissions |
NotSupportedException (“Custom policies cannot be serialized”) | PolicySerializer.Serialize() | Attempting to serialize a Custom() or RequireExternalPermission() policy | Use built-in factory methods for serializable policies |
Can I use multiple [RequirePermission] attributes on one action?
Section titled “Can I use multiple [RequirePermission] attributes on one action?”No. [RequirePermission] has AllowMultiple = false. Use [RequirePermission("perm.a", "perm.b")] for AND logic (all required), or create a policy with RequireAnyPermission for OR logic.
How do I check permissions in business logic (not just as a gate)?
Section titled “How do I check permissions in business logic (not just as a gate)?”Use ICurrentUser.Authorization.HasPermission("permission.name") for branching logic inside your handler. The [RequirePermission] attribute is for the gate; IUserAuthorization is for in-handler decisions.
Can I add permissions at runtime (not from code)?
Section titled “Can I add permissions at runtime (not from code)?”Yes. Implement IDynamicPermissionStore for runtime-created permissions. They are merged with SG-generated permissions in DefaultPermissionCatalog. For runtime role management, implement IDynamicRoleStore.
How do I test authorization in integration tests?
Section titled “How do I test authorization in integration tests?”Use the NoOpAuthenticationHandler with HTTP headers:
// Set roles via headersvar client = factory.CreateClient();client.DefaultRequestHeaders.Add("X-User-Id", "test-user-id");client.DefaultRequestHeaders.Add("X-User-Roles", "booking-manager");The NoOpAuthenticationHandler maps these headers to claims. The RoleExpansionProvider resolves the role claim to permissions via the in-memory stores configured in UseAuthorization.
Why does PolicyEvaluationFilter use reflection (Activator.CreateInstance)?
Section titled “Why does PolicyEvaluationFilter use reflection (Activator.CreateInstance)?”RequirePolicyAttribute<T> has a new() constraint, so Activator.CreateInstance is safe. The created instance is cached in a static ConcurrentDictionary and reused for all subsequent requests. This is a one-time cost per policy type.
How do internal calls (intra-boundary) work with authorization?
Section titled “How do internal calls (intra-boundary) work with authorization?”PolicyEvaluationFilter (Order 210) checks CallContext.IsInternalCall and skips evaluation for internal calls. The user context (ICurrentUser) is still preserved, so downstream IResourceAuthorizer and IQueryFilter checks can still access the user’s permissions. Only the permission gate and policy evaluation are skipped.
Can I use [AllowAnonymous] to override [RequirePermission]?
Section titled “Can I use [AllowAnonymous] to override [RequirePermission]?”[AllowAnonymous] is an ASP.NET Core concept that overrides endpoint-level authorization. It works with Pragmatic.Endpoints group-level authorization. For action-level authorization ([RequirePermission] on a DomainAction), the enforcement happens in the action pipeline, not in ASP.NET Core middleware. To skip authorization on specific actions, do not apply [RequirePermission] to them.
What happens when a user has both direct permissions and role-based permissions?
Section titled “What happens when a user has both direct permissions and role-based permissions?”All provider results are merged with union semantics (additive). If ClaimsPermissionProvider (Order 0) returns {"booking.reservation.read"} and RoleExpansionProvider (Order 100) returns {"booking.*"}, the final set is {"booking.reservation.read", "booking.*"}. No provider can remove permissions added by another.
How do I list all permissions in the system for an admin UI?
Section titled “How do I list all permissions in the system for an admin UI?”Use IPermissionCatalog.GetAllPermissionsAsync(). This returns SG-generated permissions merged with any IDynamicPermissionStore implementations. For roles, use IPermissionCatalog.GetAllRolesAsync(). For protected resources and their policies, use IResourceCatalog.GetAllResourcesAsync().
Can I serialize a policy to store it in a database?
Section titled “Can I serialize a policy to store it in a database?”Yes, use PolicySerializer.Serialize(policy) to convert a ResourcePolicy to a PolicyExpression record tree. The expression can be serialized to JSON. To restore: PolicySerializer.Deserialize(expr). Delegate-based policies (Custom() and RequireExternalPermission()) are not serializable and throw NotSupportedException.
Startup Fails with InvalidOperationException
Section titled “Startup Fails with InvalidOperationException”WithoutPermissions Wildcard Conflict
Section titled “WithoutPermissions Wildcard Conflict”Error: “Cannot exclude ‘booking.reservation.delete’ — covered by wildcard ‘booking.*’. Use explicit permissions instead of wildcards when using WithoutPermissions.”
Cause: RoleBuilder.WithoutPermissions detects that the excluded permission is covered by a wildcard grant, which would silently fail at runtime.
Fix: Replace the wildcard with explicit permissions, then exclude the unwanted one:
// Instead of:authz.MapRole<Role>(r => r .WithPermissions("booking.*") .WithoutPermissions("booking.reservation.delete"));
// Use:authz.MapRole<Role>(r => r .WithPermissions( BookingPermissions.Reservation.Read, BookingPermissions.Reservation.Create, BookingPermissions.Reservation.Update, BookingPermissions.Guest.All));Observability Counters Show High Denial Rate
Section titled “Observability Counters Show High Denial Rate”If pragmatic.authorization.permission_denied is much higher than expected:
Checklist
Section titled “Checklist”-
Check role mappings. A common cause is roles that are too restrictive. Verify that each role has the permissions its users need.
-
Check permission naming. If the SG generates
booking.reservation.readbut the[RequirePermission]uses a hardcoded string with a different casing or spelling, the exact match fails. Even though matching is case-insensitive, use generated constants to be safe. -
Check wildcard coverage. A role with
"booking.reservation.*"does not grant"billing.invoice.read". Verify that wildcard patterns cover the right boundary and entity. -
Check for anonymous requests. Unauthenticated requests always fail permission checks. If you have public endpoints, use
DefaultEndpointPolicy.AllowAnonymousor[AllowAnonymous]rather than expecting anonymous users to have permissions. -
Check cache behavior. If
pragmatic.authorization.cache_missesis very high, the provider chain runs on every request. This may indicate that cross-request caching is not configured or thatICacheStackis not registered.
JSON Role Seeding Not Working
Section titled “JSON Role Seeding Not Working”The roles.pragmatic.json file exists but SeedFromJson() is not available.
Checklist
Section titled “Checklist”-
Is the file included as an AdditionalFile? The SG reads
roles.pragmatic.jsonviaAdditionalTextsProvider. Ensure your.csprojincludes it:<ItemGroup><AdditionalFiles Include="roles.pragmatic.json" /></ItemGroup> -
Is the file name exact? The SG looks for files ending with
roles.pragmatic.json(case-insensitive). -
Is the JSON valid? Invalid JSON will cause the SG to skip the file silently. Validate the JSON structure.
-
Check the SG output. Look for a generated file named
_Infra.Identity.RoleSeeding.g.csin the analyzer output.
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
- Showcase Examples: See the
Showcaseproject for working authorization configurations, role compositions, and integration tests. - Getting Started: See getting-started.md for a zero-to-running walkthrough.
- Permission Resolution: See permission-resolution.md for provider chain internals.
- Policies: See policies.md for composable
ResourcePolicyandAsyncResourcePolicy. - Roles and Groups: See roles-and-groups.md for the composition model.
- Stores: See stores.md for in-memory vs database-backed stores.