Search code examples
asp.net-coreasp.net-core-webapiautofacmulti-tenant

Autofac multitenancy resolve tenant from current user’s principal


I have a .NET 5 ASP.NET Core API in which I'm trying to setup multitenancy using Autofac.AspNetCore.Multitenant v4.0.1. I've implemented a TenantIdentificationStrategy that identifies the tenant from a claim of the current user's principal. The problem is that the User seems to be not yet populated at the time Autofac is resolving the tenant identifier.

The Autofac documentation in here https://autofaccn.readthedocs.io/en/latest/advanced/multitenant.html#tenant-identification states:

The ITenantIdentificationStrategy allows you to retrieve the tenant ID from anywhere appropriate to your application: an environment variable, a role on the current user’s principal, an incoming request value, or anywhere else.

Because of this statement, I assume that this should work, so I'm wondering if I'm doing something wrong or if there is a bug on the framework, or if the docs are stating something that is not supported by the framework.

Any idea on how to get this working?

Find below my app configuration:

Program

private static IHostBuilder CreateHostBuilder(string[] args)
    => Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
        .ConfigureWebHostDefaults(webHostBuilder =>
        {
            webHostBuilder.UseStartup<Startup>();
        });

Startup

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            // [...]
        });

    services.AddControllers();

    services.AddAutofacMultitenantRequestServices();
}

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<ClaimTenantIdentificationStrategy>()
        .As<ITenantIdentificationStrategy>()
        .SingleInstance();

    builder.RegisterType<SomeService>()
        .As<ISomeService>()
        .InstancePerTenant();
}

public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
    => new MultitenantContainer(container.Resolve<ITenantIdentificationStrategy>(), container);

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

TenantIdentificationStrategy implementation

public class ClaimTenantIdentificationStrategy : ITenantIdentificationStrategy
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ClaimTenantIdentificationStrategy(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    public bool TryIdentifyTenant(out object tenantId)
    {
        tenantId = null;

        var claimsPrincipal = _httpContextAccessor.HttpContext?.User;
        var claimsIdentity = claimsPrincipal?.Identity;

        if (claimsIdentity != null && claimsIdentity.IsAuthenticated)
        {
            var identifier = claimsPrincipal.FindFirst("tenantId")?.Value;

            if (!string.IsNullOrEmpty(identifier))
                tenantId = identifier;
        }

        return tenantId != null;
    }
}

EDIT: TenantIdentificationStrategy

public class ClaimTenantIdentificationStrategy : ITenantIdentificationStrategy
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ITenantStore _tenantStore;

    public ClaimTenantIdentificationStrategy(IHttpContextAccessor httpContextAccessor, ITenantStore tenantStore)
    {
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        _tenantStore = tenantStore ?? throw new ArgumentNullException(nameof(tenantStore));
    }

    public bool TryIdentifyTenant(out object tenantId)
    {
        var httpContext = _httpContextAccessor.HttpContext;

        tenantId = httpContext?.Items["TenantId"];

        if (tenantId != null)
            return true;

        var authorizationHeaderValue = httpContext?.Request?.Headers?[HeaderNames.Authorization];

        if (authorizationHeaderValue.HasValue && !StringValues.IsNullOrEmpty(authorizationHeaderValue.Value))
        {
            var authorizationHeader = AuthenticationHeaderValue.Parse(authorizationHeaderValue);

            var jwtTokenHandler = new JwtSecurityTokenHandler();
            var jwtToken = jwtTokenHandler.ReadJwtToken(authorizationHeader.Parameter);

            var tenantIdClaim = jwtToken.Claims
                .FirstOrDefault(x => x.Type == "tenantId");

            if (tenantIdClaim != null && _tenantStore.IsValidTenant(tenantIdClaim.Value))
            {
                tenantId = tenantIdClaim.Value;
                httpContext.Items["TenantId"] = tenantId;

                return true;
            }
        }

        return false;
    }
}

Solution

  • Something to consider when reading the Autofac docs is that Autofac supports any application type - console app, Windows service, ASP.NET app, whatever. So when you see that the tenant can come from the user's principal... well, it can. Whether your app has it ready to go at the time is up to your app. The console might get the principal from the currently executing thread and it's already there when the app runs.

    For ASP.NET / ASP.NET Core apps, there's middleware that runs to validate an inbound token, convert that token into a principal, and attach that principal to the current request.

    But if you think about it... middleware has dependencies that need to be resolved from the request container, and in a multitenant system that means we need to know the tenant first thing in the middleware pipeline, not wait until the authentication/authorization bits run.

    This was a problem in ASP.NET classic, too. It's not new to ASP.NET Core. I've hit it with my own apps, too.

    How you solve it is going to be largely app specific - what type of token you're using, how expensive it is to validate the token, how much risk you want to take security-wise, etc.

    In my most recent run-in with this, we were using signed (but not encrypted) JWT coming in. One of the claims, tid, had the tenant ID in it. What we did was decide to do some manual work on the token - validate the signature, then just grab the tid claim out of the token directly. No more parsing, no other validation (audience, etc.). It needed to be fast. We then cached the tenant ID in the HttpContext.Items so we didn't have to look it up again and so it could be used anywhere else we needed the tenant ID that ran before authentication middleware.

    There was a slight risk that someone can get a different tenant's pipeline to execute from initial request up to the authentication middleware and then get rejected by the authentication middleware. We were OK with that since it was still pretty quickly going to reject unauthorized folks. That may or may not be OK for you, so you'll have to decide.

    But, the long and the short of it was: Due to the race condition of needing the tenant ID before the authentication middleware has run, we had to figure something else out. It looks like you, too, are going to have to figure something else out.