Skip to main content

Command Palette

Search for a command to run...

🏢 Building a Secure Multi-Tenant .NET 8 API with JWT Authentication and Dynamic Connection Strings

Updated
5 min read
🏢 Building a Secure Multi-Tenant .NET 8 API with JWT Authentication and Dynamic Connection Strings
D

I'm a highly motivated and experienced developer expertise in leveraging the power of .NET Core Technology. Currently collaborating with an Australian company based in Nusa Dua, Bali, Indonesia, to deliver innovative application development services that push the boundaries of what technology can achieve, and also contribute to the ever-evolving landscape of the global IT industry

In this guide, we’ll build a multi-tenant .NET 8 Web API where each tenant (for example, AWX and NSW) has its own SQL Server database, and authentication is handled securely via JWT tokens that include the tenantId claim.


🧠 Why Multi-Tenancy?

Imagine you’re building a SaaS app serving multiple organizations each wants their data isolated in their own database, but you want to maintain a single codebase.
This is where multi-tenancy comes in.

Each tenant will have:

  • its own connection string

  • its own users and data

  • secure access via JWT claims


🧩 Project Overview

We’ll implement:

  1. JWT Authentication

  2. Dynamic tenant database switching

  3. Tenant Middleware

  4. Database Seeding

  5. Migration-ready EF Core setup

Example tenants

Tenant IDDatabase NameConnection String
AWXTenant_AWXServer=.;Database=Tenant_AWX;Trusted_Connection=True;
NSWTenant_NSWServer=.;Database=Tenant_NSW;Trusted_Connection=True;

⚙️ Step 1. Install Required Packages

Run the following NuGet commands:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Swashbuckle.AspNetCore

🏗️ Step 2. Define Models

Models/Tenant.cs

public class Tenant
{
    public int Id { get; set; }
    public string TenantId { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string ConnectionString { get; set; } = string.Empty;
}

Models/User.cs

public class User
{
    public int Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public string TenantId { get; set; } = string.Empty;
}

🗄️ Step 3. Setup the AppDbContext

Data/AppDbContext.cs

using Microsoft.EntityFrameworkCore;
using MultiTenantApi.Models;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Tenant> Tenants => Set<Tenant>();
    public DbSet<User> Users => Set<User>();
}

🧩 Step 4. Tenant Database Factory

We use this to dynamically switch the database based on tenant ID.

Data/ITenantDbContextFactory.cs

public interface ITenantDbContextFactory
{
    AppDbContext CreateDbContext(string tenantId);
}

Data/TenantDbContextFactory.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

public class TenantDbContextFactory : ITenantDbContextFactory
{
    private readonly IConfiguration _config;
    public TenantDbContextFactory(IConfiguration config)
    {
        _config = config;
    }

    public AppDbContext CreateDbContext(string tenantId)
    {
        var connectionString = _config.GetConnectionString(tenantId)
            ?? throw new Exception($"Connection string for tenant '{tenantId}' not found.");

        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlServer(connectionString)
            .Options;

        return new AppDbContext(options);
    }
}

🧱 Step 5. Middleware for Tenant Resolution

This middleware reads the tenant ID from JWT claims and makes it available for other services.

Middleware/TenantMiddleware.cs

using System.Security.Claims;

public class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var tenantId = context.User.FindFirstValue("tenantId");
        if (!string.IsNullOrEmpty(tenantId))
        {
            context.Items["TenantId"] = tenantId;
        }
        await _next(context);
    }
}

public static class TenantMiddlewareExtensions
{
    public static IApplicationBuilder UseTenantResolver(this IApplicationBuilder app)
    {
        return app.UseMiddleware<TenantMiddleware>();
    }
}

🔐 Step 6. Authentication Services

Services/IUserService.cs

public interface IUserService
{
    Task<bool> IsValidUserAsync(string username, string password, string tenantId);
    string GenerateJWTToken(string username, string tenantId);
}

Services/UserService.cs

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.Text;

public class UserService : IUserService
{
    private readonly ITenantDbContextFactory _factory;
    private readonly IConfiguration _config;

    public UserService(ITenantDbContextFactory factory, IConfiguration config)
    {
        _factory = factory;
        _config = config;
    }

    public async Task<bool> IsValidUserAsync(string username, string password, string tenantId)
    {
        using var db = _factory.CreateDbContext(tenantId);
        var user = await db.Users.FirstOrDefaultAsync(u =>
            u.Username == username && u.Password == password && u.TenantId == tenantId);

        return user != null;
    }

    public string GenerateJWTToken(string username, string tenantId)
    {
        var jwtKey = _config["Jwt:Key"];
        var issuer = _config["Jwt:Issuer"];
        var audience = _config["Jwt:Audience"];

        var claims = new[]
        {
            new Claim(ClaimTypes.Name, username),
            new Claim("tenantId", tenantId)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey!));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            claims: claims,
            expires: DateTime.UtcNow.AddHours(1),
            signingCredentials: creds);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

🧰 Step 7. Auth Controller

Controllers/AuthController.cs

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IUserService _userService;

    public AuthController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginRequest model)
    {
        var isValid = await _userService.IsValidUserAsync(model.Username, model.Password, model.TenantId);
        if (!isValid)
            throw new Exception("User Not Found");

        var token = _userService.GenerateJWTToken(model.Username, model.TenantId);
        return Ok(new { token });
    }
}

public record LoginRequest(string Username, string Password, string TenantId);

🌱 Step 8. Seed Data for Tenants and Users

Data/AppDbContext.cs (add inside OnModelCreating)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Tenant>().HasData(
        new Tenant { Id = 1, TenantId = "AWX", Name = "AWX Org", ConnectionString = "Server=.;Database=Tenant_AWX;Trusted_Connection=True;" },
        new Tenant { Id = 2, TenantId = "NSW", Name = "NSW Org", ConnectionString = "Server=.;Database=Tenant_NSW;Trusted_Connection=True;" }
    );

    modelBuilder.Entity<User>().HasData(
        new User { Id = 1, Username = "john.awx", Password = "Password0", TenantId = "AWX" },
        new User { Id = 2, Username = "mary.nsw", Password = "Password0", TenantId = "NSW" }
    );
}

🚀 Step 9. Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

var jwtConfig = builder.Configuration.GetSection("Jwt");

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtConfig["Issuer"],
            ValidAudience = jwtConfig["Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig["Key"]!))
        };
    });

builder.Services.AddAuthorization();

builder.Services.AddScoped<ITenantDbContextFactory, TenantDbContextFactory>();
builder.Services.AddScoped<IUserService, UserService>();

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("AWX"))); // for migration

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();
app.UseTenantResolver();
app.MapControllers();
app.Run();

⚡ Step 10. appsettings.json

{
  "ConnectionStrings": {
    "AWX": "Server=.;Database=Tenant_AWX;Trusted_Connection=True;TrustServerCertificate=True;",
    "NSW": "Server=.;Database=Tenant_NSW;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "Jwt": {
    "Key": "SuperSecretKeyForJwt",
    "Issuer": "MultiTenantApi",
    "Audience": "MultiTenantApiUsers"
  }
}

🧪 Testing

Login Request:

POST /api/auth/login
Content-Type: application/json

{
  "username": "john",
  "password": "123",
  "tenantId": "AWX"
}

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

This token includes "tenantId": "AWX" in its claims — and the middleware ensures all requests for that token connect to AWX’s database.


✅ Summary

ConceptDescription
TenantDbContextFactoryCreates DbContext dynamically using the correct tenant connection string.
TenantMiddlewareExtracts tenantId from JWT and injects into request context.
Seed DataProvides demo tenants and users for testing.
JWT AuthenticationEnsures each user logs in and receives a token tied to their tenant.

💡 Final Thoughts

This architecture is scalable, secure, and migration-friendly — perfect for SaaS systems serving multiple organizations.
Each tenant remains logically isolated, yet the API maintains one unified codebase.