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
- 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).
- Verify the Finbuckle multitenancy middleware is registered and ordered before the endpoint executes.
- Confirm the tenant exists in the tenant store and its host/identifier pattern matches the incoming request.
- 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
- Always attach tenant identifiers (header/query/host) to auth-related requests.
- Test password flows from scripts/curl, not just the browser app.
- Verify Finbuckle middleware order after upgrading packages.
- Log TenantInfo.Id at request start in staging to catch resolution gaps early.
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)