{"record":{"id":"dc331ca38cffe6b8","repo":"fullstackhero/dotnet-starter-kit","slug":"missing-tenant-context-dc331c","errorCode":null,"errorMessage":"missing tenant context","messagePattern":"missing tenant context","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs","lineNumber":34,"sourceCode":"    ISecurityAudit securityAudit,\n    IRequestContext requestContext,\n    ILogger<RevokeImpersonationGrantCommandHandler> logger)\n    : ICommandHandler<RevokeImpersonationGrantCommand, ImpersonationGrantDto>\n{\n    public async ValueTask<ImpersonationGrantDto> Handle(\n        RevokeImpersonationGrantCommand request,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        if (!currentUser.IsAuthenticated())\n        {\n            throw new UnauthorizedException();\n        }\n\n        var callerUserId = currentUser.GetUserId().ToString();\n        var callerTenantId = currentUser.GetTenant()\n            ?? throw new UnauthorizedException(\"missing tenant context\");\n        var isRoot = string.Equals(callerTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal);\n\n        // Enforce visibility before revoking: tenant admins may only revoke grants in their own\n        // tenant. Cross-tenant grants return 404 (not 403) so existence isn't confirmed out of scope.\n        var grant = await grantService.GetByIdAsync(request.GrantId, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException(\"impersonation grant not found\");\n\n        var withinTenant = string.Equals(grant.ImpersonatedTenantId, callerTenantId, StringComparison.Ordinal)\n            || string.Equals(grant.ActorTenantId, callerTenantId, StringComparison.Ordinal);\n\n        if (!isRoot && !withinTenant)\n        {\n            throw new NotFoundException(\"impersonation grant not found\");\n        }\n\n        var updated = await grantService.RevokeAsync(\n            id: request.GrantId,\n            revokedByUserId: callerUserId,","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs#L16-L52","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nclassicHttpClient.DefaultRequestHeaders.Remove(\"tenant\");\n// after\nclient.DefaultRequestHeaders.Add(\"tenant\", tenantId); // ensure tenant is resolved for the JWT/claims","handlingStrategy":"validation","validationCode":"const tenant = claims.find(c => c.type === 'tenant')?.value;\nif (!tenant) throw new Error('token has no tenant claim; re-authenticate');","typeGuard":"function hasTenantClaim(c: {type: string; value?: string}[]): c is {type: string; value: string}[] {\n  return !!c.find(x => x.type === 'tenant' && !!x.value);\n}","tryCatchPattern":"try { await api.revokeImpersonationGrant(grantId); }\ncatch (e) { if (e.status === 401 && e.message.includes('tenant context')) { await reauthenticateWithTenant(tenantId); } else throw e; }","preventionTips":["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"],"tags":["auth","multitenancy","impersonation"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}