fullstackhero/dotnet-starter-kit · error · NotFoundException

target user not found

Error message

target user not found

What it means

The handler asks IIdentityService.BuildClaimsForUserAsync to build claims for the target user in the target tenant. A null result means no such user exists in that tenant, so it throws NotFoundException('target user not found') (HTTP 404).

Solutions

  1. Verify TargetUserId and TargetTenantId match an existing, active user (re-fetch the user list for that tenant)
  2. Confirm you are passing the user's id, not email or username
  3. Check the target tenant id spelling against the tenants list

Example fix

// before
await api.startImpersonation({ targetUserId: user.email, targetTenantId }); // 404
// after
await api.startImpersonation({ targetUserId: user.id, targetTenantId });
Defensive patterns

Strategy: validation

Validate before calling

const user = await api.getUser(targetUserId, targetTenantId).catch(() => null);
if (!user) throw new Error(`no user ${targetUserId} in tenant ${targetTenantId}`);

Try / catch

try { await api.startImpersonation(req); }
catch (e) { if (e.status === 404 && /target user/.test(e.message)) { refreshTargetUserList(); return; } throw e; }

Prevention

When it happens

Trigger: TargetUserId does not exist, exists in a different tenant than TargetTenantId, or the user is inactive/deleted in that tenant.

Common situations: Target user id from another environment; targeting a user by email instead of id; tenant mismatch — user exists but under a different tenant id; user deleted while the support UI list was open.

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/14da1e23626fcebe. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs:96

        }

        // Prevent nesting: if the caller is already impersonating, require end-impersonation first.
        var callerClaims = _currentUser.GetUserClaims();
        if (callerClaims is not null
            && callerClaims.Any(c => c.Type == ClaimConstants.ActorSubject))
        {
            throw new CustomException(
                "end current impersonation before starting a new one",
                errors: null,
                System.Net.HttpStatusCode.BadRequest);
        }

        var targetClaimsResult = await _identityService
            .BuildClaimsForUserAsync(request.TargetUserId, request.TargetTenantId, cancellationToken);

        if (targetClaimsResult is null)
        {
            throw new NotFoundException("target user not found");
        }

        var (subject, claims) = targetClaimsResult.Value;
        var targetUserName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value
            ?? claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Name)?.Value;

        // Strip the auto-generated jti from BuildClaimsForUserAsync and inject our own, so the persisted
        // ImpersonationGrant row and the issued JWT share the same jti.
        var jti = Guid.NewGuid().ToString("N");
        var impersonationClaims = claims
            .Where(c => c.Type != JwtRegisteredClaimNames.Jti)
            .Concat(
            [
                new Claim(JwtRegisteredClaimNames.Jti, jti),
                // RFC 8693 actor claims so the issued token carries who is acting.
                new Claim(ClaimConstants.ActorSubject, actorUserId),
                new Claim(ClaimConstants.ActorTenant, actorTenantId)
            ])

View on GitHub (pinned to 3f2959e683)