fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

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.

Solutions

  1. Verify the user id exists in the target tenant's database before requesting tokens or permission resolution.
  2. Re-authenticate to get a fresh principal bound to an existing user.
  3. Check that tenant resolution is correct so the query hits the tenant that actually contains the user.
  4. Restore or re-seed the missing user row if deletion was accidental.

Example fix

// before: stale token of a deleted user keeps failing
POST /api/tokens with credentials of the deleted user
// after: re-authenticate as an existing user
POST /api/tokens with credentials of an existing, non-deleted user
Defensive patterns

Strategy: try-catch

Validate before calling

// confirm the user exists before requesting permissions/tokens
const check = await fetch(`/api/users/${userId}/exists`);
if (!check.ok) throw new Error('User no longer exists; re-authenticate');

Try / catch

try { await getPermissions(userId); }
catch (e) { if (e.status === 401) { await logout(); redirectToLogin('session-user-missing'); } else { throw e; } }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "User not found", "Invalid user", and "does not exist": what missing-user lookup errors mean across Rocket.Chat, LiteLLM, Phabricator, rustfs, and pnpm — this error's family across 10 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/0c095e5a3e885827. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/UserPermissionService.cs:69

    private ValueTask<PermissionSet> GetOrLoadAsync(string userId, CancellationToken cancellationToken)
    {
        // Stateless factory overload — the factory is a static method group, so the runtime
        // reuses a cached delegate and no closure is allocated per call (including L1 hits).
        var state = new FactoryState(userManager, roleManager, db, userId);

        return cache.GetOrCreateAsync(
            CacheKeys.UserPermissions(userId),
            state,
            LoadPermissionsAsync,
            options: EntryOptions,
            tags: Tags,
            cancellationToken: cancellationToken);
    }

    private static async ValueTask<PermissionSet> LoadPermissionsAsync(FactoryState s, CancellationToken ct)
    {
        var user = await s.UserManager.FindByIdAsync(s.UserId).ConfigureAwait(false);
        _ = user ?? throw new UnauthorizedException();

        var userRoles = await s.UserManager.GetRolesAsync(user).ConfigureAwait(false);

        var directRoleIds = await s.RoleManager.Roles
            .Where(r => userRoles.Contains(r.Name!))
            .Select(r => r.Id)
            .ToListAsync(ct).ConfigureAwait(false);

        // Group-derived roles confer permissions too — the JWT already unions them
        // (IdentityService.AddRoleClaimsAsync) and every group mutation invalidates this
        // cache entry, so the effective set must include roles reachable via UserGroups.
        var groupRoleIds = await s.Db.GroupRoles
            .Where(gr => s.Db.UserGroups
                .Where(ug => ug.UserId == s.UserId)
                .Select(ug => ug.GroupId)
                .Contains(gr.GroupId))
            .Select(gr => gr.RoleId)
            .Distinct()

View on GitHub (pinned to 3f2959e683)