fullstackhero/dotnet-starter-kit · error · UnauthorizedException

Tenant context is required.

Error message

Tenant context is required.

What it means

This handler throws UnauthorizedException('Tenant context is required.') when tenantAccessor.MultiTenantContext?.TenantInfo?.Id resolves to null. BillingDbContext is not tenant-filtered, so the handler must resolve the caller's tenant id explicitly to pin reads to that tenant; if Finbuckle has not resolved a tenant for the request, no safe tenant scope exists and the handler refuses to run. It is an application-level guard, not an infrastructure failure.

Solutions

  1. Pass a tenant identifier with the request: add header __tenant__: <tenant-id-or-identifier> (or use the tenant-resolving hostname/route value configured in the app).
  2. Verify Finbuckle multitenancy middleware is registered and ordered before endpoint mapping in the host pipeline.
  3. If calling from a non-HTTP context (job/CLI), set the tenant explicitly (e.g. ITenantContext/TenantInfo scoped service or tenant context accessor) before invoking the handler.
  4. In tests, register/configure a mock ITenantAccessor whose MultiTenantContext.TenantInfo.Id returns a non-null tenant id.

Example fix

// before
curl -H "Authorization: Bearer $TOKEN" https://localhost:7030/api/v1/subscriptions/3fa85f64...
// after
curl -H "Authorization: Bearer $TOKEN" -H "__tenant__: acme" https://localhost:7030/api/v1/subscriptions/3fa85f64...
Defensive patterns

Strategy: try-catch

Validate before calling

var tenantId = request.Headers.TryGetValues("__tenant__", out var v) ? v.FirstOrDefault() : null;
if (string.IsNullOrWhiteSpace(tenantId)) throw new InvalidOperationException("__tenant__ header required before calling GetSubscription");

Type guard

bool HasTenant(ITenantAccessor a) => a.MultiTenantContext?.TenantInfo?.Id is not null;

Try / catch

try
{
    var sub = await api.GetSubscriptionAsync(id);
}
catch (UnauthorizedException ex) when (ex.Message == "Tenant context is required.")
{
    logger.LogWarning(ex, "No tenant context resolved; re-issue with __tenant__ header");
    // re-dispatch with tenant header or surface a 401 to the caller
}

Prevention

When it happens

Trigger: Calling GET subscription endpoint without a tenant identifier resolvable by Finbuckle — e.g. request missing the __tenant__ header/route/query value, hostname not mapped to a tenant in TenantStore, or the call made from a background/Hangfire/CLI context with no TenantContext set.

Common situations: Testing the endpoint with curl/Postman omitting the tenant header; a reverse proxy stripping the tenant host header; calling the API as root/admin without impersonating a tenant; integration tests that build the handler manually without seeding ITenantInfo; middleware ordering where multitenancy middleware runs after the endpoint pipeline.

Related errors


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

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Subscriptions/GetSubscription/GetSubscriptionQueryHandler.cs:22

using FSH.Modules.Billing.Contracts.Dtos;
using FSH.Modules.Billing.Contracts.v1.Subscriptions;
using FSH.Modules.Billing.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace FSH.Modules.Billing.Features.v1.Subscriptions.GetSubscription;

public sealed class GetSubscriptionQueryHandler(
    BillingDbContext dbContext,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : IQueryHandler<GetSubscriptionQuery, SubscriptionDto?>
{
    public async ValueTask<SubscriptionDto?> Handle(GetSubscriptionQuery query, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(query);

        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");

        // BillingDbContext is not tenant-filtered, so a tenant caller is pinned to its OWN
        // subscription and only root may pass an arbitrary tenant id (else cross-tenant reads).
        var tenantId = callerTenantId == MultitenancyConstants.Root.Id
            ? query.TenantId ?? callerTenantId
            : callerTenantId;

        var sub = await (from s in dbContext.Subscriptions.AsNoTracking()
                         join p in dbContext.Plans.AsNoTracking() on s.PlanId equals p.Id
                         where s.TenantId == tenantId
                            && s.Status == Contracts.SubscriptionStatus.Active
                         select new SubscriptionDto(s.Id, s.TenantId, s.PlanId, p.Key, s.StartUtc, s.EndUtc, s.Status))
                        .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
        return sub;
    }
}

View on GitHub (pinned to 3f2959e683)