SaaSIdentityAugust 2026 · 7 min read

Multi-Tenant SaaS .NET: Authentication, Claims, and Tenant Resolution

TL;DR

Multi-Tenant SaaS .NET authentication starts with Identity and JWT: the token carries a tenant_id claim, middleware resolves the tenant from the claim (with a header or subdomain fallback), and a Scoped TenantContext feeds tenant-aware services for the whole request. Claims-based authorization then guards every endpoint with both role and tenant checks.

Multi-Tenant SaaS .NET authentication has to answer two questions on every request: who is this user, and which tenant do they belong to? ASP.NET Core Identity answers the first, and a tenant_id claim inside the JWT answers the second. Because the token is signed and validated before your application sees it, the claim is a trustworthy source of truth for tenancy. That is why the Indotalent SaaS CRM and HRM products use exactly this model: ASP.NET Core Identity, JWT tokens, and tenant claims that flow into every vertical slice.

JWT Tenant Claims in Multi-Tenant SaaS .NET

When a user signs in, the login endpoint looks up the user together with their tenant, builds a claim set, and issues a signed JWT. The token below carries the standard subject claim, a role claim, and the tenant_id claim. The tenant claim travels inside the token, so the client does not need a separate request to discover tenancy.

var claims = new List<Claim>
{
    new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
    new(ClaimTypes.Role, role.Name),
    new("tenant_id", user.TenantId.ToString())
};

var token = new JwtSecurityToken(
    issuer: _jwtOptions.Issuer,
    audience: _jwtOptions.Audience,
    claims: claims,
    expires: DateTime.UtcNow.AddHours(1),
    signingCredentials: signingCredentials);

Clients send the token as an Authorization: Bearer header. The authentication middleware validates the signature, the issuer, and the audience, then maps the claims onto a ClaimsPrincipal. From that point on, context.User.FindFirst("tenant_id") returns the tenant for the request without any additional lookup.

Token validation is configured once with AddAuthentication and AddJwtBearer. The signature, issuer, audience, and lifetime are all verified automatically, and you can hook the TokenValidated event to copy custom claims or to reject tokens whose tenant has been disabled. Keeping this logic in one place means every endpoint inherits the same authentication guarantees without per-route setup.

Tenant Resolution: Claim, Header, and Subdomain Fallback

Resolution happens right after authentication. The middleware below reads the tenant claim, falls back to an X-Tenant-Id header for machine clients, and finally to the first subdomain segment for branding routes. The resolved value is written into the Scoped TenantContext before any business service runs.

app.Use(async (context, next) =>
{
    var tenantId = context.User.FindFirst("tenant_id")?.Value
        ?? context.Request.Headers["X-Tenant-Id"].FirstOrDefault()
        ?? context.Request.Host.Host.Split('.')[0];

    if (tenantId is not null)
    {
        context.RequestServices
            .GetRequiredService<TenantContext>().Id = Guid.Parse(tenantId);
    }

    await next();
});

Claim-first ordering matters. A subdomain can be rewritten by a proxy or a malicious client header, but the claim is cryptographically bound to the authenticated user. Using the claim as the primary source and the header as a fallback keeps the resolution honest without breaking API clients.

Tenant-Aware Scoped Services for Multi-Tenant SaaS .NET

The TenantContext is registered as Scoped so every service resolved within a request sees the same tenant. The AppDbContext is also Scoped and depends on the context, so its global query filters apply the correct tenant. The registration below wires the whole dependency graph to the per-request tenant.

builder.Services.AddScoped<TenantContext>();
builder.Services.AddScoped<AppDbContext>();
builder.Services.AddScoped<ICurrentTenant>(sp =>
    sp.GetRequiredService<TenantContext>());

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TenantMember", policy => policy
        .RequireAuthenticatedUser()
        .RequireClaim("tenant_id"));
});

Authorization is where tenancy and identity meet. The TenantMember policy requires an authenticated user with a tenant claim, and handlers can go further by comparing the policy's tenant to the TenantContext. This layered check means an attacker cannot simply replay a token across tenants.

The choice of lifetime is deliberate. A Singleton TenantContext would be shared by every concurrent user, and a Transient one would produce a new instance per injection, so two services in the same request could disagree about the tenant. Scoped is the only lifetime that guarantees one tenant per request and one context for every consumer of that request.

Authorization with Roles and Tenants

Roles and tenancy are orthogonal. A user can hold the Admin role, but that role only means something within their own tenant. Keep role checks and tenant checks separate: role claims authorize capability, and the TenantContext authorizes scope. Combined, they give you precise rules such as "only a billing admin of this tenant may export invoices".

One more nuance for Blazor Server: the DI scope follows the circuit, not a single HTTP request. When a user authenticates inside a circuit, set the TenantContext for that circuit, and re-scope on tenant switch or logout. This keeps one user's session from inheriting another tenant's context.

You can also pair role and tenant policies in one handler. A handler can read the tenant claim, compare it with the TenantContext, and then check the role, producing precise rules such as only tenant billing admins may export invoices. The two checks stay independent so that adding a tenant to an admin does not accidentally widen their permissions.

Key Takeaways

  • Put a tenant_id claim inside the JWT so tenancy is signed and trusted
  • Resolve the tenant once per request and store it in a Scoped TenantContext
  • Order resolution as claim, header, then subdomain
  • Use authorization policies that require both authentication and a tenant claim
  • In Blazor Server, scope the tenant context to the circuit, not to a page

FAQ

Can one user belong to several tenants? Yes. Issue the token with a current tenant claim and keep the full tenant list as a second claim. Switching tenants means re-issuing the token or resolving a new current tenant claim.

Why prefer the JWT claim over the subdomain? The claim is validated by the token signature. Subdomains and headers can be influenced by proxies and clients, so they belong only in the fallback chain.

Should tenant checks live in handlers or the UI? In the service boundary and in authorization policies. Hiding buttons in the UI is UX, not security.

Does Blazor Server change the auth design? Only the scope lifetime. Keep the same JWT and claims, but set the TenantContext when the circuit authenticates so the scoped services read the right tenant.

Ready to study JWT and Identity in production code?

Indotalent ships SaaS multi-tenant CRM and HRM products with complete .NET 10 source code — $21 each.

Explore Products