Troubleshooting
Practical problem/solution guide for Pragmatic.Identity. Each section covers a common issue, the likely causes, and the fix.
The Host Refuses to Start: No Authentication Method for the Environment
Section titled “The Host Refuses to Start: No Authentication Method for the Environment”The endpoints require authorization and no authentication method is configured for environment Production …The endpoints require authorization by default, and nothing can authenticate a caller in the environment
the host is running in: typically UseDevelopmentIdentity() guarded by IsDevelopment(), and the host
started without a launch profile, so in Production. The host stops there (with the maintenance page,
503, unless EnableOnStartupFailure is off) rather than answering 500 to every protected request.
- Run it in the environment you configured (
Properties/launchSettings.json, orASPNETCORE_ENVIRONMENT). - Or give that environment a scheme:
UseJwtAuthentication(),UseKeycloakAuthentication(),UseOidcAuthentication(),UseAuthentication<THandler>(scheme). - Or, for an application that deliberately has no users, declare the host
[AnonymousHost].
ICurrentUser.Id Is Empty Despite Valid Authentication
Section titled “ICurrentUser.Id Is Empty Despite Valid Authentication”The user authenticates successfully (no 401), but ICurrentUser.Id returns an empty string.
Checklist
Section titled “Checklist”-
Check the claim type mapping.
ClaimsPrincipalUserAccessorreads the user ID from the claim type specified byIdentityOptions.UserIdClaimType. The default is the long XML namespace URIhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier. If your IdP usessubor a custom claim:services.AddPragmaticIdentity(opts =>{opts.UserIdClaimType = "sub";}); -
Inspect the actual claims. Add a debug endpoint or log statement to see what claims the token contains:
var claims = currentUser.Claims;foreach (var (key, values) in claims)logger.LogDebug("Claim {Key}: {Values}", key, string.Join(", ", values)); -
Check JWT handler claim mapping. The .NET JWT handler renames
subandroleto the WS-* URIsIdentityOptionsdefaults to, and leavesnameas it is.UseJwtAuthentication()therefore setsNameClaimType = "name",RoleClaimType = ClaimTypes.Role, andIdentityOptions.DisplayNameClaimType = "name". If you are using a custom JWT setup, check thatTokenValidationParameters.NameClaimTypeandIdentityOptions.DisplayNameClaimTypename the same claim; when they do not,User.Identity.Namehas a value andICurrentUser.DisplayNameis null. -
Check if
AddPragmaticIdentity()was called. Without it,ICurrentUseris not registered and may resolve to a default or throw.
User Is Always Anonymous (Header Auth Not Working)
Section titled “User Is Always Anonymous (Header Auth Not Working)”Requests with X-User-Id headers result in ICurrentUser.IsAuthenticated = false.
Checklist
Section titled “Checklist”-
Is
HeaderUserMiddlewareregistered in the pipeline? It must be added manually inIStartupStep.ConfigurePipeline:app.UseMiddleware<HeaderUserMiddleware>(); -
Is the middleware order correct?
HeaderUserMiddlewaremust run beforeUseAuthentication():app.UseMiddleware<HeaderUserMiddleware>(); // Firstapp.UseAuthentication(); // Secondapp.UseAuthorization(); // Third -
Is
NoOpAuthenticationHandlerconfigured? The handler trusts identities set by earlier middleware. Without it, ASP.NET Core has no scheme to authenticate with:app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault"); -
Is the
X-User-Idheader present and non-empty?HeaderUserMiddlewareonly creates an identity whenX-User-Idexists and is not empty. Check for typos in the header name (case-sensitive).
Permissions Always Denied (403 on Everything)
Section titled “Permissions Always Denied (403 on Everything)”Every request that requires permissions returns 403 Forbidden, even for admin users.
Checklist
Section titled “Checklist”-
Are role-to-permission mappings configured? If the user has roles but no direct permissions in claims,
RoleExpansionProvidermust map roles to permissions. Configure viaUseAuthorization():app.UseAuthorization(authz =>{authz.MapRole<AdminRole>();authz.MapRole<BookingManagerRole>(r => r.WithPermissions("booking.reservation.create", "booking.guest.read"));}); -
Are roles in the token/headers? Check that the user’s JWT or headers include role claims. For headers:
X-User-Roles: admin,booking-manager -
Is
AddPragmaticAuthorization()called? This registersPragmaticPermissionHandler. It is called automatically byUseAuthentication()andUseJwtAuthentication(). In a standalone setup, call it explicitly:services.AddPragmaticAuthorization(); -
Is the permission name correct? Permission checks are case-insensitive but must match the configured permission string exactly (or match a wildcard pattern). A user with
booking.reservation.createdoes not matchbooking.reservations.create(plural). -
Is the
IPermissionCheckerregistered? The default isClaimsPermissionChecker. Verify it is in DI:var checker = serviceProvider.GetService<IPermissionChecker>();// Should not be null -
Is the permission provider chain running?
CachedPermissionResolvercollects from allIPermissionProviderinstances. If no providers are registered, the permission set is empty.
JWT Token Rejected (401 Unauthorized)
Section titled “JWT Token Rejected (401 Unauthorized)”Requests with a Bearer token return 401.
Checklist
Section titled “Checklist”-
Is the signing key correct? The key used for generation must match the key used for validation. If they differ (e.g., different environments), the token signature is invalid.
-
Is the token expired? Check the
expclaim. The default expiration is 1 hour with 1 minute clock skew tolerance. -
Is the issuer correct? If
JwtOptions.Issueris set, the token’sissclaim must match exactly. -
Is the audience correct? If
JwtOptions.Audienceis set, the token’saudclaim must match exactly. -
Is the token well-formed? Decode the token at jwt.io and verify:
- The header has
"alg": "HS256" - The payload has
sub,exp, andissclaims - The signature is valid with your key
- The header has
-
Is the Authorization header format correct? Must be
Authorization: Bearer <token>(note the space after “Bearer”). -
Is
UseJwtAuthentication()called? Verify inProgram.csthat the JWT setup runs:app.UseJwtAuthentication(jwt =>{jwt.SigningKey = configuration["Jwt:Key"]!;});
ClaimsPrincipalUserAccessor Circular Dependency
Section titled “ClaimsPrincipalUserAccessor Circular Dependency”At startup, you see an error about circular dependency involving ICurrentUser and IUserAuthorization.
Explanation
Section titled “Explanation”CachedPermissionResolver (which implements IUserAuthorization) depends on ICurrentUser to read claims. ClaimsPrincipalUserAccessor (which implements ICurrentUser) depends on IUserAuthorization via the .Authorization property.
Solution
Section titled “Solution”This is handled internally. ClaimsPrincipalUserAccessor resolves IUserAuthorization lazily via IServiceProvider instead of constructor injection:
public IUserAuthorization Authorization => _authorization ??= serviceProvider.GetRequiredService<IUserAuthorization>();If you see a circular dependency error, it means something else in your DI registration is creating the cycle. Check for services that inject both ICurrentUser and IUserAuthorization in their constructors and trigger resolution of both during construction.
Permission Cache Not Invalidating
Section titled “Permission Cache Not Invalidating”Permissions change in the database, but the user still sees the old permissions.
Checklist
Section titled “Checklist”-
Is cross-request caching enabled? Without
UsePermissionCache(), permissions are resolved fresh per request. If caching is enabled, check the expiration:app.UseAuthorization(authz =>{authz.UsePermissionCache(TimeSpan.FromMinutes(5));}); -
Is the cache being invalidated? When role/permission mappings change, invalidate the cache by user tag:
await cache.RemoveByTagAsync($"user:{userId}"); -
Is the cache provider registered? Cross-request caching requires
ICacheStackfromPragmatic.Caching. Without it,CachedPermissionResolverfalls back to per-request resolution only (no cross-request caching). -
Per-request caching is always on. Even without
UsePermissionCache(),CachedPermissionResolvercaches the resolved permission set for the lifetime of the request scope. This means permission changes take effect on the next request.
ILocalIdentityStore Not Found
Section titled “ILocalIdentityStore Not Found”RegisterUser or LoginUser throws: “Unable to resolve service for type ‘ILocalIdentityStore’”.
Checklist
Section titled “Checklist”-
Does the
[PragmaticUser]entity have aLocalIdentityproperty? That is what makes the generator write{User}.LocalIdentityStore; without one there are no local credentials to store. -
Is
Pragmatic.Persistence.EFCorereferenced by the project that declares the user? The store saves through the boundary’s unit of work, which that package provides. -
Did you write your own? A class implementing
ILocalIdentityStorein the project turns the generated one off; check it is registered ([Service]) and its dependencies resolve.
Self-registration throws NotSupportedException
Section titled “Self-registration throws NotSupportedException”RegisterUser fails with “… is not created by self-registration”. The user entity does not implement
ISelfRegisteringUser<TUser>, so the generated store will not guess how to create a user. Implement it
(static TUser Register(LocalIdentity identity)) or provision users another way.
Account Lockout Not Working
Section titled “Account Lockout Not Working”Failed login attempts do not trigger lockout.
Checklist
Section titled “Checklist”-
Check
LocalIdentityOptions.MaxFailedLoginAttempts. Default is 5. Verify the threshold:services.Configure<LocalIdentityOptions>(opts =>{opts.MaxFailedLoginAttempts = 5;opts.LockoutDuration = TimeSpan.FromMinutes(15);}); -
Is
ILocalIdentityStore.UpdateAsyncpersisting changes? TheLoginUseraction incrementsFailedLoginAttemptsand callsUpdateAsync. IfUpdateAsyncdoes not callSaveChangesAsync, the count is lost. -
Is
IClockregistered? Lockout usesIClock.UtcNowfor timestamp comparison. WithoutIClock, the temporal check fails. This is auto-registered whenPragmatic.Temporalis referenced.
Diagnostics Reference
Section titled “Diagnostics Reference”Identity uses the PRAG1000-1099 range shared with Authorization. The following diagnostics are relevant to Identity configuration:
| ID | Severity | Cause | Fix |
|---|---|---|---|
| PRAG1001 | Error | A permission value declared twice, or equal to a CRUD permission | Declare each [assembly: Permission] value once |
| PRAG1003 | Error | IRole.Name is empty |
Ensure the static abstract Name property returns a non-empty string literal |
| PRAG1050 | Error | Duplicate [UsePackage<T>] on module |
Only one [UsePackage<T>] per package type per module |
| PRAG1695 | Error | Pragmatic.Authorization referenced without Pragmatic.Identity in a host that does not declare [AnonymousHost] |
Reference Pragmatic.Identity.AspNetCore, or put [AnonymousHost] on the host’s [Module] class if the host deliberately has no authentication |
| PRAG1696 | Warning | Pragmatic.Identity.Persistence referenced without Pragmatic.Authorization |
Add the Pragmatic.Authorization reference |
Permission naming (boundary.entity.operation) is a convention, not a compile-time rule: nothing
checks it. It matters because wildcard grants such as booking.* match on those segments.
Can I use ICurrentUser outside of HTTP requests?
Section titled “Can I use ICurrentUser outside of HTTP requests?”Yes. In background jobs, register SystemUser.Instance as the scoped ICurrentUser:
services.AddScoped<ICurrentUser>(_ => SystemUser.Instance);In unit tests, use AnonymousUser.Instance or create a test double.
How do I test with specific permissions?
Section titled “How do I test with specific permissions?”Use HeaderUserMiddleware with the X-User-Permissions header:
GET /api/ordersX-User-Id: test-userX-User-Permissions: orders.read,orders.createOr in integration tests with WebApplicationFactory, set headers on the HttpClient:
client.DefaultRequestHeaders.Add("X-User-Id", "test-user");client.DefaultRequestHeaders.Add("X-User-Permissions", "orders.read,orders.create");Can I use multiple identity providers simultaneously?
Section titled “Can I use multiple identity providers simultaneously?”Yes. Use the UseAuthentication(auth => ...) overload to configure multiple schemes:
app.UseAuthentication(auth =>{ auth.AddJwtBearer("Bearer", options => { /* ... */ }); auth.AddOpenIdConnect("oidc", options => { /* ... */ });});ASP.NET Core evaluates schemes in order. ClaimsPrincipalUserAccessor works with whatever ClaimsPrincipal the winning scheme produces.
Why does ICurrentUser.Kind only return User or Anonymous?
Section titled “Why does ICurrentUser.Kind only return User or Anonymous?”ClaimsPrincipalUserAccessor returns PrincipalKind.User when IsAuthenticated is true, and PrincipalKind.Anonymous when false. The Service and System kinds are set by application-specific implementations. If you need to distinguish service tokens from user tokens, create a custom ICurrentUser wrapper that checks a claim (e.g., client_id or token_type) and returns PrincipalKind.Service.
How do I handle token refresh?
Section titled “How do I handle token refresh?”Pragmatic.Identity.Local.Jwt generates tokens but does not include a refresh token mechanism. For refresh tokens, implement a refresh endpoint that validates the refresh token (stored in your database) and generates a new JWT via JwtTokenGenerator.Generate(). Consider using short-lived access tokens (15 minutes) with longer-lived refresh tokens (7 days).
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/pragmatic-design/Pragmatic.Design/issues
- Showcase Examples: See the
Showcaseproject for working identity configurations across development and production modes. - Package Architecture: See packages.md for detailed descriptions of each sub-package.
- Dev Authentication: See dev-authentication.md for header format and testing patterns.
- JWT Authentication: See jwt-authentication.md for token generation and validation details.
- Authorization: See Pragmatic.Authorization docs for role/permission configuration.
