{"record":{"id":"cae5c6f40607b31a","repo":"fullstackhero/dotnet-starter-kit","slug":"unauthorized-identityservice","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/IdentityService.cs","lineNumber":175,"sourceCode":"\n        if (userRoleIds.Count > 0)\n        {\n            var roleNames = await _dbContext.Roles\n                .IgnoreQueryFilters()\n                .Where(r => userRoleIds.Contains(r.Id) && EF.Property<string>(r, \"TenantId\") == tenantId)\n                .Select(r => r.Name!)\n                .ToListAsync(ct);\n\n            claims.AddRange(roleNames.Select(r => new Claim(ClaimTypes.Role, r)));\n        }\n\n        return (user.Id, claims);\n    }\n\n    private AppTenantInfo GetValidatedTenant()\n    {\n        var tenant = _multiTenantContextAccessor!.MultiTenantContext.TenantInfo\n            ?? throw new UnauthorizedException();\n\n        if (string.IsNullOrWhiteSpace(tenant.Id))\n        {\n            throw new UnauthorizedException();\n        }\n\n        return tenant;\n    }\n\n    private async Task<FshUser> FindAndValidateUserByCredentialsAsync(string email, string password)\n    {\n        var user = await _userManager.FindByEmailAsync(email.Trim().Normalize());\n        if (user is null)\n        {\n            // Generic 401 — never confirm or deny account existence from this path.\n            throw new UnauthorizedException();\n        }\n","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/IdentityService.cs#L157-L193","documentation":"GetValidatedTenant throws UnauthorizedException when Finbuckle's MultiTenantContext has no TenantInfo, meaning the request reached the Identity service without a resolved tenant. The multitenancy middleware could not derive a tenant identifier (header, route, or host strategy) from the incoming request, so authentication is refused with a generic 401.","triggerScenarios":"Calling any authentication endpoint (login, refresh, GetProfile) without a tenant identifier header (e.g. missing 'tenant' header in a header-strategy setup), or calling the API on a hostname that Finbuckle's host strategy does not map to a tenant. The call 'tenant' path invokes GetValidatedTenant which finds MultiTenantContext.TenantInfo null.","commonSituations":"New API clients forget the tenant header; integration scripts that worked against a single-tenant deployment; reverse-proxy stripping host headers; Finbuckle strategy misconfiguration (wrong header name or case); testing tools (curl/Postman) missing the header.","solutions":["Add the tenant identifier to the request per the configured Finbuckle strategy — typically the 'tenant' HTTP header for header strategy.","Verify the hostname used matches a tenant mapping when using the host strategy; check appsettings Multitenancy section.","Confirm Finbuckle multitenancy middleware is registered before the endpoints in Program.cs and the strategy configured (WithHeaderStrategy/WithHostStrategy)."],"exampleFix":"// before\ncurl -X POST https://localhost:7030/api/tokens -H 'Content-Type: application/json' -d '{...}'\n// after\ncurl -X POST https://localhost:7030/api/tokens -H 'tenant: root' -H 'Content-Type: application/json' -d '{...}'","handlingStrategy":"validation","validationCode":"const tenant = config.tenantId ?? localStorage.getItem('tenant');\nif (!tenant) throw new Error('tenant identifier must be sent with every request');\nheaders['tenant'] = tenant;","typeGuard":null,"tryCatchPattern":"catch (ApiError e) when (e.StatusCode == 401) { redirectToTenantSetup(); }","preventionTips":["Centralize tenant header injection in the HTTP client interceptor so it can never be forgotten.","Document the required tenant header in the API client README/onboarding.","Add a smoke test that asserts login fails fast with a clear message when the tenant header is absent."],"tags":["auth","multitenancy","http-401"],"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"}