{"record":{"id":"148531a765aebd37","repo":"fullstackhero/dotnet-starter-kit","slug":"current-session-is-not-an-impersonation-session","errorCode":null,"errorMessage":"current session is not an impersonation session","messagePattern":"current session is not an impersonation session","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"warning","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs","lineNumber":66,"sourceCode":"        ArgumentNullException.ThrowIfNull(request);\n\n        if (!_currentUser.IsAuthenticated())\n        {\n            throw new UnauthorizedException();\n        }\n\n        var claims = _currentUser.GetUserClaims()?.ToList()\n            ?? throw new UnauthorizedException();\n\n        var actorUserId = claims.FirstOrDefault(c => c.Type == ClaimConstants.ActorSubject)?.Value;\n        var actorTenantId = claims.FirstOrDefault(c => c.Type == ClaimConstants.ActorTenant)?.Value;\n        var jti = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti)?.Value;\n\n        if (string.IsNullOrWhiteSpace(actorUserId) || string.IsNullOrWhiteSpace(actorTenantId))\n        {\n            // Signed in but no act_sub claim (End called on a non-impersonation token): client error,\n            // must be 4xx not CustomException's default 500.\n            throw new CustomException(\n                \"current session is not an impersonation session\",\n                errors: null,\n                System.Net.HttpStatusCode.BadRequest);\n        }\n\n        var impersonatedUserId = _currentUser.GetUserId().ToString();\n        var impersonatedTenantId = _currentUser.GetTenant() ?? string.Empty;\n\n        // Mark grant ended BEFORE issuing actor tokens so a racing JWT-hook request sees \"ended\" (safer than the reverse).\n        // If MarkEnded fails we proceed anyway: the grant expires naturally and the hook treats Unknown states as revoked.\n        if (!string.IsNullOrWhiteSpace(jti))\n        {\n            try\n            {\n                await _grantService.MarkEndedByJtiAsync(jti, cancellationToken).ConfigureAwait(false);\n            }\n            catch (Exception ex)\n            {","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs#L48-L84","documentation":"EndImpersonation requires actor claims (act_sub/act_tenant) present only on tokens minted by StartImpersonation. If the authenticated caller's token lacks them, the handler throws CustomException with HTTP 400 BadRequest — the session is valid but it is not an impersonation session, so there is nothing to end.","triggerScenarios":"Calling EndImpersonation with a regular (non-impersonation) login token — one with no ClaimConstants.ActorSubject/ActorTenant claims, so actorUserId/actorTenantId come back null or whitespace.","commonSituations":"Double-clicking an 'exit impersonation' button after the first call already restored the original token; a UI bug that calls End on page load with the normal session; replaying a captured End request after impersonation already ended.","solutions":["Only call EndImpersonation when the current session actually started via StartImpersonation — track that state client-side.","Treat HTTP 400 from this endpoint as 'already a normal session' and simply continue with the current token instead of surfacing an error.","After a successful EndImpersonation, immediately swap the stored token for the returned original-actor token and disable the end-impersonation action."],"exampleFix":"// before\nonMount(() => api.post(\"/impersonation/end\")); // fires even for normal sessions\n\n// after\nif (session.isImpersonating) {\n  await api.post(\"/impersonation/end\");\n  session.isImpersonating = false;\n}","handlingStrategy":"fallback","validationCode":"if (!session.isImpersonating) return; // nothing to end; skip the call entirely","typeGuard":"bool isImpersonationToken(string jwt) =>\n    new JwtSecurityTokenHandler().ReadJwtToken(jwt).Claims.Any(c => c.Type == \"act_sub\");","tryCatchPattern":"try\n{\n    await api.post(\"/impersonation/end\");\n}\ncatch (CustomException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)\n{\n    // already a normal session: keep current token, clear client impersonation flag\n    session.isImpersonating = false;\n}","preventionTips":["Track impersonation state client-side and only show/call End when active.","After ending impersonation, immediately swap the token and set the flag false.","Ignore (don't rethrow) 400 from EndImpersonation — it means the session was already normal."],"tags":["impersonation","bad-request","session"],"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"}