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
- Authenticate and obtain a fresh JWT before calling the endpoint
- Attach the Authorization: Bearer header to the request
- 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
- Attach the Authorization header in a shared apiFetch wrapper
- Refresh tokens proactively before they expire
- In tests, perform login in a setup hook before impersonation calls
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
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)