Troubleshooting
Practical problem/solution guide for Pragmatic.Identity. Each section covers a common issue, the likely causes, and the fix.
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 may remap claim types.
UseJwtAuthentication()configuresNameClaimType = "name"andRoleClaimType = "role". If you are using a custom JWT setup, verifyTokenValidationParameters.NameClaimType. -
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”-
Did you implement
ILocalIdentityStore? ThePragmatic.Identity.Localpackage does not include a persistence implementation. You must provide one:public sealed class EfLocalIdentityStore(AppDbContext db) : ILocalIdentityStore{// Implement FindByEmailAsync, FindByExternalKeyAsync, CreateAsync, UpdateAsync, EmailExistsAsync} -
Did you register it in DI?
services.AddScoped<ILocalIdentityStore, EfLocalIdentityStore>(); -
Is the DbContext registered? If your
ILocalIdentityStoredepends on aDbContext, verify it is registered and its connection string is configured.
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 |
|---|---|---|---|
| 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 (e.g., booking.reservation.create) |
| 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 |
| PRAG1051 | Error | Missing [PragmaticUser] when Identity package imported | Add [PragmaticUser] to your user entity |
| PRAG1052 | Error | Identity property type mismatch | The owned Identity property must match the package’s identity record type |
| PRAG1053 | Error | Duplicate package route prefix | Two packages cannot share the same route prefix |
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/nicola-pragmatic/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.