fullstackhero/dotnet-starter-kit · error · UnauthorizedException

invalid tenant

Error message

invalid tenant

What it means

EnsureValidTenant throws UnauthorizedException('invalid tenant') when the Finbuckle multitenant context has no tenant id resolved for the current request. It guards tenant-scoped password operations so they never run without an explicit tenant scope, which would break user isolation and store lookups. The library treats a missing tenant as an authorization failure rather than a configuration error.

Solutions

  1. Ensure the client sends tenant resolution data with the request (e.g. X-Tenant-Id header, __tenant__ query parameter, or the tenant's mapped host).
  2. Verify the Finbuckle multitenancy middleware is registered and ordered before the endpoint executes.
  3. Confirm the tenant exists in the tenant store and its host/identifier pattern matches the incoming request.
  4. If single-tenant usage is intended, configure a default tenant info so TenantInfo.Id is always set.

Example fix

// before (curl)
curl -X POST https://api.example.com/api/users/forgot-password -d '{"email":"a@b.c"}'
// after
curl -X POST https://api.example.com/api/users/forgot-password -H 'X-Tenant-Id: my-tenant' -d '{"email":"a@b.c"}'
Defensive patterns

Strategy: validation

Validate before calling

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

Try / catch

try { await api.post('/api/users/forgot-password', body); }
catch (e) { if (e.status === 401 && e.title === 'invalid tenant') { fixTenantResolution(); } else { throw e; } }

Prevention

When it happens

Trigger: Calling ForgotPasswordAsync or ResetPasswordAsync on a request that has no resolvable tenant: no __tenant__ query string/form field/route/header, no host mapping, and no default tenant configured.

Common situations: Calling the token/password endpoints from scripts or tools that omit the tenant header; misconfigured multitenancy middleware ordering so the tenant resolver never runs; missing Finbuckle tenant store entries for the requesting host; a base URL/host rename that broke per-tenant host resolution.

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/UserPasswordService.cs:117

        }

        // Raise domain event for password change
        var tenantId = multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id;
        user.RecordPasswordChanged(wasReset: false, tenantId);
        await db.SaveChangesAsync(cancellationToken);

        // Update password expiry date
        await passwordExpiryService.UpdateLastPasswordChangeDateAsync(userId, cancellationToken);

        // Save to history
        await passwordHistoryService.SavePasswordHistoryAsync(userId, cancellationToken);
    }

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

View on GitHub (pinned to 3f2959e683)