fullstackhero/dotnet-starter-kit · error · UnauthorizedException
missing tenant context
Error message
missing tenant context
What it means
RevokeImpersonationGrantCommandHandler.Handle throws UnauthorizedException('missing tenant context') when the authenticated caller's claims contain no tenant identifier. Impersonation grants are tenant-scoped, so revocation cannot enforce visibility rules without knowing the caller's tenant. The exception maps to HTTP 401.
Solutions
- Issue/refresh the token so it includes the tenant claim, then retry
- Ensure the request reaches the API through the tenant resolution mechanism (header/route/host as configured) so claims include the tenant
- If calling programmatically, set the tenant identifier header expected by the multitenancy config
Example fix
// before
classicHttpClient.DefaultRequestHeaders.Remove("tenant");
// after
client.DefaultRequestHeaders.Add("tenant", tenantId); // ensure tenant is resolved for the JWT/claims Defensive patterns
Strategy: validation
Validate before calling
const tenant = claims.find(c => c.type === 'tenant')?.value;
if (!tenant) throw new Error('token has no tenant claim; re-authenticate'); Type guard
function hasTenantClaim(c: {type: string; value?: string}[]): c is {type: string; value: string}[] {
return !!c.find(x => x.type === 'tenant' && !!x.value);
} Try / catch
try { await api.revokeImpersonationGrant(grantId); }
catch (e) { if (e.status === 401 && e.message.includes('tenant context')) { await reauthenticateWithTenant(tenantId); } else throw e; } Prevention
- Always call the API with tokens issued by the standard login flow
- Include the tenant header/route segment per the multitenancy config in every request
- In service/CLI clients, resolve and send the tenant explicitly
- Add an integration test asserting tokens carry the tenant claim
When it happens
Trigger: Calling POST/DELETE for revoking an impersonation grant with a valid JWT that lacks the tenant claim (e.g. a token issued outside Finbuckle multitenancy resolution, or a hand-crafted/service token without tenant info).
Common situations: Tokens minted by custom auth flows that omit the __tenant__ claim; calling the endpoint from a background service or CLI that reuses a token without tenant headers; misconfigured Finbuckle tenant resolver so the tenant is not resolved into the user's claims.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/dc331ca38cffe6b8.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs:34
ISecurityAudit securityAudit,
IRequestContext requestContext,
ILogger<RevokeImpersonationGrantCommandHandler> logger)
: ICommandHandler<RevokeImpersonationGrantCommand, ImpersonationGrantDto>
{
public async ValueTask<ImpersonationGrantDto> Handle(
RevokeImpersonationGrantCommand request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
if (!currentUser.IsAuthenticated())
{
throw new UnauthorizedException();
}
var callerUserId = currentUser.GetUserId().ToString();
var callerTenantId = currentUser.GetTenant()
?? throw new UnauthorizedException("missing tenant context");
var isRoot = string.Equals(callerTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal);
// Enforce visibility before revoking: tenant admins may only revoke grants in their own
// tenant. Cross-tenant grants return 404 (not 403) so existence isn't confirmed out of scope.
var grant = await grantService.GetByIdAsync(request.GrantId, cancellationToken).ConfigureAwait(false)
?? throw new NotFoundException("impersonation grant not found");
var withinTenant = string.Equals(grant.ImpersonatedTenantId, callerTenantId, StringComparison.Ordinal)
|| string.Equals(grant.ActorTenantId, callerTenantId, StringComparison.Ordinal);
if (!isRoot && !withinTenant)
{
throw new NotFoundException("impersonation grant not found");
}
var updated = await grantService.RevokeAsync(
id: request.GrantId,
revokedByUserId: callerUserId,View on GitHub (pinned to 3f2959e683)