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:
- Look at the current user claims (ClaimsPrincipal.Claims property)
- Look at the JWT token directly (Bearer Authorization header)
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 |
|
Need to maintain and manage the JTW tokens |
| Cookie |
|
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
Post a Comment