{"record":{"id":"c6cbf33f5a9997d4","repo":"fullstackhero/dotnet-starter-kit","slug":"unauthorized-revokeimpersonationgrantcommandhandler","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs","lineNumber":29,"sourceCode":"namespace FSH.Modules.Identity.Features.v1.Impersonation.RevokeImpersonationGrant;\n\npublic sealed class RevokeImpersonationGrantCommandHandler(\n    IImpersonationGrantService grantService,\n    ICurrentUser currentUser,\n    ISecurityAudit securityAudit,\n    IRequestContext requestContext,\n    ILogger<RevokeImpersonationGrantCommandHandler> logger)\n    : ICommandHandler<RevokeImpersonationGrantCommand, ImpersonationGrantDto>\n{\n    public async ValueTask<ImpersonationGrantDto> Handle(\n        RevokeImpersonationGrantCommand request,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        if (!currentUser.IsAuthenticated())\n        {\n            throw new UnauthorizedException();\n        }\n\n        var callerUserId = currentUser.GetUserId().ToString();\n        var callerTenantId = currentUser.GetTenant()\n            ?? throw new UnauthorizedException(\"missing tenant context\");\n        var isRoot = string.Equals(callerTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal);\n\n        // Enforce visibility before revoking: tenant admins may only revoke grants in their own\n        // tenant. Cross-tenant grants return 404 (not 403) so existence isn't confirmed out of scope.\n        var grant = await grantService.GetByIdAsync(request.GrantId, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException(\"impersonation grant not found\");\n\n        var withinTenant = string.Equals(grant.ImpersonatedTenantId, callerTenantId, StringComparison.Ordinal)\n            || string.Equals(grant.ActorTenantId, callerTenantId, StringComparison.Ordinal);\n\n        if (!isRoot && !withinTenant)\n        {\n            throw new NotFoundException(\"impersonation grant not found\");","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs#L11-L47","documentation":"RevokeImpersonationGrantCommandHandler.Handle rejects unauthenticated callers: if currentUser.IsAuthenticated() is false it throws UnauthorizedException. Revoking an impersonation grant is a privileged operation, so the caller must present a valid authenticated principal (which is then checked for tenant context and root/tenant-admin scope).","triggerScenarios":"Calling the RevokeImpersonationGrant endpoint with no token, an expired/invalid JWT, or a request missing the Authorization header entirely.","commonSituations":"Session expired while an admin was managing grants list; frontend sent the revoke call without attaching the bearer token; token invalidated by signing-key rotation or by a prior logout that revoked the session.","solutions":["Re-authenticate and retry the revoke with a fresh token.","Ensure the HTTP client attaches the Authorization: Bearer header for this call (check interceptors).","Handle 401 globally by redirecting to login, then re-running the pending revoke action.","Verify JWT validation parameters (clock skew, issuer, audience, keys) if seemingly valid tokens are rejected."],"exampleFix":"// before\nawait fetch(`/api/impersonation/grants/${grantId}`, { method: \"DELETE\" }); // no auth header\n\n// after\nawait apiFetch(`/api/impersonation/grants/${grantId}`, {\n  method: \"DELETE\",\n  headers: { Authorization: `Bearer ${auth.getToken()}` },\n});","handlingStrategy":"try-catch","validationCode":"if (!auth.isAuthenticated() || auth.isTokenExpired()) {\n    await auth.refresh(); // ensure a live token before revoking grants\n    return;\n}","typeGuard":"bool canRevoke(AuthState s) => s is { IsAuthenticated: true };","tryCatchPattern":"try\n{\n    await api.delete(`/impersonation/grants/${grantId}`);\n}\ncatch (UnauthorizedException)\n{\n    await auth.loginThen(() => api.delete(`/impersonation/grants/${grantId}`));\n}","preventionTips":["Use a shared API client that always injects the bearer token.","Implement a global 401 interceptor that refreshes the token and replays the request once.","Check token expiry before privileged admin operations instead of after failure."],"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"}