fullstackhero/dotnet-starter-kit · error · ForbiddenException
cross-tenant impersonation is restricted to platform…
Error message
cross-tenant impersonation is restricted to platform operators
What it means
The handler allows impersonation only within the actor's own tenant, unless the actor belongs to the root (platform) tenant. Any other cross-tenant attempt throws ForbiddenException('cross-tenant impersonation is restricted to platform operators'), mapping to HTTP 403.
Solutions
- Use an account in the root tenant (platform operator) for cross-tenant impersonation
- Target users within your own tenant instead
- Ensure TargetTenantId exactly matches the actor's tenant (ordinal, no whitespace)
Example fix
// before
await api.startImpersonation({ targetUserId, targetTenantId: otherTenantId }); // 403
// after
const targetTenantId = myTenantId; // stay in-tenant, or use a root-tenant operator token
await api.startImpersonation({ targetUserId, targetTenantId }); Defensive patterns
Strategy: validation
Validate before calling
const isRoot = myTenantId === rootTenantId;
if (!isRoot && targetTenantId !== myTenantId) {
throw new Error('cross-tenant impersonation requires a platform operator (root tenant) account');
} Try / catch
try { await api.startImpersonation(req); }
catch (e) { if (e.status === 403) { notify('Only platform operators may impersonate across tenants'); return; } throw e; } Prevention
- Gate cross-tenant impersonation UI behind operator role checks
- Normalize/trim tenant ids before comparing (API compares ordinally)
- Document that tenant admins are limited to their own tenant
When it happens
Trigger: A tenant admin of tenant A calls start-impersonation with TargetTenantId = tenant B while the actor's tenant is neither root nor B.
Common situations: Support staff trying to help a user in another tenant without platform-operator credentials; tests using a regular tenant token expecting cross-tenant access; tenant id case/whitespace mismatch making the equality check fail unintentionally.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Cross-tenant audit summary requires…
- Cross-tenant audit access requires…
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
- Tenant context is required.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/937b6344e33047a0.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs:69
{
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)
&& string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))
{
throw new CustomException("cannot impersonate yourself", errors: null, System.Net.HttpStatusCode.BadRequest);
}
// Prevent nesting: if the caller is already impersonating, require end-impersonation first.
var callerClaims = _currentUser.GetUserClaims();
if (callerClaims is not null
&& callerClaims.Any(c => c.Type == ClaimConstants.ActorSubject))
{
throw new CustomException(
"end current impersonation before starting a new one",
errors: null,View on GitHub (pinned to 3f2959e683)