0

I am currently trying my hand at a microservices architecture for the first time, and I am looking to put together a multi-tenant application built on a this architecture. Tenants are created with their own subdomain, and the tenant owner can create further user accounts linked to that tenant

I currently have the identity api set up, and was thinking of composing the rest a bit like the following:

enter image description here

The Gateways are intended to be implemented as Backend-For-Frontend and would aggregate data as necessary to satisfy the client request to that gateway.

In the identity API, I use the SaasKit middleware to check the subdomain and get tenant details. I was wondering what would be the best approach to apply this tenant discovery across the rest of the services? I am wary of creating a coupling that would undermine the autonomy of microservices. Would I do my tenant discovery in the gateways and pass the tenant ID to the microservices when requests are made to the services, should I be holding local copies of tenant information in each service, or should I use SaasKit in each service and call out to the identity API in each service to get tenant information if its not already cached?

EDIT: To add some context on to how tenants are created; The tenants are created via an API call from a separate system which provides a JWT created by a central authentication service separate to this. Users are also created this way, but the users created here are authenticated here rather than the 'other' authentication service

4
  • The Gateways are intended to be implemented as Backend-For-Frontend and would aggregate data as necessary to satisfy the client request to that gateway. No, Gateways are intended to provide a single entry point for several and different APIS. The APIs don't even need to be related. It's an infrastructure element unrelated to the domain or functionality of the application. API Gateways might seem a good place for aggregates but they are not. The same way we don't make aggregates in our HTTP Server or balancers. – Laiv Jul 23 '19 at 9:52
  • I'm not sure I understand your point there? I am trying to implement a gateway per user interface, similar to what is described at: samnewman.io/patterns/architectural/bff which talks explicitly about aggregation of calls. – Steven Brookes Jul 23 '19 at 10:25
  • @Steven Brookes How did you solve this problem? I have the same situation and using the same middleware SaasKit. Another question, do all of your microservices endpoints have a tenant_id parameter? In other words, how did you get the tenant information inside a microservice? – Guilherme Ferreira Oct 16 '20 at 13:30
  • @GuilhermeFerreira I utilized an approach similar to what panda submitted. I will add an answer with what I implemented – Steven Brookes Oct 16 '20 at 13:48
3

Since you are using micro-services I am guessing that you are using token based authentication/authorization which means that you have a signed token in which you can securely pass data from a client to a service and from service to service to authorize requests. If not I would suggest to do so because this will enable you to:

  • Capture/Recognize the tenant information in the API Gateway through the identity service. The identity service would return the auth token which will include the tenant id.

  • There is no need to keep local copies of tenant ids or make extra calls to the identity server since the tenant id is already in the token

  • In order to acquire specific tenant configurations/access rights, caching may be required to cache data per tenant for quick retrieval.

3
  • That's a good suggestion, and something that did come to mind. The question I have on that though is, would that make the subdomain redundant after login? If I login on tenant1.mydomain.com, and the token contains the tenant ID, I wouldn't envisage I should be allowed to goto tenant2.mydomain.com and do something, even if the checks are done at token level post-login – Steven Brookes Jul 23 '19 at 10:21
  • Also, I have edited my initial post to add some info on tenant/user creation. There would already be tokens provided by an external service in those contexts, and those tokens would also be used for some of the API gateway calls – Steven Brookes Jul 23 '19 at 10:31
  • @StevenBrookes, this is a good point. If you need to enforce domain accessibility this is a check that can be done by the API Gateway to ensure that the tenant-id matches the domain name. However, if this is a SaaS environment, wouldn't domain access be used just for tenant identification purposes? If this is the case then even if users access a different domain, they would be treated for the tenant they have been authenticated with since their tenant-id will not change. – panda Jul 26 '19 at 14:23
1

The implementation I used relied on SaaS kit purely for the Identity service to use the domain detected for authenticating against the correct tenant. From when I the JWT passed to the the gateway and in turn to the underlying services contained a tid field for the tenant ID

I then used that in my Entity Framework context. As an example:

public class MyDbContext : DbContext
{
    private readonly ITenantProvider _tenantProvider;
    private Guid? TenantId => _tenantProvider.GetTenantId();

    public MyDbContext(DbContextOptions<MyDbContext> options, ITenantProvider tenantProvider) : base(options)
    {
        _tenantProvider = tenantProvider;
    }

    public DbSet<MyModel> MyModels { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<MyModel>().HasQueryFilter(s => TenantId != null && s.TenantId == TenantId);
    }
}

And implemented by ITenantProvider like so. I was checking for both tid and the url for tenantid as the default for JWT is to use the URL if you add a tid property, as described here:

public class HttpContextTenantProvider : ITenantProvider
    {
        private readonly HttpContext _httpContext;

        public HttpContextTenantProvider(IHttpContextAccessor httpContextAccessor)
        {
            _httpContext = httpContextAccessor.HttpContext;
        }

        public Guid? GetTenantId()
        {
            if (_httpContext?.User == null)
            {
                return null;
            }

            var tenantIdClaim = _httpContext.User.Claims.FirstOrDefault(c => c.Type.Equals("http://schemas.microsoft.com/identity/claims/tenantid", StringComparison.InvariantCultureIgnoreCase) || c.Type.Equals("tid", StringComparison.InvariantCultureIgnoreCase));

            if (tenantIdClaim == null)
            {
                return null;
            }

            return Guid.Parse(tenantIdClaim.Value);
        }
    }
2
  • Thanks for the answer. One thing I haven't been able to understand yet is: when the API Gateway is going to make an HTTP GET to a service A, do you pass tenant_id as a parameter? That is, do all your services expect to receive a tenant_id parameter? Or all yours services get tenant_id from the user Claim? – Guilherme Ferreira Oct 16 '20 at 17:47
  • 1
    I pass the JWT that the gateway received, so they all authenticate requests the same as the gateway. That is why my HttpContextTenantProvider is looking at the User object in HttpContext, as it is hooked into the JWT middleware – Steven Brookes Oct 16 '20 at 18:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.