MultiTenancy

Four steps to Teams success

GroupHive is a multi tenant application. And even though this feature was developed for GroupHive it can also be used in other applications.
This article describes the basic concepts.

Main and client tenants

In each multi tenant federation there has to exist exactly one main organization (tenant) plus an arbitrary number of client organizations. The decision which tenant serves as main and which as client is persisted in configuration files:
There is always an „AllTenants“ file that could look like this:

{
    "Tenants": ["83b5ab59-e45f-489c-a25b-98a53bfd8861", "2ef09b02-2e8f-4181-8bb2-78cfc813799a"]
}

This file is very simple. It contains just a single array with all the AAD Tenant ids that should be part of the federation.
Each tenant needs now an additional configuration that could look like this:

{
    "Id": "hfmg",
    "AzureAdTenantId": "2ef09b02-2e8f-4181-8bb2-78cfc813799a",
    "Environments": [{
            "Id": "prod",
            "ApplicationInsightsInstrumentationKey": "2c3aa1fa-8cb7-43d4-9af7-xxxxxxxxxxxx",
            "DBConnectionStringList": ["xxxxxxxxxxx"],
            "ServiceBusConnectionString": "ServiceBusConnectionString",
            "TopicName": "gh-hfmg-prod",
            "ServiceMap": ["prod", "prod", "prod", "prod", "prod"]
        }, {
            "Id": "test",
            "ApplicationInsightsInstrumentationKey": "2c3aa1fa-8cb7-43d4-9af7-xxxxxxxxxxxxxxxx",
            "DBConnectionStringList": ["xxxxxxxxxxxxxxxxxxxxxx"],
            "ServiceBusConnectionString": "ServiceBusConnectionString",
            "ServiceMap": ["test", "test", "test", "test", "test"]
        }
    ]
}

Or for the main tenant like this:

{
    "Id": "cphf",
    "AzureAdTenantId": "83b5ab59-e45f-489c-a25b-98a53bfd8861",
    "IsMainTenant": true
}

Client tenants always need an „Environments“ array. This array has to contain at least one environment, but it could also contain more than just that. An environment is a seperate „instance“ of the application that has its own infrastructure (ie database, service bus…)

Main tenants always need a property „IsMainTenant“ that is set to „true“.
Important: There has to be exactly one main tenant.

App registration

For each tenant federation a multi tenant app registration* has to be created in Azure AD. This application has to reside in one of the organizations whose tenant id appears in the AllTenants configuration. Usually this is the main tenant, but not necessarily.
For example GroupHive has its app registration in its own tenant and GroupHives tenant id is marked as „main“ in the configuration. The HF Mixing Group tenant is then installing the GroupHive app as an enterprise application in its own tenant.
But the customer portal what is also a federation of tenants has the main app registration in the HF Mixing Group tenant.
However, the main tenant of the CustomerPortal is GroupHive. This is because the HF Mixing Group wants to be a user of the CustomerPortal, but the user should not be the main at the same time.
More about this later.

*This app should accept accounts of all organizations:

Development

Once the app has been created, the development of a multi tenant application can start.
Lets say that the example application consists of:

  • A web api (Asp.Net Core)
  • A web application (ie Angular SPA)
  • A daemon application running in the background

The web api has to install the NuGet package „DataHive.MultiTenancy“.
The package enables the web api to be part of the multi tenant federation.
It can be called in different ways now:

  • In the context of a user of the main organization
  • In the context of a user of one of the client organizations
  • In the context of an app of the main organization
  • In the context of an app in one of the client organizations

The last point will only work if the client organization creates her own app registration that adds at least one api permission from the root organizations app registration.

Lets have a closer look at each approach:

In the context of a user of the root organization

The main organization is always the „admin“ organization. If a user of this organization calls the WebApi, the tenant id will be extracted from the token.
The relevant section of a token could look like this:

„aud“: „0f5ff0f2-5a7f-430e-8ea1-6e133055990e“,
„iss“: „https://login.microsoftonline.com/2ef09b02-2e8f-4181-8bb2-78cfc813799a/v2.0″,

The issuer, contains the tenant id (bold) of the organization of the calling user. This id is called the „ValidatedTenantIdentifier“, because the token will be validated before the tenant id gets extrated.
After the tenantId has been extracted, the configuration for the tenant will be loaded. This configuration will have the „IsMainTenant“ flag activated.
Therefore the current user will be treated as an „admin user“. This user can attach an HTTP header to each request that contains the tenantId of the organization for who he wants to fetch data.
This identifier is called the „RequestedTenantIdentifier“.
This identifier could be any of the tenantIds of the AllTenants configuration file.

The logic is also represented in the source code:

            var validatedTenantIdentifier = _tenantResolutionStrategy.GetValidatedTenantIdentifier();
            var result = await _tenantStore.GetTenantByIdentifierAsync(validatedTenantIdentifier);
            if(result.IsMainTenant)
            {
                var requestedTenantIdentifier = _tenantResolutionStrategy.GetRequestedTenantIdentifier();
                if(requestedTenantIdentifier != null)
                {
                    result = await _tenantStore.GetTenantByIdentifierAsync(requestedTenantIdentifier);
                }                
            }
            var environment = _tenantResolutionStrategy.GetRequestedEnvironment();
            result.TargetEnvironment = result.Environments.FirstOrDefault(env => env.Id == environment);
            return result;

Important to understand: The roles that are applied to a user in the main organization will be valid for all client tenants.

If there is no http header that contains any tenant to request, the resolved tenant will fall back to the original ValidatedTenantIdentifier.

Why should the main organization not use the application for its own employees?

It would be possible for the main organization to use the app itself. In this case the users would make normal use of the app, but could also request all the data of all client organizations. In most scenarios this is not wanted. Also all the applied roles would be granted for the organization itself and equally to all other organizations. This is probably not wanted most of the time, but would work.

In the context of a user of one of the client organizations

Users of the client organization are not allowed to attach any HTTP header with a tenantId. They will just call the api with their own identity via Authorization code flow with PKCE.
The ValidatedTenantIdentifier will always be the tenant they are accessing.
The only thing they can do is changing the environment. This also works with a Http header. You can see how the environment gets determined in an excert of the above code snippet:

            var environment = _tenantResolutionStrategy.GetRequestedEnvironment();
            result.TargetEnvironment = result.Environments.FirstOrDefault(env => env.Id == environment);

In the context of an app of the main organization

This is the highest possible way to access the API. In this case the app can also determine the tenant and environment to access via Http headers.
Usually this will only happen for apps that are part of the current federation. This app only access will include all roles by default.

In the context of an app in one of the client organizations

Its possible for client applications to allow an app-only access for only their own tenant. For this the client application needs to create an app registration for itself that will then add at least one permission of the enterprise application of the main tenant.
Now an app only access via client secret or certificates is possible. This app only access will include all roles by default.