fullstackhero/dotnet-starter-kit · error · UnauthorizedException

missing tenant context

Error message

missing tenant context

What it means

GetImpersonationGrantsQueryHandler resolves the caller's tenant via currentUser.GetTenant() and throws UnauthorizedException("missing tenant context") when it returns null. The query is tenant-scoped (root operators see any tenant's grants; tenant admins only their own), so without a tenant claim the request cannot be scoped and is rejected.

Solutions

  1. Authenticate with a normal tenant-aware JWT that includes the tenant claim (issued via the standard login/identity flow).
  2. If using a test token, add the tenant claim (matching MultitenancyConstants.Root.Id for root operators) to the claims set.
  3. Verify the Finbuckle tenant middleware is resolving/normalizing the tenant so ICurrentUser.GetTenant() is populated for the request.

Example fix

// before
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId) }; // no tenant claim

// after
var claims = new List<Claim>
{
    new(ClaimTypes.NameIdentifier, userId),
    new("tenant", tenantId), // required for tenant-scoped queries
};
Defensive patterns

Strategy: validation

Validate before calling

var tenant = auth.getClaim("tenant");
if (string.IsNullOrEmpty(tenant)) {
    throw new InvalidOperationException("Token lacks tenant claim; re-authenticate via the tenant-aware login flow");
}

Type guard

bool hasTenantContext(ClaimsPrincipal p) =>
    p.FindFirst("tenant")?.Value is { Length: > 0 };

Try / catch

try
{
    var grants = await api.get("/impersonation/grants");
}
catch (UnauthorizedException)
{
    // missing tenant context: re-authenticate with a tenant-scoped token
    await auth.login({ tenantId });
}

Prevention

When it happens

Trigger: Calling the GetImpersonationGrants endpoint with an authenticated token that lacks the tenant claim — e.g. a token issued outside the multitenant pipeline, a service token, or a hand-crafted test token without the tenant identifier.

Common situations: Testing with a minimal JWT missing the tenant claim; tokens issued by a legacy auth path predating multitenancy claims; API keys/service accounts that never carry tenant context.

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/GetImpersonationGrants/GetImpersonationGrantsQueryHandler.cs:23

using FSH.Modules.Identity.Contracts.v1.Impersonation;
using FSH.Modules.Identity.Contracts.v1.Impersonation.GetImpersonationGrants;
using Mediator;

namespace FSH.Modules.Identity.Features.v1.Impersonation.GetImpersonationGrants;

public sealed class GetImpersonationGrantsQueryHandler(
    IImpersonationGrantService grantService,
    ICurrentUser currentUser)
    : IQueryHandler<GetImpersonationGrantsQuery, IReadOnlyList<ImpersonationGrantDto>>
{
    public async ValueTask<IReadOnlyList<ImpersonationGrantDto>> Handle(
        GetImpersonationGrantsQuery request,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(request);

        var callerTenant = currentUser.GetTenant()
            ?? throw new UnauthorizedException("missing tenant context");
        var isRoot = string.Equals(callerTenant, MultitenancyConstants.Root.Id, StringComparison.Ordinal);

        // Tenant scoping: root operators target any tenant; tenant admins are locked to their
        // own regardless of input. Mirrors the StartImpersonation cross-tenant rule.
        var tenantFilter = isRoot ? request.ImpersonatedTenantId : callerTenant;

        return await grantService.ListAsync(
            status: request.Status,
            impersonatedTenantId: tenantFilter,
            actorUserId: request.ActorUserId,
            take: request.Take,
            ct: cancellationToken).ConfigureAwait(false);
    }
}

View on GitHub (pinned to 3f2959e683)