fullstackhero/dotnet-starter-kit · error · UnauthorizedException

missing tenant context

Error message

missing tenant context

What it means

After confirming authentication, the start-impersonation handler requires the actor's tenant claim; _currentUser.GetTenant() returning null triggers UnauthorizedException('missing tenant context') (HTTP 401). Tenant scoping is required to enforce the cross-tenant rules that follow.

Solutions

  1. Re-issue the token through the standard login flow so tenant claims are included
  2. Verify Finbuckle multitenancy registration and the tenant claim constant still match
  3. Assign the calling service/user to a tenant

Example fix

// before
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId) };
// after
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId), new("tenant", tenantId) };
Defensive patterns

Strategy: validation

Validate before calling

const tenant = claims.find(c => c.type === 'tenant')?.value;
if (!tenant) throw new Error('no tenant claim on token; re-login through the standard flow');

Try / catch

try { await api.startImpersonation(req); }
catch (e) { if (e.status === 401 && /tenant context/.test(e.message)) { await reauthWithTenant(); return; } throw e; }

Prevention

When it happens

Trigger: An authenticated token without the tenant claim is used to start impersonation — e.g. a token issued by a custom auth endpoint that skips tenant claim generation, or a request bypassing the tenant resolver.

Common situations: Manually minted dev tokens lacking tenant claims; service accounts provisioned without tenant membership; Finbuckle tenant strategy changed so the claim name no longer matches what GetTenant reads.

Related errors


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

Appendix: source

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

        _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)
            && string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))
        {
            throw new CustomException("cannot impersonate yourself", errors: null, System.Net.HttpStatusCode.BadRequest);
        }

View on GitHub (pinned to 3f2959e683)