fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

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).

Solutions

  1. Re-authenticate and retry the revoke with a fresh token.
  2. Ensure the HTTP client attaches the Authorization: Bearer header for this call (check interceptors).
  3. Handle 401 globally by redirecting to login, then re-running the pending revoke action.
  4. Verify JWT validation parameters (clock skew, issuer, audience, keys) if seemingly valid tokens are rejected.

Example fix

// before
await fetch(`/api/impersonation/grants/${grantId}`, { method: "DELETE" }); // no auth header

// after
await apiFetch(`/api/impersonation/grants/${grantId}`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${auth.getToken()}` },
});
Defensive patterns

Strategy: try-catch

Validate before calling

if (!auth.isAuthenticated() || auth.isTokenExpired()) {
    await auth.refresh(); // ensure a live token before revoking grants
    return;
}

Type guard

bool canRevoke(AuthState s) => s is { IsAuthenticated: true };

Try / catch

try
{
    await api.delete(`/impersonation/grants/${grantId}`);
}
catch (UnauthorizedException)
{
    await auth.loginThen(() => api.delete(`/impersonation/grants/${grantId}`));
}

Prevention

When it happens

Trigger: Calling the RevokeImpersonationGrant endpoint with no token, an expired/invalid JWT, or a request missing the Authorization header entirely.

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

Understand the failure class

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs:29

namespace FSH.Modules.Identity.Features.v1.Impersonation.RevokeImpersonationGrant;

public sealed class RevokeImpersonationGrantCommandHandler(
    IImpersonationGrantService grantService,
    ICurrentUser currentUser,
    ISecurityAudit securityAudit,
    IRequestContext requestContext,
    ILogger<RevokeImpersonationGrantCommandHandler> logger)
    : ICommandHandler<RevokeImpersonationGrantCommand, ImpersonationGrantDto>
{
    public async ValueTask<ImpersonationGrantDto> Handle(
        RevokeImpersonationGrantCommand request,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(request);

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

        var callerUserId = currentUser.GetUserId().ToString();
        var callerTenantId = currentUser.GetTenant()
            ?? throw new UnauthorizedException("missing tenant context");
        var isRoot = string.Equals(callerTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal);

        // Enforce visibility before revoking: tenant admins may only revoke grants in their own
        // tenant. Cross-tenant grants return 404 (not 403) so existence isn't confirmed out of scope.
        var grant = await grantService.GetByIdAsync(request.GrantId, cancellationToken).ConfigureAwait(false)
            ?? throw new NotFoundException("impersonation grant not found");

        var withinTenant = string.Equals(grant.ImpersonatedTenantId, callerTenantId, StringComparison.Ordinal)
            || string.Equals(grant.ActorTenantId, callerTenantId, StringComparison.Ordinal);

        if (!isRoot && !withinTenant)
        {
            throw new NotFoundException("impersonation grant not found");

View on GitHub (pinned to 3f2959e683)