MultiTenantAuth.AspNetCore 1.1.0

Prefix Reserved
dotnet add package MultiTenantAuth.AspNetCore --version 1.1.0
                    
NuGet\Install-Package MultiTenantAuth.AspNetCore -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MultiTenantAuth.AspNetCore" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MultiTenantAuth.AspNetCore" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="MultiTenantAuth.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MultiTenantAuth.AspNetCore --version 1.1.0
                    
#r "nuget: MultiTenantAuth.AspNetCore, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MultiTenantAuth.AspNetCore@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MultiTenantAuth.AspNetCore&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=MultiTenantAuth.AspNetCore&version=1.1.0
                    
Install as a Cake Tool

MultiTenantAuth.AspNetCore

NuGet CI License: MIT Changelog

Lightweight, production-ready multi-tenant authentication and authorization middleware for ASP.NET Core.

Resolve tenant context from subdomain, header, route value, claim, or a fully custom strategy. Enforce tenant-aware access control in SaaS applications — without third-party runtime dependencies.


When to use this package

  • You are building a SaaS application where users belong to one or more tenants.
  • You need to resolve the tenant from each HTTP request automatically.
  • You need to validate that the authenticated user is allowed to access the resolved tenant.
  • You want a lightweight, dependency-free solution that plugs into standard ASP.NET Core middleware.

When NOT to use this package

  • You need a full-featured multi-tenancy framework (database-per-tenant, full DI scoping, etc.) — consider Finbuckle.MultiTenant or similar.
  • You are building a single-tenant application.
  • You need tenant resolution before routing (e.g. to select a connection string) — the tenant is resolved inside middleware, which runs after routing.

Installation

dotnet add package MultiTenantAuth.AspNetCore

Quick Start

// Program.cs

builder.Services.AddAuthentication(/* ... */);
builder.Services.AddAuthorization();

builder.Services.AddMultiTenantAuth();   // ← default options

var app = builder.Build();

app.UseAuthentication();
app.UseMultiTenantAuth();                // ← must be after UseAuthentication
app.UseAuthorization();

app.MapControllers();

Middleware Order

Important: middleware order matters.

app.UseRouting();

app.UseAuthentication();     // 1. Authenticate the user (populate HttpContext.User)

app.UseMultiTenantAuth();    // 2. Resolve tenant; validate user belongs to tenant

app.UseAuthorization();      // 3. Enforce [Authorize] policies

app.MapControllers();        // 4. Route to endpoints

UseAuthentication must run first so that the user's claims are available when the tenant is validated.
UseMultiTenantAuth must run before UseAuthorization so the tenant context is available to authorization policies.


Configuration

builder.Services.AddMultiTenantAuth(options =>
{
    // Resolution order: first strategy that finds a tenant wins.
    options.ResolutionOrder = new[]
    {
        TenantResolutionStrategy.RouteValue,
        TenantResolutionStrategy.Header,
        TenantResolutionStrategy.Subdomain,
        TenantResolutionStrategy.Claim
    };

    options.TenantHeaderName        = "X-Tenant-Id";     // default
    options.TenantRouteValueName    = "tenantId";         // default
    options.TenantClaimType         = "tenant_id";        // default
    options.AllowedTenantsClaimType = "allowed_tenants";  // default

    options.RequireAuthenticatedUser = true;   // 401 if user is not authenticated
    options.RequireResolvedTenant    = true;   // 400 if tenant cannot be resolved
    options.RequireTenantClaim       = true;   // 403 if user's claim doesn't match tenant

    options.MaxTenantIdLength    = 64;
    options.MaxTenantSlugLength  = 100;
    options.AllowedTenantPattern = @"^[a-zA-Z0-9\-_]+$";  // default
});

Accessing the Tenant in Code

Inject ITenantContextAccessor anywhere in your application:

public class ProjectService
{
    private readonly ITenantContextAccessor _tenantContextAccessor;

    public ProjectService(ITenantContextAccessor tenantContextAccessor)
    {
        _tenantContextAccessor = tenantContextAccessor;
    }

    public async Task<IEnumerable<Project>> GetProjectsAsync()
    {
        var tenantId = _tenantContextAccessor.Current?.TenantId
            ?? throw new InvalidOperationException("No tenant context.");

        return await _dbContext.Projects
            .Where(p => p.TenantId == tenantId)
            .ToListAsync();
    }
}

Examples by Strategy

Header-based tenant

options.ResolutionOrder = [TenantResolutionStrategy.Header];
options.TenantHeaderName = "X-Tenant-Id";

HTTP request:

GET /api/projects HTTP/1.1
X-Tenant-Id: acme
Authorization: Bearer <token>

Security note: Header values can be forged. Always combine header-based resolution with token claim validation (RequireTenantClaim = true).

Subdomain-based tenant

options.ResolutionOrder = [TenantResolutionStrategy.Subdomain];

Request to acme.yoursaas.comTenantId = "acme", TenantSlug = "acme".

Works for any subdomain format. Single-label hosts (e.g. localhost) are skipped.

Route-based tenant

options.ResolutionOrder = [TenantResolutionStrategy.RouteValue];
options.TenantRouteValueName = "tenantId"; // default
app.MapGet("/api/{tenantId}/projects", ...);

Claims-based tenant (token-only)

options.ResolutionOrder = [TenantResolutionStrategy.Claim];
options.TenantClaimType = "tenant_id";

The tenant is read from the tenant_id claim in the user's authenticated identity. Useful when every request carries a JWT with the tenant embedded.


Claims Validation

By default, after resolving the tenant, the middleware checks that the authenticated user has a matching claim:

  • Primary claim (TenantClaimType, default tenant_id) — exact match.
  • Multiple claims — if the user has multiple tenant_id claims, any match succeeds.
  • Allowed tenants claim (AllowedTenantsClaimType, default allowed_tenants) — may be a single value or a comma-separated list.
{
  "sub": "user123",
  "tenant_id": "acme",
  "allowed_tenants": "acme,beta,gamma"
}

Custom Resolver

public class DatabaseTenantResolver : ITenantResolver
{
    private readonly ITenantRepository _repo;

    public DatabaseTenantResolver(ITenantRepository repo) => _repo = repo;

    public async ValueTask<TenantResolutionResult> ResolveAsync(
        HttpContext context,
        CancellationToken cancellationToken = default)
    {
        var host = context.Request.Host.Host;
        var tenant = await _repo.FindByHostAsync(host, cancellationToken);

        if (tenant is null)
            return TenantResolutionResult.Fail("Tenant not found for host.");

        return TenantResolutionResult.Success(new TenantContext
        {
            TenantId = tenant.Id,
            TenantSlug = tenant.Slug,
            Source = "database",
            IsResolved = true
        });
    }
}

Register and configure:

builder.Services.AddSingleton<ITenantResolver, DatabaseTenantResolver>();

builder.Services.AddMultiTenantAuth(options =>
{
    options.ResolutionOrder = [TenantResolutionStrategy.Custom];
    options.CustomResolverType = typeof(DatabaseTenantResolver);
});

Custom Validator

public class RoleTenantValidator : ITenantValidator
{
    public ValueTask<TenantValidationResult> ValidateAsync(
        TenantContext tenant,
        ClaimsPrincipal user,
        HttpContext context,
        CancellationToken cancellationToken = default)
    {
        if (!user.IsInRole($"tenant:{tenant.TenantId}"))
        {
            return new(TenantValidationResult.Fail(
                StatusCodes.Status403Forbidden,
                "User does not have the required tenant role."));
        }

        return new(TenantValidationResult.Success());
    }
}

Register:

builder.Services.AddSingleton<ITenantValidator, RoleTenantValidator>();

builder.Services.AddMultiTenantAuth(options =>
{
    options.CustomValidatorType = typeof(RoleTenantValidator);
});

Query String Warning

Warning: Query string tenant resolution is disabled by default and should remain so in most applications.

Query string values are:

  • Visible in server logs, browser history, and referrer headers.
  • Easily manipulated by end users and attackers.
  • Susceptible to CSRF and link-sharing attacks.

Only enable if you have additional protections:

// Only enable this when you fully understand the implications.
options.EnableQueryStringResolution = true;
options.ResolutionOrder = [TenantResolutionStrategy.QueryString];

HTTP Status Behaviour

Situation Default status
Tenant format invalid 400 Bad Request
Tenant not resolved (when required) 400 Bad Request
Tenant not resolved, ReturnNotFoundForUnknownTenant = true 404 Not Found
User not authenticated (when required) 401 Unauthorized
User authenticated but not in tenant 403 Forbidden
Custom validator returns status As configured

SaaS Architecture Notes

  • Tenant isolation: This library resolves and validates the tenant; it does not enforce data isolation. Apply tenant filters in your repositories/queries.
  • Database per tenant: Combine this package with a custom ITenantResolver that selects a connection string based on the resolved tenant.
  • Caching: The default resolvers do not cache. For subdomain lookups against a database, implement caching in your custom resolver.
  • Scoped DI: The tenant context is stored in an AsyncLocal — it is automatically scoped to the current async execution context (one per request).

Version Compatibility

Package version .NET version
1.x .NET 8, .NET 10

The library is designed with stable ASP.NET Core abstractions so adding .NET 11+ should require only a TargetFrameworks change.


Publishing to NuGet

dotnet pack src/MultiTenantAuth.AspNetCore/MultiTenantAuth.AspNetCore.csproj -c Release -o ./artifacts

dotnet nuget push ./artifacts/MultiTenantAuth.AspNetCore.1.0.0.nupkg \
  --api-key $NUGET_API_KEY \
  --source https://api.nuget.org/v3/index.json

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for the full development workflow, coding conventions, and PR process. For security issues, follow SECURITY.md — do not open public issues.


Community

Document Purpose
CHANGELOG.md Version history and breaking changes
CONTRIBUTING.md How to contribute
CODE_OF_CONDUCT.md Community standards
SECURITY.md Responsible disclosure

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 87 5/22/2026
1.0.0 90 5/20/2026