Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

ASP.NET Core Web API Multi-Tenant JWTs

April 10, 2018
dotnet

Authentication via a JWT is pretty much standard practice these days and there are lots of blog posts and sample code showing how to do this in ASP.NET Core. However, what if we are implementing a multi-tenant API and want the JWT signing key secret to be different for each tenant? In this post we go through how to implement a multi-tenant JWT.

ASP.NET Core Web API Multi-Tenant Authentication with JWTs and Dapper

JWT Generation

First we need an end point that is going to give us a token in exchange for valid credentials. Below is a controller that gives us a good start, validating the user credentials and also the API key. Note that we use the API key to determine the tenant. We’ll come back to this code later to implement the code that generates the JWT.

[Route("api/auth")]
public class AuthController : Controller
{
    private readonly IConfiguration _configuration;
    private readonly Tenants _tenants;
    public AuthController(IConfiguration configuration, Tenants tenants)
    {
        _configuration = configuration;
        _tenants = tenants;
    }

    [AllowAnonymous]
    [HttpPost]
    public IActionResult RequestToken([FromBody] UserCredentials userCredentials)
    {
        // Check the API key is correct
        Tenant tenant = null;
        string requestAPIKey = Request.Headers["x-api-key"].FirstOrDefault();
        if (!string.IsNullOrEmpty(requestAPIKey))
        {
            tenant = _tenants.Where(t => t.APIKey.ToLower() == requestAPIKey.ToLower()).FirstOrDefault();
        }
        if (tenant == null)
        {
            return Unauthorized();
        }

        // Check the user credentials and return a 400 if they are invalid
        if (!ValidateCredentials(userCredentials))
        {
            return BadRequest();
        }

        // TODO - generate and return the token ...

    }

    private bool ValidateCredentials(UserCredentials credentials)
    {
        // Not for production !!!
        return true;
    }
}

Below is our tenant model with it’s associated API key and a secret key we need to use in the JWT signing process:

public class Tenant
{
    public string TenantId { get; set; }
    public string SecretKey { get; set; }
    public string APIKey { get; set; }
}

We also have a Tenants class that gives us some hardcoded tenants for testing.

public class Tenants: List<Tenant>
{
    public Tenants()
    {
        Add(new Tenant { TenantId = "55EC325B-4993-4421-84A6-3312FE7D26CF", SecretKey = "7BD9CDBC-B3C1-47E1-88F4-DEC98A2F7403", APIKey = "4C4505CF-C658-4D69-A2D7-A027DCAE749E" });
        Add(new Tenant { TenantId = "C2D79825-9F57-4C38-8FE5-676FC49D0C93", SecretKey = "B270D88D-342E-49F8-8FB5-5DA1613EF1EE", APIKey = "FA9279AB-1BB3-4F0B-999F-51697CB4031F" });
        Add(new Tenant { TenantId = "6572D148-6E31-405A-8D6E-E69F69A0A2AF", SecretKey = "A520D71F-E8E4-40E4-BC4A-E74F4101BAB8", APIKey = "4837CD06-4A13-462D-88D0-E042F47501FB" });
    }
}

So, let’s do the JWT generatation now. We retreive the issuer and expiry duration from appsettings.json. We set the audience and something called a kid to the API key. So, what’s the kid? it’s a hint indicating which key was used to secure the JWS. The kid will play a crucial part in our code to validate the JWT.

public IActionResult RequestToken([FromBody] UserCredentials userCredentials)
{
    // Check the API key is correct
    ...

    // Check the user credentials and return a 400 if they are invalid
    ...

    // Get the token config from appsettings.json
    string issuer = _configuration["Token:Issuer"];
    int expiryDuration;
    if (!int.TryParse(_configuration["Token:ExpiryDurationMins"], out expiryDuration))
    {
        expiryDuration = 30;
    }
    DateTime expiry = DateTime.UtcNow.Add(TimeSpan.FromMinutes(expiryDuration));

    // Create and sign the token
    var jwtSecurityToken = new JwtSecurityToken(
        issuer: issuer,
        audience: requestAPIKey,
        claims: new[]
        {
            new Claim(ClaimTypes.Name, userCredentials.UserName)
        },
        expires: expiry,
        signingCredentials: new SigningCredentials(
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tenant.SecretKey)),
            SecurityAlgorithms.HmacSha256
        )
    );
    jwtSecurityToken.Header.Add("kid", requestAPIKey);
    var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);

    // return the token and when it expires
    return Ok(new
    {
        AccessToken = token,
        Expiry = expiry
    });
}

With our endpoint in place, we can now generate a multi-tenant JWT:

Getting a multi-tenant JWT

Authentication

On to the code that validates the JWT …

First we switch JWT authentication on in the Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Tenants>();

    // Get access to the tenants
    var tenants = services.BuildServiceProvider().GetService<Tenants>();

    // Setup the JWT authentication
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                // TODO - specify how to validate the JWT
            };
        });

    services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Turn on authentication
    app.UseAuthentication();

    app.UseMvc();
}

Now comes the interesting part …

We specify that we want to check the issuer, audience, lifetime as well as the signing key. We specify the correct issuer from appsettings.json. We then specify the valid audiences from the tenant API keys. The most interesting bit (and the bit that took me a while to figure out) is how the signing key is checked. We use the IssuerSigningKeyResolver delegate. Notice that we pass the kid from the JWT into the delegate. The delegate finds the tenant from kid (our API key) and uses it’s secret key to produce the signing key.

...
// Setup the JWT authentication
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                // Specify what in the JWT needs to be checked
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,

                // Specify the valid issue from appsettings.json
                ValidIssuer = Configuration["Token:Issuer"],

                // Specify the tenant API keys as the valid audiences
                ValidAudiences = tenants.Select(t => t.APIKey).ToList(),

                IssuerSigningKeyResolver = (string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) =>
                {
                    Tenant tenant = tenants.Where(t => t.APIKey == kid).FirstOrDefault();
                    List<SecurityKey> keys = new List<SecurityKey>();
                    if (tenant != null)
                    {
                        var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tenant.SecretKey));
                        keys.Add(signingKey);
                    }
                    return keys;
                }
            };
        });
...

Checking an Authorised Endpoint

Let’s implement a nice little authorised endpoint to test all this:

[Authorize]
[Route("api/contacts")]
public class ContactController : Controller
{
    [HttpGet("{id}")]
    public IActionResult GetById(string id)
    {
        return Ok(new { Id = id, FirstName = "Fred", Surname = "Smith" });
    }
}

If we try it without the Authorization HTTP header we should get a 401:

Unauthorized request

If we try with the token we generated earlier, we get access to the data:

Authorized request

Conclusion

The key bit to implementing a multi-tenant JWT in ASP.NET core is using the kid to identify the tenant. We simply include it in the JWT header during generation and then use the IssuerSigningKeyResolver delegate to check it during the JWT validation process.


Comments

Ieuan Walker April 11, 2018

Great post, but will you be doing a follow up on how to add roles to different secret keys?


Monsignor April 13, 2018

If I am a tenant in the system and I somehow learned the ApiKey of some other tenant I can use my credentials to access that tenants data. Because all I need is to pass that key in the x-api-key header during retrieval of the token. Am I right?

Carl April 14, 2018

Good question Monsignor, In addition to the API key you will need to pass valid user credentials in the HTTP body and the user credentials are per tenant. So, if I manage to get hold of another tenant’s API key, I can’t get access to the tenants data unless I have valid credentials for that tenant.

Monsignor April 16, 2018

User credentials are per tenant According to the RequestToken method the user credentials are checked before the API key is even read from the headers. I don’t see a second check that would ensure the credentials match that API key. I have an impression that any credentials of any tenant would satisfy the RequestToken method.

Carl April 17, 2018

Good spot Monsignor. Checking the API key should come before checking the user credentials in RequestToken()


ong January 3, 2019

Thanks. I have been searching this for a week. I tried it and it works flawlessly. Keep the good work.


ro January 24, 2019

Would this work in a web farm?

Carl January 24, 2019

Thanks for the question. I can’t think of any reason why it wouldn’t work in a web farm

ro January 28, 2019

Thanks for your answer Carl. I have found an issue here. You set all tenants as valid audience. If you login as, lets say Tenant1, get the token and list customers, everything is fine and you get the customers. But if you take the same token (token generated by Tenant1) and make a request to Tenant2, as Tenant2 is a valid tenant, and the token is a valid token, you get the list of customers of Tenant2! So there should be anywhere a validation where checks that the kid is generated by the same tenant in the current request. Please correct me if I’m wrong! 🙂 Here is my fix :

IssuerSigningKeyResolver = (string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) =>
{
    List keys = new List();
    var tenantManager = services.BuildServiceProvider().GetRequiredService();
    // TODO:CACHÉ
    var tokenTenant = tenantManager.GetTenants().FirstOrDefault(t => t.APIKey == kid);
    var requestTenant = tenantManager.GetRequestTenant(); // get the current request tenant
    if (tokenTenant.Id == requestTenant.Id)
    {
        gKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(tokenTenant.SecretKey));
        keys.Add(signingKey);
    }
    return keys;
}

Andreas October 25, 2019

How do you handle key rotations? Right now it seems like the app will just stop working (especially if you have RS256 tokens and the issuer rotates key pairs).

Ideally you would want to invoke some public key cache in IssuerSigningKeyResolver, but at that point you don’t have access to the application DI container.

If you to learn about using React with ASP.NET Core you might find my book useful:

ASP.NET Core 5 and React

ASP.NET Core 5 and React
Find out more

Want more content like this?

Subscribe to receive notifications on new blog posts and courses

Required
© Carl Rippon
Privacy Policy