{"record":{"id":"c52587d9f77849c4","repo":"fullstackhero/dotnet-starter-kit","slug":"missing-tenant-context-startimpersonationcommandhandler","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/StartImpersonation/StartImpersonationCommandHandler.cs","lineNumber":61,"sourceCode":"        _grantService = grantService;\n        _timeProvider = timeProvider;\n        _logger = logger;\n    }\n\n    public async ValueTask<ImpersonationResponse> Handle(\n        StartImpersonationCommand request,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        if (!_currentUser.IsAuthenticated())\n        {\n            throw new UnauthorizedException();\n        }\n\n        var actorUserId = _currentUser.GetUserId().ToString();\n        var actorTenantId = _currentUser.GetTenant()\n            ?? throw new UnauthorizedException(\"missing tenant context\");\n        var actorUserName = _currentUser.Name;\n\n        // Cross-tenant impersonation requires the actor to be in the root tenant. Tenant admins\n        // can only impersonate users within their own tenant.\n        if (!string.Equals(actorTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal)\n            && !string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))\n        {\n            throw new ForbiddenException(\"cross-tenant impersonation is restricted to platform operators\");\n        }\n\n        // Prevent self-impersonation (pointless, confuses the audit trail). Caller error → explicit 4xx,\n        // not the 500 CustomException defaults to.\n        if (string.Equals(actorUserId, request.TargetUserId, StringComparison.Ordinal)\n            && string.Equals(actorTenantId, request.TargetTenantId, StringComparison.Ordinal))\n        {\n            throw new CustomException(\"cannot impersonate yourself\", errors: null, System.Net.HttpStatusCode.BadRequest);\n        }\n","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/StartImpersonation/StartImpersonationCommandHandler.cs#L43-L79","documentation":"After confirming authentication, the start-impersonation handler requires the actor's tenant claim; _currentUser.GetTenant() returning null triggers UnauthorizedException('missing tenant context') (HTTP 401). Tenant scoping is required to enforce the cross-tenant rules that follow.","triggerScenarios":"An authenticated token without the tenant claim is used to start impersonation — e.g. a token issued by a custom auth endpoint that skips tenant claim generation, or a request bypassing the tenant resolver.","commonSituations":"Manually minted dev tokens lacking tenant claims; service accounts provisioned without tenant membership; Finbuckle tenant strategy changed so the claim name no longer matches what GetTenant reads.","solutions":["Re-issue the token through the standard login flow so tenant claims are included","Verify Finbuckle multitenancy registration and the tenant claim constant still match","Assign the calling service/user to a tenant"],"exampleFix":"// before\nvar claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId) };\n// after\nvar claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId), new(\"tenant\", tenantId) };","handlingStrategy":"validation","validationCode":"const tenant = claims.find(c => c.type === 'tenant')?.value;\nif (!tenant) throw new Error('no tenant claim on token; re-login through the standard flow');","typeGuard":null,"tryCatchPattern":"try { await api.startImpersonation(req); }\ncatch (e) { if (e.status === 401 && /tenant context/.test(e.message)) { await reauthWithTenant(); return; } throw e; }","preventionTips":["Verify tokens carry the tenant claim after any auth-flow change","Keep Finbuckle tenant resolution aligned with the claim constant used by GetTenant","Assign service accounts to a tenant","Add a startup/CI check that a sample token contains tenant claims"],"tags":["auth","multitenancy","claims"],"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"}