fullstackhero/dotnet-starter-kit · error · UnauthorizedException

invalid tenant

Error message

invalid tenant

What it means

EnsureValidTenant throws UnauthorizedException('invalid tenant') when the Finbuckle MultiTenantContext has no TenantInfo.Id for the current context. It guards tenant-scoped existence checks and image URL updates so they never execute outside a tenant scope, protecting tenant isolation.

Solutions

  1. Pass tenant resolution data on every request (tenant header, __tenant__ parameter, or mapped host).
  2. In background/non-HTTP contexts, set the multitenant context explicitly before calling the service.
  3. Verify Finbuckle middleware registration/order and the tenant store contents.
  4. Configure a default tenant if the deployment is effectively single-tenant.

Example fix

// before (job code, no tenant context)
await userProfileService.ExistsWithEmailAsync(email);
// after
using (multiTenantContextAccessor.MultiTenantContext.SetTenantInfo(
    new TenantInfo("tenant-1", "tenant-1", null), replace: false))
{
    await userProfileService.ExistsWithEmailAsync(email);
}
Defensive patterns

Strategy: validation

Validate before calling

const tenantId = headers['X-Tenant-Id'] ?? new URLSearchParams(location.search).get('__tenant__');
if (!tenantId) throw new Error('No tenant resolved; call would throw "invalid tenant"');

Try / catch

try { await existsService.ExistsWithEmailAsync(email); }
catch (e) { if (e.status === 401 && e.message.includes('invalid tenant')) { resolveTenantThenRetry(); } else { throw e; } }

Prevention

When it happens

Trigger: Calling SetImageUrlAsync, ExistsWithEmailAsync, ExistsWithNameAsync, or ExistsWithPhoneNumberAsync from a context with no resolved tenant (background job, migration host, or request lacking tenant identifiers).

Common situations: Calling these services from Hangfire jobs or startup code where the tenant resolver never ran; scripts hitting endpoints without tenant headers; Finbuckle store missing the host mapping; middleware ordering issues.

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/UserProfileService.cs:160

    }

    public async Task<bool> ExistsWithNameAsync(string name, CancellationToken cancellationToken = default)
    {
        EnsureValidTenant();
        return await userManager.FindByNameAsync(name) is not null;
    }

    public async Task<bool> ExistsWithPhoneNumberAsync(string phoneNumber, string? exceptId = null, CancellationToken cancellationToken = default)
    {
        EnsureValidTenant();
        return await userManager.Users.FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber, cancellationToken) is FshUser user && user.Id != exceptId;
    }

    private void EnsureValidTenant()
    {
        if (string.IsNullOrWhiteSpace(multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id))
        {
            throw new UnauthorizedException("invalid tenant");
        }
    }

    private string? ResolveImageUrl(Uri? imageUrl)
    {
        if (imageUrl is null)
        {
            return null;
        }

        // Absolute URLs (e.g., S3) pass through unchanged.
        if (imageUrl.IsAbsoluteUri)
        {
            return imageUrl.ToString();
        }

        // For relative paths from local storage, prefix with the API origin and wwwroot.
        if (_originUrl is null)

View on GitHub (pinned to 3f2959e683)