{"record":{"id":"d05f1d9bf5f6d89d","repo":"fullstackhero/dotnet-starter-kit","slug":"unauthorized","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs","lineNumber":52,"sourceCode":"    {\n        _identityService = identityService;\n        _tokenService = tokenService;\n        _securityAudit = securityAudit;\n        _currentUser = currentUser;\n        _requestContext = requestContext;\n        _grantService = grantService;\n        _logger = logger;\n    }\n\n    public async ValueTask<TokenResponse> Handle(\n        EndImpersonationCommand request,\n        CancellationToken cancellationToken)\n    {\n        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        }","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/EndImpersonation/EndImpersonationCommandHandler.cs#L34-L70","documentation":"EndImpersonationCommandHandler.Handle first checks _currentUser.IsAuthenticated() and throws UnauthorizedException when the caller has no valid principal. Ending impersonation requires an authenticated user because the handler must read the actor claims (act_sub, act_tenant) from the current token to mint the original actor's new token.","triggerScenarios":"Calling the EndImpersonation endpoint with no JWT, an expired token, or an otherwise invalid/unrecognized authentication scheme so ICurrentUser.IsAuthenticated() returns false.","commonSituations":"The impersonation/access token expired while impersonating; the client never attached the Authorization header; the token was issued before a key/signing-credential rotation and now fails validation.","solutions":["Re-authenticate to obtain a fresh token (if possible via the original login flow) before calling EndImpersonation.","Ensure the Authorization: Bearer header is actually sent on the EndImpersonation request.","If tokens expire quickly, end impersonation proactively before expiry or handle 401 by falling back to a fresh login.","Check JWT validation settings (issuer/audience/signing key) if valid tokens are being rejected."],"exampleFix":"// before\nawait api.PostAsync(\"/impersonation/end\"); // 401: token expired\n\n// after\nif (auth.isTokenExpired()) {\n    await auth.loginAgain(); // cannot silently end impersonation without a valid session\n    return;\n}\nawait api.post(\"/impersonation/end\");","handlingStrategy":"try-catch","validationCode":"if (!auth.isAuthenticated() || auth.isTokenExpired()) {\n    await auth.refreshOrLogin();\n    return; // only call EndImpersonation with a live session\n}","typeGuard":"bool canEndImpersonation(AuthState s) => s is { IsAuthenticated: true, AccessToken.Length: > 0 };","tryCatchPattern":"try\n{\n    await api.post(\"/impersonation/end\");\n}\ncatch (UnauthorizedException)\n{\n    await auth.login(); // session gone; recover via fresh login\n}","preventionTips":["Attach the Authorization header via a central HTTP interceptor.","Refresh tokens proactively before they expire during long impersonation sessions.","Treat any 401 as 'go to login', not as an application bug."],"tags":["auth","impersonation","unauthorized"],"backgroundTag":"authentication-required","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"}