Security for multitenant web applications with ASP.NET Core

In this post I’m going to show you how to enable security per tenant for the multitenant web application created in my previous blog post.

1. Create new application with .NET server-side project, add new MSSQL data-source connected to our Sample database and auto-generate pages.

2. Run the application from Radzen to generate everything needed and add the following files to application ignore list to tell Radzen to ignore them during subsequent code generation.

client\src\environments\environment.ts
server\Program.cs

3. Add the following Multitenancy section in server\appsettings.json with information about tenants, their hostnames and connection strings. We specify two hostnames per tenant - one for debug mode (so you can test while developing the application) and one for production (which your users will browse).

{
  ...
  "Multitenancy": {
    "Tenants": [
      {
        "Name": "Tenant1",
        "Hostnames": [
          "localhost:5001",
          "tenant1.radzen-rocks.com"
        ],
        "ConnectionString": "Server=.;Initial Catalog=Sample-Tenant1;Persist Security Info=False;User ID=sa;Password=yourpassword;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
      },
      {
        "Name": "Tenant2",
        "Hostnames": [
          "localhost:5002",
          "tenant2.radzen-rocks.com"
        ],
        "ConnectionString": "Server=.;Initial Catalog=Sample-Tenant2;Persist Security Info=False;User ID=sa;Password=yourpassword;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"
      }
    ]
  }
}

4. Enable default security for the application and auto-generate pages for user and role management.

5. Add server\Data\SampleContext.Custom.cs and server\Data\ApplicationIdentityDbContext.Custom.cs with the following code to read the tenants.

SampleContext.Custom.cs

public partial class SampleContext
{
    private readonly HttpContext context;
    private readonly Multitenancy multitenancy;

    public SampleContext(DbContextOptions<SampleContext> options, ApplicationIdentityDbContext identityDbContext, IHttpContextAccessor httpContextAccessor, Multitenancy mt) : base(options)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
        Database.EnsureCreated();
        identityDbContext.Database.Migrate();
    }

    public SampleContext(IHttpContextAccessor httpContextAccessor, Multitenancy mt, ApplicationIdentityDbContext identityDbContext)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
        Database.EnsureCreated();
        identityDbContext.Database.Migrate();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (multitenancy != null && context != null)
        {
            var tenant = multitenancy.Tenants
                    .Where(t => t.Hostnames.Contains(context.Request.Host.Value)).FirstOrDefault();

            if (tenant != null)
            {
                optionsBuilder.UseSqlServer(tenant.ConnectionString);
            }
        }
    }
}

ApplicationIdentityDbContext.Custom.cs

public partial class ApplicationIdentityDbContext
{
    private readonly HttpContext context;
    private readonly Multitenancy multitenancy;

    public ApplicationIdentityDbContext(DbContextOptions<ApplicationIdentityDbContext> options, IHttpContextAccessor httpContextAccessor, Multitenancy mt) : base(options)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
    }

    public ApplicationIdentityDbContext(IHttpContextAccessor httpContextAccessor, Multitenancy mt)
    {
        context = httpContextAccessor.HttpContext;
        multitenancy = mt;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (multitenancy != null && context != null)
        {
            var tenant = multitenancy.Tenants
                    .Where(t => t.Hostnames.Contains(context.Request.Host.Value)).FirstOrDefault();

            if (tenant != null)
            {
                optionsBuilder.UseSqlServer(tenant.ConnectionString);
            }
        }
    }
}

6. Add UseUrls() to server\Program.cs to specify the urls the web host will listen on for each tenant in debug mode.

public class Program
{
    public static void Main(string[] args)
    {
        var host = BuildWebHost(args);

        host.Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseKestrel()
            .UseUrls("http://localhost:5001", "http://localhost:5002")
            .UseStartup<Startup>()
            .Build();
}

7. Specify Angular application data services url for default tenant for debug (client\src\environments\environment.ts).

client\src\environments\environment.ts - for debug

export function dataSourceRoot(): string {
  return 'http://localhost:5001';
}

export const environment = {
  sample: `${dataSourceRoot()}/odata/Sample`,
  securityUrl: `${dataSourceRoot()}/auth`,
  production: false,
};

8. Run the application from Radzen and login using admin/admin to access Tenant1 database. You can now create roles and users for Tenant1.

9. Modify .env and client\src\environments\environment.ts to set default tenant to Tenant2.

.env

ConnectionStrings__SampleConnection="Server=.;Initial Catalog=Sample-Tenant2;Persist Security Info=False;User ID=sa;Password=pwd;MultipleActiveResultSets=False;Encrypt=false;TrustServerCertificate=true;Connection Timeout=30"

client\src\environments\environment.ts

export function dataSourceRoot(): string {
  return 'http://localhost:5002';
}
...

10. Run the application and login using admin/admin to access Tenant2 database and add roles and users for Tenant2.

You have now separate security for each tenant!

Both ApplicationIdentityDbContext and SampleDataContext will retrieve runtime the tenant connection string depending on the application host:

To deploy the application please follow the instructions in my previous blog post.

Source Code

Enjoy!

Leverage Radzen on LinkedIn

Yes, we are on LinkedIn and you should follow us!

Now, you have the opportunity to showcase your expertise by adding Radzen Blazor Studio and Radzen Blazor Components as skills to your LinkedIn profile. Present your skills to employers, collaborators, and clients.

All you have to do is go to our Products page on LinkedIn and click the + Add as skill button.

by Vladimir Enchev

Visually design Blazor application

In this blog post I’m going to show you how to use the powerful Radzen designer to visually design your Blazor application.
Read more