Blazor Azure AD Multitenancy Part 2:Eliminating The Use Of HttpContext

In this video I create an Blazor Server Multitenant App that uses Azure AD for authentication. The use of HttpContext for implementing multitenancy is eliminated in this tutorial as recommended by Microsoft.
The process flow is as follows:

  1. User opens a browser and accesses the app
  2. User is authenticated at Azure AD.
  3. If user is valid user in any of the tenants, user is logged in on home page
  4. If user tries to access any data driven page, the application checks whether the user is accessing the data on the right tenant(URL). If user is accessing data on the right tenant, data is displayed. Otherwise a data connection is created to the dummy tenant which contains no data and blank list with no data is presented to user.

Blazor C# code can be accessed at GitHub - benjaminsqlserver/BlazorADMultitenancyWithoutHttpContext: Blazor Server Multitenant App Which Uses Azure AD For Authentication. The use of httpcontext for tenant differentiation is eliminated in this app.

Transact-SQL Query files for the three database tenants used in this video can be accessed at GitHub - benjaminsqlserver/BlazorADTenantsSQL: Code repo for database files for https://github.com/benjaminsqlserver/BlazorADMultitenancyWithoutHttpContext

I also saw the Url strategy for determining the current tenant overly complex to realise in Azure. Quite simple on a local IIS env but for serverless it was nowhere near a "Zen" experience.

I found however a fairly simple work around that has 2 caveats.

  1. adding users to Roles has to be done manually in the db (yeah I know not nice but working on it...)
  2. users emails are unique across all Tenants.

Step 1
I created an overload for the GetTenant() function as:

    /// <summary>
    /// Overloaded to allow use by FindByNameAsync override
    /// </summary>
    /// <param name="normalizedEmail"></param>
    /// <returns></returns>
    private ApplicationTenant GetTenant(string normalizedEmail)
    {
        var tenants = Context.Set<ApplicationTenant>().ToList();

        var user = Context.Set<ApplicationUser>().Where(u => u.NormalizedEmail == normalizedEmail).FirstOrDefault();

        return tenants.Where(t => t.Id == user.TenantId).FirstOrDefault();

        //var host = httpContextAccessor.HttpContext.Request.Host.Value;

        //return tenants.Where(t => t.Hosts.Split(',').Where(h => h.Contains(host)).Any()).FirstOrDefault();
    }

Step 2)
In the ApplicationIdentityDbContext.FindByNameAssync method I used the new overload of GetTenant()


        public override async Task<ApplicationUser> FindByNameAsync(string normalizedEmail, CancellationToken cancellationToken = default)
        {
            if (normalizedEmail.ToLower() == "tenantsadmin")
            {
                return await base.FindByNameAsync(normalizedEmail, cancellationToken);
            }

            var tenant = GetTenant(normalizedEmail); // calling the new overloaded method

            ApplicationUser user = null;

            if (tenant != null)
            {
                user = await Context.Set<ApplicationUser>().SingleOrDefaultAsync(r => r.NormalizedUserName == normalizedEmail && r.TenantId == tenant.Id, cancellationToken);
            }

            return user;
        }
    }

NOTE: For all your DataService calls to GetObjects ensure you have a filter by your Tenant Id column if the are multi-tenanted!

Example:

  public async Task<IQueryable<Models.FOO.MyObject>> GetMyObjects(Query query = null)
  {
    var items = Context.MyObjects.Where(p => p.TenantId == securityService.Tenant.Id).AsQueryable();

//....