Why tenant isolation matters in modern SaaS
Imagine a single platform serving thousands of independent businesses, each with its own branding, data, and compliance requirements. According to a 2023 Gartner report, 78% of enterprises plan to adopt multi‑tenant SaaS within the next two years, but security concerns remain the top barrier. Tenant isolation prevents one customer’s data from leaking into another’s, protects against cross‑tenant attacks, and simplifies audit trails.
Architecture overview for .NET 8 multi‑tenant solutions
.NET 8 introduces native support for minimal APIs, improved performance, and built‑in support for HTTP/3, making it a solid foundation for high‑scale SaaS. A typical architecture layers the presentation (Blazor or MVC), a domain layer that contains tenant‑aware services, and a data layer where each tenant either gets a separate schema or a filtered row‑level security view. The identity layer sits at the edge, delegating authentication to Azure AD B2C while using Duende IdentityServer for OAuth2 and OpenID Connect flows across tenants.
Implementing tenant isolation in code
Start by injecting a TenantInfo object into every request. In ASP.NET Core you can use middleware that reads the host header (e.g., tenant1.myapp.com) or a JWT claim ("tid") and stores the value in HttpContext.Items. Example:
public class TenantResolutionMiddleware { private readonly RequestDelegate _next; public TenantResolutionMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { var host = context.Request.Host.Host; var tenantId = host.Split('.')[0]; context.Items["TenantId"] = tenantId; await _next(context); } } All services then retrieve the tenant identifier via context.GetTenantId() and apply it to EF Core queries:
public IQueryable GetCustomers() { var tenantId = _httpContextAccessor.HttpContext.GetTenantId(); return _db.Customers.Where(c => c.TenantId == tenantId); } For stricter isolation, consider using separate databases per tenant. Azure SQL supports elastic pools, allowing you to host thousands of databases while paying for pooled resources.
Configuring Duende IdentityServer with Azure AD B2C
Duende IdentityServer acts as a federation gateway. It validates tokens from Azure AD B2C, enriches them with tenant claims, and issues access tokens for your APIs. The following minimal configuration shows how to wire the two together in a .NET 8 WebApplication.
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddDuendeIdentityServer(options => { options.IssuerUri = "https://login.myapp.com"; }) .AddConfigurationStore<ConfigurationDbContext>(options => { options.ConfigureDbContext = b => b.UseSqlServer(Configuration.GetConnectionString("IdentityDb")); }) .AddOperationalStore<PersistedGrantDbContext>(options => { options.ConfigureDbContext = b => b.UseSqlServer(Configuration.GetConnectionString("IdentityDb")); }) .AddAspNetIdentity<ApplicationUser>(); services.AddAuthentication() .AddOpenIdConnect("AzureAdB2C", options => { options.Authority = $"https://{Configuration[\"AzureAdB2C:Tenant\"]}.b2clogin.com/{Configuration[\"AzureAdB2C:Tenant\"]}.onmicrosoft.com/{Configuration[\"AzureAdB2C:Policy\"]}/v2.0/"; options.ClientId = Configuration[\"AzureAdB2C:ClientId\"]; options.ResponseType = "code"; options.Scope.Add("openid"); options.Scope.Add("profile"); options.SaveTokens = true; }); } } Key points: set the Authority to the B2C policy URL, request the "openid" and "profile" scopes, and enable SaveTokens so the downstream API can read the tenant claim ("tfp" or custom "tenant_id").
Securing APIs with .NET 8 and policy‑based authorization
Each microservice validates the JWT issued by IdentityServer. Use the RequireClaim policy to enforce tenant boundaries:
services.AddAuthorization(options => { options.AddPolicy("TenantAccess", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("tenant_id"); }); }); In controllers, annotate actions with [Authorize(Policy = "TenantAccess")]. The policy automatically rejects tokens lacking the correct tenant claim, ensuring that even a compromised client cannot access another tenant’s resources.
Deployment considerations on Azure
Deploy the solution to Azure App Service or Azure Container Apps with the following checklist:
- Enable Managed Identity for the app to retrieve connection strings from Azure Key Vault without hard‑coding secrets.
- Configure Azure Front Door or Azure Application Gateway to route sub‑domains (tenant1.myapp.com) to the same backend pool, preserving host headers for tenant resolution middleware.
- Scale the IdentityServer instance horizontally; Duende supports distributed caching via Redis, which you should enable for persisted grants.
Monitoring is essential. Azure Monitor and Application Insights can track authentication failures per tenant, helping you detect anomalous activity early.
Conclusion
Building a secure multi‑tenant SaaS on .NET 8 requires a disciplined approach to tenant isolation, a robust identity federation layer with Duende IdentityServer, and the scalability of Azure AD B2C. By injecting tenant context early, leveraging policy‑based authorization, and deploying with Azure’s managed services, you can meet the high security expectations of enterprise customers while keeping operational overhead low.
Author: Mahmut Sarıkaya — sarikayadev.com
Sources
- Microsoft Docs – Azure AD B2C documentation
- Duende IdentityServer – Official documentation
- Gartner, "Forecast Analysis: Public Cloud Services, Worldwide", 2023