{"record":{"id":"cc2d9a92bb740af3","repo":"fullstackhero/dotnet-starter-kit","slug":"end-current-impersonation-before-starting-a-new-one","errorCode":null,"errorMessage":"end current impersonation before starting a new one","messagePattern":"end current impersonation before starting a new one","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"warning","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs","lineNumber":85,"sourceCode":"            && !string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))\n        {\n            throw new ForbiddenException(\"cross-tenant impersonation is restricted to platform operators\");\n        }\n\n        // Prevent self-impersonation (pointless, confuses the audit trail). Caller error → explicit 4xx,\n        // not the 500 CustomException defaults to.\n        if (string.Equals(actorUserId, request.TargetUserId, StringComparison.Ordinal)\n            && string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))\n        {\n            throw new CustomException(\"cannot impersonate yourself\", errors: null, System.Net.HttpStatusCode.BadRequest);\n        }\n\n        // Prevent nesting: if the caller is already impersonating, require end-impersonation first.\n        var callerClaims = _currentUser.GetUserClaims();\n        if (callerClaims is not null\n            && callerClaims.Any(c => c.Type == ClaimConstants.ActorSubject))\n        {\n            throw new CustomException(\n                \"end current impersonation before starting a new one\",\n                errors: null,\n                System.Net.HttpStatusCode.BadRequest);\n        }\n\n        var targetClaimsResult = await _identityService\n            .BuildClaimsForUserAsync(request.TargetUserId, request.TargetTenantId, cancellationToken);\n\n        if (targetClaimsResult is null)\n        {\n            throw new NotFoundException(\"target user not found\");\n        }\n\n        var (subject, claims) = targetClaimsResult.Value;\n        var targetUserName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value\n            ?? claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Name)?.Value;\n\n        // Strip the auto-generated jti from BuildClaimsForUserAsync and inject our own, so the persisted","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs#L67-L103","documentation":"Impersonation sessions cannot nest. If the caller's claims already contain an ActorSubject claim — meaning they are currently impersonating someone — the handler throws CustomException('end current impersonation before starting a new one', BadRequest).","triggerScenarios":"Calling start-impersonation while already holding an active impersonation session (ActorSubject claim present in the JWT).","commonSituations":"A support agent forgets to end the current session and starts another; a SPA holding a stale impersonated token retries the start call; automation loops issuing start without stop.","solutions":["Call the end-impersonation endpoint first, then start the new session","Use the original (pre-impersonation) token when starting a new impersonation","Make the client guard: only show 'start impersonation' when not currently impersonating"],"exampleFix":"// before\nawait api.startImpersonation(target); // 400 while impersonating\n// after\nif (isImpersonating) await api.endImpersonation();\nawait api.startImpersonation(target);","handlingStrategy":"validation","validationCode":"if (claims.some(c => c.type === 'actor_subject' || c.type === 'ActorSubject')) {\n  throw new Error('already impersonating; end the current session first');\n}","typeGuard":"const isImpersonating = (claims: {type: string}[]) => claims.some(c => c.type.toLowerCase().includes('actorsubject'));","tryCatchPattern":"try { await api.startImpersonation(req); }\ncatch (e) { if (e.status === 400 && /end current impersonation/.test(e.message)) { await api.endImpersonation(); return retryOnce(); } throw e; }","preventionTips":["Track impersonation state in the client and hide 'start' while active","Always start impersonation from the original token, never the impersonated one","End the session on logout/token refresh","In automation, pair every start with an end in cleanup"],"tags":["impersonation","state","bad-request"],"backgroundTag":"invalid-state-transition","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"}