fullstackhero/dotnet-starter-kit · error · UnauthorizedException
Tenant context is required.
Error message
Tenant context is required.
What it means
AssignSubscriptionCommandHandler resolves the caller's tenant before assigning: only root may target an arbitrary command.TenantId; a tenant caller is pinned to its own tenant. If TenantInfo is null (no tenant context resolved), it throws UnauthorizedException("Tenant context is required.") so a caller can neither assign nor cancel a subscription anonymously/unscoped.
Solutions
- Provide a tenant identifier with the request (X-Tenant-Id or __tenant__) so TenantInfo resolves.
- If the caller should target another tenant, ensure it authenticates as root (MultitenancyConstants.Root.Id); non-root callers are pinned to their own tenant and command.TenantId is ignored.
- For automation, execute within an explicit tenant scope or use a root-scoped background path.
- Verify multitenancy middleware and strategies are registered and the tenant exists in the store.
Example fix
// before: unscoped service call
POST /subscriptions/assign { "tenantId": "acme", "planKey": "pro" }
// after: send root token + tenant header
apiFetch('/subscriptions/assign', { method: 'POST', headers: { 'X-Tenant-Id': 'acme' }, body }); Defensive patterns
Strategy: validation
Validate before calling
if (!tenantId) throw new Error('Tenant context required for subscription assignment; set X-Tenant-Id or __tenant__');
// non-root callers: target tenant must equal caller tenant (command.tenantId is ignored) Type guard
function hasTenant(t) { return typeof t === 'string' && t.length > 0; } Try / catch
try { await assignSubscription(payload); }
catch (e) { if (isUnauthorized(e)) { redirectToTenantSelection(); } throw e; } Prevention
- Bind service/automation tokens to a tenant or run them as root with an explicit tenant header.
- Only root may target arbitrary tenants; tenant callers should omit or match their own tenantId.
- Verify the assignment request goes through the tenant middleware (standard API route), not a bypass.
When it happens
Trigger: POST /subscriptions/assign (v1) without a resolvable tenant context — missing tenant token/header or host mapping, request outside the multitenant pipeline, or a system/scheduler call with no tenant scope.
Common situations: An onboarding script calling the assign endpoint with a service token not bound to any tenant; root automation running before Finbuckle middleware is hit (direct handler invocation); reverse proxy stripping the header the tenant strategy keys on.
Related errors
- Tenant context is required.
- Only the root operator may generate invoices across tenants.
- Tenant context is required.
- Tenant context is required.
- Tenant context is required.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/2f4c6f5003571b60.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Subscriptions/AssignSubscription/AssignSubscriptionCommandHandler.cs:24
using FSH.Modules.Billing.Domain;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Billing.Features.v1.Subscriptions.AssignSubscription;
public sealed class AssignSubscriptionCommandHandler(
BillingDbContext dbContext,
IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
: ICommandHandler<AssignSubscriptionCommand, Guid>
{
public async ValueTask<Guid> Handle(AssignSubscriptionCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
// Only root may target an arbitrary tenant; a tenant caller is pinned to its own, so it can't
// (re)assign or cancel another tenant's subscription via a foreign tenant id in the body.
var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
?? throw new UnauthorizedException("Tenant context is required.");
var isRoot = callerTenantId == MultitenancyConstants.Root.Id;
var targetTenantId = isRoot ? command.TenantId : callerTenantId;
#pragma warning disable CA1308 // Plan keys are canonical lowercase slugs
var key = command.PlanKey.ToLowerInvariant();
#pragma warning restore CA1308
var plan = await dbContext.Plans.FirstOrDefaultAsync(p => p.Key == key && p.IsActive, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException($"Active plan with key '{command.PlanKey}' not found.");
var now = DateTime.UtcNow;
var current = await dbContext.Subscriptions
.FirstOrDefaultAsync(s => s.TenantId == targetTenantId && s.Status == Contracts.SubscriptionStatus.Active, cancellationToken)
.ConfigureAwait(false);
current?.Cancel(now);
var subscription = Subscription.Create(targetTenantId, plan.Id, now);
dbContext.Subscriptions.Add(subscription);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);View on GitHub (pinned to 3f2959e683)