{"record":{"id":"89651d0ca01a4fc5","repo":"fullstackhero/dotnet-starter-kit","slug":"invalid-tenant-userprofileservice","errorCode":null,"errorMessage":"invalid tenant","messagePattern":"invalid tenant","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/UserProfileService.cs","lineNumber":160,"sourceCode":"    }\n\n    public async Task<bool> ExistsWithNameAsync(string name, CancellationToken cancellationToken = default)\n    {\n        EnsureValidTenant();\n        return await userManager.FindByNameAsync(name) is not null;\n    }\n\n    public async Task<bool> ExistsWithPhoneNumberAsync(string phoneNumber, string? exceptId = null, CancellationToken cancellationToken = default)\n    {\n        EnsureValidTenant();\n        return await userManager.Users.FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber, cancellationToken) is FshUser user && user.Id != exceptId;\n    }\n\n    private void EnsureValidTenant()\n    {\n        if (string.IsNullOrWhiteSpace(multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id))\n        {\n            throw new UnauthorizedException(\"invalid tenant\");\n        }\n    }\n\n    private string? ResolveImageUrl(Uri? imageUrl)\n    {\n        if (imageUrl is null)\n        {\n            return null;\n        }\n\n        // Absolute URLs (e.g., S3) pass through unchanged.\n        if (imageUrl.IsAbsoluteUri)\n        {\n            return imageUrl.ToString();\n        }\n\n        // For relative paths from local storage, prefix with the API origin and wwwroot.\n        if (_originUrl is null)","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/UserProfileService.cs#L142-L178","documentation":"EnsureValidTenant throws UnauthorizedException('invalid tenant') when the Finbuckle MultiTenantContext has no TenantInfo.Id for the current context. It guards tenant-scoped existence checks and image URL updates so they never execute outside a tenant scope, protecting tenant isolation.","triggerScenarios":"Calling SetImageUrlAsync, ExistsWithEmailAsync, ExistsWithNameAsync, or ExistsWithPhoneNumberAsync from a context with no resolved tenant (background job, migration host, or request lacking tenant identifiers).","commonSituations":"Calling these services from Hangfire jobs or startup code where the tenant resolver never ran; scripts hitting endpoints without tenant headers; Finbuckle store missing the host mapping; middleware ordering issues.","solutions":["Pass tenant resolution data on every request (tenant header, __tenant__ parameter, or mapped host).","In background/non-HTTP contexts, set the multitenant context explicitly before calling the service.","Verify Finbuckle middleware registration/order and the tenant store contents.","Configure a default tenant if the deployment is effectively single-tenant."],"exampleFix":"// before (job code, no tenant context)\nawait userProfileService.ExistsWithEmailAsync(email);\n// after\nusing (multiTenantContextAccessor.MultiTenantContext.SetTenantInfo(\n    new TenantInfo(\"tenant-1\", \"tenant-1\", null), replace: false))\n{\n    await userProfileService.ExistsWithEmailAsync(email);\n}","handlingStrategy":"validation","validationCode":"const tenantId = headers['X-Tenant-Id'] ?? new URLSearchParams(location.search).get('__tenant__');\nif (!tenantId) throw new Error('No tenant resolved; call would throw \"invalid tenant\"');","typeGuard":null,"tryCatchPattern":"try { await existsService.ExistsWithEmailAsync(email); }\ncatch (e) { if (e.status === 401 && e.message.includes('invalid tenant')) { resolveTenantThenRetry(); } else { throw e; } }","preventionTips":["In Hangfire jobs/background code, set the multitenant context explicitly before calling tenant-scoped services.","Never call tenant-scoped services during app startup/migration.","Send tenant identifiers on every API request from custom clients.","Add a smoke test asserting tenant resolution before these endpoints."],"tags":["multitenancy","identity","authorization"],"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"}