{"record":{"id":"2880ce682723ca30","repo":"fullstackhero/dotnet-starter-kit","slug":"original-actor-not-found","errorCode":null,"errorMessage":"original actor not found","messagePattern":"original actor not found","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs","lineNumber":96,"sourceCode":"        {\n            try\n            {\n                await _grantService.MarkEndedByJtiAsync(jti, cancellationToken).ConfigureAwait(false);\n            }\n            catch (Exception ex)\n            {\n                _logger.LogWarning(ex,\n                    \"Failed to mark impersonation grant ended for jti={Jti}. Actor swap will still proceed.\",\n                    jti);\n            }\n        }\n\n        var actorClaimsResult = await _identityService\n            .BuildClaimsForUserAsync(actorUserId, actorTenantId, cancellationToken);\n\n        if (actorClaimsResult is null)\n        {\n            throw new NotFoundException(\"original actor not found\");\n        }\n\n        var (subject, actorClaims) = actorClaimsResult.Value;\n\n        var token = await _tokenService.IssueAsync(subject, actorClaims, actorTenantId, cancellationToken);\n        await _identityService.StoreRefreshTokenAsync(subject, token.RefreshToken, token.RefreshTokenExpiresAt, cancellationToken);\n\n        await _securityAudit.ImpersonationEndedAsync(\n            actorUserId: actorUserId,\n            actorTenantId: actorTenantId,\n            targetUserId: impersonatedUserId,\n            targetTenantId: impersonatedTenantId,\n            clientId: _requestContext.ClientId ?? \"unknown\",\n            ct: cancellationToken);\n\n        if (_logger.IsEnabled(LogLevel.Information))\n        {\n            _logger.LogInformation(","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs#L78-L114","documentation":"After extracting act_sub/act_tenant from the token, the handler calls _identityService.BuildClaimsForUserAsync(actorUserId, actorTenantId) to rebuild the original actor's claims. If that returns null — the original user no longer exists (deleted/disabled) or is otherwise unresolvable — it throws this NotFoundException, and no restoration token is issued.","triggerScenarios":"Ending impersonation when the original actor account was deleted or deactivated between StartImpersonation and EndImpersonation, or when act_sub references a user in a tenant where the identity service cannot resolve them.","commonSituations":"Long-running impersonation sessions during which an admin deleted the operator's account; test/staging tokens referencing users pruned by a cleanup job; tenant data restored from a backup missing the actor.","solutions":["Recover by performing a fresh explicit login as the original user — the old impersonated token cannot be converted without the actor account.","Invalidate outstanding impersonation grants when deleting/deactivating users so clients don't attempt to restore a vanished actor.","Verify the actor's tenant and user ID in the token's act_sub/act_tenant claims match an existing account (check the users table for that tenant)."],"exampleFix":"// before\nawait api.post(\"/impersonation/end\"); // 404: actor deleted while impersonating\n\n// after\ntry {\n  await api.post(\"/impersonation/end\");\n} catch (err) {\n  if (err.status === 404) {\n    await auth.fullLogin(); // fall back to a fresh login\n  }\n}","handlingStrategy":"fallback","validationCode":"// before ending, confirm the actor account still exists (if the API exposes it)\nvar actorOk = await userApi.ExistsAsync(session.actorUserId);\nif (!actorOk) { await auth.fullLogin(); return; }","typeGuard":"bool actorStillExists(UserLookupResult? r) => r is { Exists: true };","tryCatchPattern":"try\n{\n    await api.post(\"/impersonation/end\");\n}\ncatch (NotFoundException)\n{\n    // original actor deleted/disabled: only recovery is a fresh login\n    await auth.fullLogin();\n    notify(\"Your original account could not be restored; please sign in again.\");\n}","preventionTips":["Cap impersonation session duration so actor accounts rarely change mid-session.","Revoke active impersonation grants when deleting or deactivating users.","Handle 404 on end-impersonation with a login redirect, not a raw error dialog."],"tags":["impersonation","not-found","user"],"backgroundTag":"record-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"}