{"record":{"id":"0c095e5a3e885827","repo":"fullstackhero/dotnet-starter-kit","slug":"unauthorized-userpermissionservice","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/UserPermissionService.cs","lineNumber":69,"sourceCode":"    private ValueTask<PermissionSet> GetOrLoadAsync(string userId, CancellationToken cancellationToken)\n    {\n        // Stateless factory overload — the factory is a static method group, so the runtime\n        // reuses a cached delegate and no closure is allocated per call (including L1 hits).\n        var state = new FactoryState(userManager, roleManager, db, userId);\n\n        return cache.GetOrCreateAsync(\n            CacheKeys.UserPermissions(userId),\n            state,\n            LoadPermissionsAsync,\n            options: EntryOptions,\n            tags: Tags,\n            cancellationToken: cancellationToken);\n    }\n\n    private static async ValueTask<PermissionSet> LoadPermissionsAsync(FactoryState s, CancellationToken ct)\n    {\n        var user = await s.UserManager.FindByIdAsync(s.UserId).ConfigureAwait(false);\n        _ = user ?? throw new UnauthorizedException();\n\n        var userRoles = await s.UserManager.GetRolesAsync(user).ConfigureAwait(false);\n\n        var directRoleIds = await s.RoleManager.Roles\n            .Where(r => userRoles.Contains(r.Name!))\n            .Select(r => r.Id)\n            .ToListAsync(ct).ConfigureAwait(false);\n\n        // Group-derived roles confer permissions too — the JWT already unions them\n        // (IdentityService.AddRoleClaimsAsync) and every group mutation invalidates this\n        // cache entry, so the effective set must include roles reachable via UserGroups.\n        var groupRoleIds = await s.Db.GroupRoles\n            .Where(gr => s.Db.UserGroups\n                .Where(ug => ug.UserId == s.UserId)\n                .Select(ug => ug.GroupId)\n                .Contains(gr.GroupId))\n            .Select(gr => gr.RoleId)\n            .Distinct()","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/UserPermissionService.cs#L51-L87","documentation":"LoadPermissionsAsync throws UnauthorizedException() when the user id captured in the permission factory state cannot be found via UserManager.FindByIdAsync. Permissions are loaded lazily from the user's roles and role claims; without a user record there is nothing to authorize. This surfaces when building a permission set for a principal whose user row is missing or deleted in the current context.","triggerScenarios":"Resolving permissions (e.g. during JWT generation or claims transformation) for a UserId that does not exist in the database — typically a deleted user whose token is still being evaluated, or a user stored in a different tenant's database.","commonSituations":"User deleted after token issuance; cross-tenant user id passed in because tenant filtering routed the query to the wrong tenant schema; stale user ids in cached claims; identity data reset (re-seeded DB) invalidating old user ids.","solutions":["Verify the user id exists in the target tenant's database before requesting tokens or permission resolution.","Re-authenticate to get a fresh principal bound to an existing user.","Check that tenant resolution is correct so the query hits the tenant that actually contains the user.","Restore or re-seed the missing user row if deletion was accidental."],"exampleFix":"// before: stale token of a deleted user keeps failing\nPOST /api/tokens with credentials of the deleted user\n// after: re-authenticate as an existing user\nPOST /api/tokens with credentials of an existing, non-deleted user","handlingStrategy":"try-catch","validationCode":"// confirm the user exists before requesting permissions/tokens\nconst check = await fetch(`/api/users/${userId}/exists`);\nif (!check.ok) throw new Error('User no longer exists; re-authenticate');","typeGuard":null,"tryCatchPattern":"try { await getPermissions(userId); }\ncatch (e) { if (e.status === 401) { await logout(); redirectToLogin('session-user-missing'); } else { throw e; } }","preventionTips":["Log users out (revoke tokens) when deleting their accounts.","After DB re-seeds, invalidate old tokens/sessions.","Keep tenant resolution consistent between token issuance and claims transformation.","Monitor for 401s on permission loading — they usually mean a deleted or cross-tenant user."],"tags":["identity","authorization","multitenancy"],"backgroundTag":"user-not-found","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}