fullstackhero/dotnet-starter-kit · warning · CustomException

current session is not an impersonation session

Error message

current session is not an impersonation session

What it means

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.

Solutions

  1. Only call EndImpersonation when the current session actually started via StartImpersonation — track that state client-side.
  2. Treat HTTP 400 from this endpoint as 'already a normal session' and simply continue with the current token instead of surfacing an error.
  3. After a successful EndImpersonation, immediately swap the stored token for the returned original-actor token and disable the end-impersonation action.

Example fix

// before
onMount(() => api.post("/impersonation/end")); // fires even for normal sessions

// after
if (session.isImpersonating) {
  await api.post("/impersonation/end");
  session.isImpersonating = false;
}
Defensive patterns

Strategy: fallback

Validate before calling

if (!session.isImpersonating) return; // nothing to end; skip the call entirely

Type guard

bool isImpersonationToken(string jwt) =>
    new JwtSecurityTokenHandler().ReadJwtToken(jwt).Claims.Any(c => c.Type == "act_sub");

Try / catch

try
{
    await api.post("/impersonation/end");
}
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
{
    // already a normal session: keep current token, clear client impersonation flag
    session.isImpersonating = false;
}

Prevention

When it happens

Trigger: Calling EndImpersonation with a regular (non-impersonation) login token — one with no ClaimConstants.ActorSubject/ActorTenant claims, so actorUserId/actorTenantId come back null or whitespace.

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

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs:66

        ArgumentNullException.ThrowIfNull(request);

        if (!_currentUser.IsAuthenticated())
        {
            throw new UnauthorizedException();
        }

        var claims = _currentUser.GetUserClaims()?.ToList()
            ?? throw new UnauthorizedException();

        var actorUserId = claims.FirstOrDefault(c => c.Type == ClaimConstants.ActorSubject)?.Value;
        var actorTenantId = claims.FirstOrDefault(c => c.Type == ClaimConstants.ActorTenant)?.Value;
        var jti = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti)?.Value;

        if (string.IsNullOrWhiteSpace(actorUserId) || string.IsNullOrWhiteSpace(actorTenantId))
        {
            // Signed in but no act_sub claim (End called on a non-impersonation token): client error,
            // must be 4xx not CustomException's default 500.
            throw new CustomException(
                "current session is not an impersonation session",
                errors: null,
                System.Net.HttpStatusCode.BadRequest);
        }

        var impersonatedUserId = _currentUser.GetUserId().ToString();
        var impersonatedTenantId = _currentUser.GetTenant() ?? string.Empty;

        // Mark grant ended BEFORE issuing actor tokens so a racing JWT-hook request sees "ended" (safer than the reverse).
        // If MarkEnded fails we proceed anyway: the grant expires naturally and the hook treats Unknown states as revoked.
        if (!string.IsNullOrWhiteSpace(jti))
        {
            try
            {
                await _grantService.MarkEndedByJtiAsync(jti, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {

View on GitHub (pinned to 3f2959e683)