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.
Enjoy!