{"record":{"id":"c284aca394568b7e","repo":"fullstackhero/dotnet-starter-kit","slug":"missing-tenant-context","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/GetImpersonationGrants/GetImpersonationGrantsQueryHandler.cs","lineNumber":23,"sourceCode":"using FSH.Modules.Identity.Contracts.v1.Impersonation;\nusing FSH.Modules.Identity.Contracts.v1.Impersonation.GetImpersonationGrants;\nusing Mediator;\n\nnamespace FSH.Modules.Identity.Features.v1.Impersonation.GetImpersonationGrants;\n\npublic sealed class GetImpersonationGrantsQueryHandler(\n    IImpersonationGrantService grantService,\n    ICurrentUser currentUser)\n    : IQueryHandler<GetImpersonationGrantsQuery, IReadOnlyList<ImpersonationGrantDto>>\n{\n    public async ValueTask<IReadOnlyList<ImpersonationGrantDto>> Handle(\n        GetImpersonationGrantsQuery request,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        var callerTenant = currentUser.GetTenant()\n            ?? throw new UnauthorizedException(\"missing tenant context\");\n        var isRoot = string.Equals(callerTenant, MultitenancyConstants.Root.Id, StringComparison.Ordinal);\n\n        // Tenant scoping: root operators target any tenant; tenant admins are locked to their\n        // own regardless of input. Mirrors the StartImpersonation cross-tenant rule.\n        var tenantFilter = isRoot ? request.ImpersonatedTenantId : callerTenant;\n\n        return await grantService.ListAsync(\n            status: request.Status,\n            impersonatedTenantId: tenantFilter,\n            actorUserId: request.ActorUserId,\n            take: request.Take,\n            ct: cancellationToken).ConfigureAwait(false);\n    }\n}\n","sourceCodeStart":5,"sourceCodeEnd":38,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/GetImpersonationGrants/GetImpersonationGrantsQueryHandler.cs#L5-L38","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Authenticate with a normal tenant-aware JWT that includes the tenant claim (issued via the standard login/identity flow).","If using a test token, add the tenant claim (matching MultitenancyConstants.Root.Id for root operators) to the claims set.","Verify the Finbuckle tenant middleware is resolving/normalizing the tenant so ICurrentUser.GetTenant() is populated for the request."],"exampleFix":"// before\nvar claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId) }; // no tenant claim\n\n// after\nvar claims = new List<Claim>\n{\n    new(ClaimTypes.NameIdentifier, userId),\n    new(\"tenant\", tenantId), // required for tenant-scoped queries\n};","handlingStrategy":"validation","validationCode":"var tenant = auth.getClaim(\"tenant\");\nif (string.IsNullOrEmpty(tenant)) {\n    throw new InvalidOperationException(\"Token lacks tenant claim; re-authenticate via the tenant-aware login flow\");\n}","typeGuard":"bool hasTenantContext(ClaimsPrincipal p) =>\n    p.FindFirst(\"tenant\")?.Value is { Length: > 0 };","tryCatchPattern":"try\n{\n    var grants = await api.get(\"/impersonation/grants\");\n}\ncatch (UnauthorizedException)\n{\n    // missing tenant context: re-authenticate with a tenant-scoped token\n    await auth.login({ tenantId });\n}","preventionTips":["Always obtain tokens through the standard multitenant login pipeline so the tenant claim is included.","Add the tenant claim when minting test/service tokens for impersonation endpoints.","Verify Finbuckle tenant resolution (header/route/host strategy) is active for the request."],"tags":["impersonation","multitenancy","unauthorized"],"backgroundTag":"missing-tenant-context","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"}