fullstackhero/dotnet-starter-kit · error · UnauthorizedException

tenant is deactivated

Error message

tenant {tenant.Id} is deactivated

What it means

ValidateTenantStatus throws UnauthorizedException with the tenant id in the message when the tenant record's IsActive flag is false. Root tenant is exempt. Called during credential login and refresh validation, a deactivated tenant blocks all of its users from obtaining or renewing tokens.

Solutions

  1. Re-enable the tenant: set IsActive=true on the tenant record (admin endpoint or tenants table).
  2. If deactivation is billing-related, resolve the subscription and re-activate; note the separate grace-period expiry throws 'validity has expired' instead.
  3. Verify the client is sending the intended tenant identifier — a typo may target a disabled tenant.

Example fix

// before
UPDATE tenants SET is_active = false WHERE id = 'acme';
// after (after renewal confirmed)
UPDATE tenants SET is_active = true WHERE id = 'acme';
Defensive patterns

Strategy: try-catch

Validate before calling

// when the API exposes tenant info, check before authenticating
const tenant = await api.get(`/api/tenants/${tenantId}`);
if (tenant && tenant.isActive === false) { showTenantDisabledScreen(); return; }

Type guard

function isActiveTenant(t: { id: string; isActive: boolean } | null | undefined): t is { id: string; isActive: true } {
  return !!t && t.isActive === true && t.id.length > 0;
}

Try / catch

catch (ApiError e) when (e.StatusCode === 401 && /tenant .* is deactivated/.test(e.Message)) {
  showMessage('Your workspace is deactivated. Contact your account manager to reactivate.');
  haltAuthRetryLoop();
}

Prevention

When it happens

Trigger: Any login/refresh request carrying a tenant header whose tenant row has IsActive=false — usually after billing-driven deactivation, an admin disabling the tenant, or upgrade/seed restoring IsActive incorrectly.

Common situations: Subscription lapsed and tenant was deactivated pending renewal; SaaS operator disabled a tenant for abuse/maintenance; client config pointing at the wrong tenant that happens to be disabled; restored database where IsActive flags were reset.

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


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/IdentityService.cs:286

            throw new UnauthorizedException("user is deactivated");
        }

        if (!user.EmailConfirmed)
        {
            throw new UnauthorizedException("email not confirmed");
        }
    }

    private void ValidateTenantStatus(AppTenantInfo tenant)
    {
        if (tenant.Id == MultitenancyConstants.Root.Id)
        {
            return;
        }

        if (!tenant.IsActive)
        {
            throw new UnauthorizedException($"tenant {tenant.Id} is deactivated");
        }

        // Honor the billing grace period: a lapsed tenant can still authenticate until
        // ValidUpto + grace (matching the request-time guard in MultitenancyModule).
        if (_timeProvider.GetUtcNow().UtcDateTime > tenant.ValidUpto.AddDays(_gracePeriodDays))
        {
            throw new UnauthorizedException($"tenant {tenant.Id} validity has expired");
        }
    }

    private async Task<List<Claim>> BuildUserClaimsAsync(FshUser user, string tenantId, CancellationToken ct)
    {
        var claims = CreateBasicClaims(user, tenantId);
        await AddRoleClaimsAsync(claims, user, ct);
        return claims;
    }

    private static List<Claim> CreateBasicClaims(FshUser user, string tenantId)

View on GitHub (pinned to 3f2959e683)