fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

StartImpersonationCommandHandler.Handle throws UnauthorizedException with no message when _currentUser.IsAuthenticated() is false. Impersonation requires an authenticated actor; the exception maps to HTTP 401.

Solutions

  1. Authenticate and obtain a fresh JWT before calling the endpoint
  2. Attach the Authorization: Bearer header to the request
  3. Refresh the token if expired, then retry

Example fix

// before
await fetch('/api/v1/impersonation/start', { method: 'POST' });
// after
await fetch('/api/v1/impersonation/start', { method: 'POST', headers: { Authorization: `Bearer ${token}` } });
Defensive patterns

Strategy: validation

Validate before calling

if (!token || isTokenExpired(token)) throw new Error('authenticate before starting impersonation');

Try / catch

try { await api.startImpersonation(req); }
catch (e) { if (e.status === 401) { await auth.login(); return retryOnce(); } throw e; }

Prevention

When it happens

Trigger: Calling the start-impersonation endpoint without a JWT, with an expired token, or with a malformed Authorization header.

Common situations: Access token expired between page load and the request; missing Authorization header in a service-to-service call; API hit before login flow completed in dev tools/Playwright tests.

Understand the failure class

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs:56

        _identityService = identityService;
        _tokenService = tokenService;
        _securityAudit = securityAudit;
        _currentUser = currentUser;
        _requestContext = requestContext;
        _grantService = grantService;
        _timeProvider = timeProvider;
        _logger = logger;
    }

    public async ValueTask<ImpersonationResponse> Handle(
        StartImpersonationCommand request,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(request);

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

        var actorUserId = _currentUser.GetUserId().ToString();
        var actorTenantId = _currentUser.GetTenant()
            ?? throw new UnauthorizedException("missing tenant context");
        var actorUserName = _currentUser.Name;

        // Cross-tenant impersonation requires the actor to be in the root tenant. Tenant admins
        // can only impersonate users within their own tenant.
        if (!string.Equals(actorTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal)
            && !string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))
        {
            throw new ForbiddenException("cross-tenant impersonation is restricted to platform operators");
        }

        // Prevent self-impersonation (pointless, confuses the audit trail). Caller error → explicit 4xx,
        // not the 500 CustomException defaults to.
        if (string.Equals(actorUserId, request.TargetUserId, StringComparison.Ordinal)

View on GitHub (pinned to 3f2959e683)