Multitenancy Techniques for ASP.NET Core

Introduction

This will be another post on the multitenancy subject. For now, I plan to write or already wrote:

  • Tenant identification (this article)
  • Data filtering (for EF Core)
  • UI customisation
  • Business logic

If you've been following this blog, you know about data filtering with EF Core as an example. This time, I will focus on tenant identification.

A tenant is identified by some id. It may have some data, configuration, etc, against it, but, for now, we just want to be able to identify who it is.

Tenant Identification

When it comes to identifying a tenant, there are some options:

  • A query string parameter (mostly for testing)
  • A route part
  • An HTTP header
  • A JWT token property
  • A cookie

A single tenant will be associated with a request, and it won't change during its processing, so we just need to obtain the tenant id first and possibly store it somewhere.

Let's reuse the same interface as before:

public interface ITenantIdProvider
{
    string GetTenantId();
}

As you can see, nothing especial about it, just a single method that returns the current tenant id as a string (unencrypted). This interface should be registered in Dependency Injection (DI) with a concrete implementation and obtained whenever we need to get the current tenant.

Let's now have a look at the different implementation options.

Query String

Using the query string for identifying a tenant (e.g., "?tenantid=xxx") is something that should probably be used in development/debugging/testing scenarios, as it is inherently insecure - we are able to provide any tenant id we want to.

The implementation seems simple enough:

public class QueryStringTenantIdProvider(IHttpContextAccessor httpContextAccessor): ITenantIdProvider
{
    public string GetTenantId()
    {
        return httpContextAccessor.HttpContext.Request.Query["TenantId"];
    }
}

builder.Services.AddScoped<ITenantIdProvider, QueryStringTenantIdProvider>();

Route

Using a route component to map the tenant id is not something that I see very often - or at all - but it is a possibility. Just imagine you have a route such as "/[controller]/[action]/{tenant-id}". You would get the tenant from the "tenant-id" component of the route. Again, like with query strings, possibly not something that you would use outside development/debugging/testing.

Here is a possible implementation:

public class RouteTenantIdProvider(IHttpContextAccessor httpContextAccessor): ITenantIdProvider
{
    public string GetTenantId()
    {
        return httpContextAccessor.HttpContext.GetRouteData().Values["tenant-id"] ?? string.Empty;
    }
}

HTTP Header

Another valid option, especially for API-to-API requests, is to use an HTTP header to send the tenant id. One commonly used header name for it is X-Tenant-Id.

A very basic implementation that does what it needs:

public class HeaderTenantIdProvider(IHttpContextAccessor httpContextAccessor): ITenantIdProvider
{
    public string GetTenantId()
    {
        return httpContextAccessor.HttpContext.Request.Headers["X-Tenant-Id"];
    }
}

JWT Token

A JWT token is the preferred way to perform authentication for web APIs.

When it comes to getting the tenant id from a property from the authentication JWT token, we have two options:

For option #1, the following code could be used:

public class ClaimsTenantIdProvider(IHttpContextAccessor httpContextAccessor) : ITenantIdProvider
{
    public string GetTenantId()
    {
        return HttpContext.User.FindFirst("tenant_id")?.Value ?? string.Empty;
    }
}

We just return the value of claim "tenant_id", if it exists, or a blank string otherwise.

Now, for parsing the JWT token, things are slightly more complex:

public class JwtTenantIdProvider(IHttpContextAccessor httpContextAccessor) : ITenantIdProvider
{
    private static readonly JwtSecurityTokenHandler _tokenHandler = new();

    static string GetToken()
    {
        var token = httpContextAccessor.HttpContext!.Request.Headers[HeaderNames.Authorization];

        if (string.IsNullOrWhiteSpace(token))
        {
            throw new InvalidOperationException("No token found in request headers");
        }

        token = token.ToString().Replace("Bearer ", string.Empty);

        return token!;
    }

    static JwtSecurityToken Parse(string token)
    {
        var jwtToken = _tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
        {
            throw new InvalidOperationException("Could not parse the JWT token");
        }

        return jwtToken!;
    }

    static string GetPayloadProperty(JwtSecurityToken jwtToken, string propertyName)
    {
        if (!jwtToken.Payload.TryGetValue(propertyName, out var propertyValue))
        {
            throw new InvalidOperationException($"Could not extract property {propertyName} from the JWT token payload");
        }

        return propertyValue.ToString()!;
    }

    public string GetTenantId()
    {
        var token = GetToken();
        var jwtToken = Parse(token);
        var tenantId = GetPayloadProperty(jwtToken, "tenant_id");
        return tenantId;
    }
}

Note that this has minimum checks, but it should work.

Cookie

This option could be useful if we set the tenant id after a user logs in, through a cookie. If the user is not logged it, it will be impossible to know what the tenant is. Of course, this plays well with scenarios where humans are involved, not APIs making calls.

The code should also be quite simple:

public class CookieTenantIdProvider(IHttpContextAccessor httpContextAccessor) : ITenantIdProvider
{
    public string GetTenantId()
    {
        return HttpContext.Request.Cookies["tenant_id"] ?? string.Empty;
    }
}

Conclusion

To be clear, I only presented minimal implementations for each strategy, with barely no validation, logging, or error checking. Hopefuly, they can serve as guides for actual implementations.

To summarise, we have 5 ways to get the tenant obtaining strategies, and here is my opinion on them:

Strategy Pros Cons
Query String Good for dev/debugging/testing as it's easy to change tenants Not meant for production as it's easy to forge a different tenant
Route Good for dev/debugging/testing as it's easy to change tenants Not meant for production as it's easy to forge a different tenant
HTTP Header Good for API calls Clients need to know their tenant id
JWT token
  • Good for API calls
  • No need to know the tenant id
Need to maintain and manage the JTW tokens
Cookie
  • Good for human usage
  • Tenants are only set from the server, not easily forged

I hope this serves as a guide to you when making a decision. As always, do let me know if you think I forgot something. More articles on multitenancy to follow shortly!

Comments

Popular posts from this blog

C# Magical Syntax

OpenTelemetry with ASP.NET Core

ASP.NET Core Middleware