Multi-tenancy

This guide will help you set up multi-tenancy in FusionAuth. Tenants allow for logical separation of users, applications and other objects in FusionAuth.

Why Use Multi-Tenancy

There are many reasons why you might be interested in a multi-tenant FusionAuth instance:

  • Provide logical separation for configuration. With only one FusionAuth instance to manage, users, applications, API keys, and other configurable objects are kept logically distinct.
  • To support building an application which you’ll offer as a service, often called SaaS (Software as a Service). Each customer can be modeled as a tenant in FusionAuth.
  • Managing different deployment environments. For instance, on one FusionAuth instance, you could run integration, user acceptance and developer environments. It’s not recommended to run production in the same instance as other environments, however.
  • To easily manage multiple developer environments. Having each developer use a tenant on a dedicated development server allows developers free reign to add, remove and modify configuration, while keeping them relatively isolated from each other.
  • Per-client custom tenant level settings are needed. Examples of such settings include password rules, the issuer of JWTs, or webhook transaction levels.

FusionAuth’s performant, lightweight multi-tenancy can help with any of these scenarios. If you want to learn more about FusionAuth tenants, there’s an entire Tenant core concepts section.

Levels of Isolation

There are two different kinds of isolation possible for tenants in FusionAuth: physical and logical.

With physical isolation, you run a separate web server, database and other components (such as FusionAuth). You run the same codebase (or perhaps multiple versions). The codebase doesn’t have to support multiple tenants.

The strengths of physical separation include:

  • Each tenant can live in different geographies, which may be required for compliance or data sovereignty rules.
  • Downtime for one tenant doesn’t affect others.
  • There is no possibility of intermixing user data.
  • Clients can live on different versions of your software and upgrades may be managed by them.

However, there are some downsides:

  • Deployment becomes more difficult, as you need to ship artifacts to N different servers.
  • It is more expensive because servers scale linearly with tenants.
  • Rollup reporting or any other cross-tenant operation becomes a series of network calls.

The other option, far more common, is logical isolation. With this architecture, you have the same web server, sending traffic to different tenants based on hostname or some other attribute, with the same database. You typically have a tenant table and a tenant_id foreign key in almost every other table. Your codebase knows about these data attributes and is built with multi-tenancy in mind.

The logical approach has the following benefits:

  • Operations are much simpler; there is one application for you to manage, even though it looks to users like many different applications.
  • Typically it will be less expensive. Even if you need a bigger server to handle multiple tenants, costs won’t scale linearly with the number of tenants.
  • Cross tenant operations such as reporting are a database query rather than network requests.

There are issues with this approach, though:

  • It is more difficult to have tenants on different versions. You can do this with feature flags or other custom coding; check out how Stripe versions their APIs for an example.
  • One tenant’s traffic can affect other tenants; this is also known as the noisy neighbor problem.
  • Hosting data in different geographies becomes complex, if not impossible.

This guide covers logical isolation of tenants. If you need physical isolation with FusionAuth, run different instances. You can also read this article for more information.

The PiedPiper Video Chat Application

To make things a bit more concrete, let’s use an example. Suppose you wanted to build a Pied Piper Video Chat SaaS application(or PPVC for short).

This will be similar to Slack in terms of how users sign up and how hostnames are used to identify different tenants, but will have a superior video experience, with patented middle out compression. This application will support multiple entirely distinct groups of users. Each user who signs up to create a tenant picks their own hostname and other configuration.

This sets up an application that can be logged into by other users. Just as you can sign up for foo.slack.com and share it with your organization, PPVC will be built to let someone sign up for foo.piedpipervideochat.com and share the awesome video chat with their colleagues.

The creator of a PPVC tenant will be able to pick a hostname and a display background color. If someone from Hooli joins, they could set up hooli.piedpipervideochat.com, for example. As mentioned above, Slack uses hostnames to differentiate tenants, and PPVC will as well. If you are familiar with Slack, you may have noticed that the URL to access Slack is your-company.slack.com. This pattern allows Slack to route your request based upon the hostname portion of the URL, your-company. It also provides each customer with a unique URL which makes life simple for everyone.

The word application is unfortunately overloaded when discussing multi-tenancy. There are actually multiple things which could all be called an “application”.

  • The entire PPVC application, which is the combination of all the functionality built to support the chat application. This will be referred to below as the PPVC application.
  • The codebase and datastore implementing features, written in some programming language. This will be called the “web application” or the “web application codebase”.
  • The applications where users will sign up for the PPVC and also the place where their users will go to access the chat functionality. These all have separate users and functionality. These could live in the same codebase or in separate code bases. These will be referred to as “control plane” or “data plane” applications, unapologetically borrowing from network routing.
  • The FusionAuth application configuration, stored in FusionAuth and managed by the FusionAuth UI or API. This includes OAuth, registration and other configuration. These will be called “FusionAuth application configuration” or the “FusionAuth application object”.

Common Components of a Multi-Tenant Setup

While FusionAuth multi-tenancy can help with many different scenarios, as mentioned above, this rest of this guide will focus on building out the Pied Piper Video Chat SaaS application.

A control plane application manages user and tenant creation. You can integrate billing and general account management in the control plane application. This is the Slack application where you sign up for Slack itself.

Users will video chat through one of the data plane applications. This is analogous to foo.slack.com.

Here are common components for a multi-tenant SaaS application:

  • The control plane application, where users will create and manage tenants.
  • The data plane application, where users will login and video chat.
  • FusionAuth tenant and application configurations, which control how users can login and register.
  • A component to determine the tenant from the hostname.
  • Code to set up the tenant.
  • The tenant object, which contains tenant specific data.

Let’s look at each of these.

The Control Plane Application

When you have a multi tenant SaaS program, you need a way to manage tenants. For our example, PPVC must have a web or mobile application where users can sign up for Pied Piper Video Chat for their company. The users may be able to do other tasks, like set up the hostname, customize the look and feel, and set up billing. If you are building the PPVC application, you must charge that per user per month fee, after all, otherwise you won’t be able to afford a car with doors that open by going up.

But the control plane application doesn’t enable video chat between users. Instead, the control plane application is basically a tenant management portal.

For PPVC, this app will likely deliver functionality over the web. However, the actual implementation details don’t matter much for the purposes of illustrating multi-tenancy concepts.

The Data Plane Applications

Data plane applications offer functionality to PPVC’s users’ users. Hooli employees will log into hooli.piedpipervideochat.com to chat amongst themselves, and Raviga employees will do the same at raviga.piedpipervideochat.com. Within each data plane application, users can have roles, language preferences, and other user profile information.

Tenant management is not part of the data plane applications. There also may be little or no overlap between the users of a data plane application and the control plane application.

For PPVC, the data plane application will also likely deliver functionality over the web using HTML, but it could be an API used by a mobile application if that worked better for the business. The actual implementation details don’t really matter for the purposes of this guide.

FusionAuth Tenants and Applications

For each of the control and data plane applications built, corresponding FusionAuth configuration objects need to be created. The control plane application will have a corresponding FusionAuth tenant configuration where the users of the control plane application will be stored. It will also have a FusionAuth application configuration which contains application specific settings, such as whether a user can self register. Each data plane application will also have both a FusionAuth tenant configuration and a FusionAuth application configuration.

For the PPVC control plane web application, the FusionAuth objects can be created manually. Each time a user signs up and creates a new PPVC application (remember, like foo.slack.com is created in Slack), a new data plane FusionAuth tenant configuration will also be created.

If an employee at Raviga signs up for PPVC, a new FusionAuth tenant object will be created for Raviga, and within that tenant a new FusionAuth application configuration object. This Raviga FusionAuth tenant object is where all users of the Raviga PPVC application would be stored.

The mapping between the data plane application and the corresponding FusionAuth tenant and application objects must be stored somewhere. In this example, each data plane application has a hostname and background display color, as well as FusionAuth configuration information. This additional metadata should be stored in the application database. This metadata will be discussed more deeply in The Tenant Object section.

Data Plane Application Tenant Determination

The data plane application needs to differentiate between different tenants. If the codebase is hosted at piedpipervideochat.com, there are a few ways to send incoming requests to the appropriate tenant.

  • The hostname: hooli.piedpipervideochat.com can point to the Hooli data plane application. As mentioned above, this is similar to how Slack operates.
  • User self-identification: A user provides an identifier that determines the tenant, in a separate field. This is how AWS operates.
  • User choice: A user logs in and is presented with a list of data plane applications to which they can login. In this case, present this option outside of the data plane applications, perhaps in the control plane application.
  • User or request attributes: If there is an attribute of the client or incoming request indicating the correct tenant, that can be used to route the user. For instance, a system may be able to read network information to determine the appropriate data plane application.

Here’s an example of the hostname approach:

The hostname approach used by Slack.

Here’s an example of the user self-identification approach:

The user self-identification approach used by AWS.

A distinct hostname is the easiest way to differentiate tenants. It is memorable to the user, works well with internet standards such as cookies, and scales well. That’s the approach this guide will take.

Tenant Setup

When a user signs up in the control plane application, they pick a hostname. Other configuration will happen behind the scenes. Some examples include:

  • You need to set up the hostname. When someone signs up with the hooli hostname, you want hooli.piedpipervideochat.com to point to your web application where the code is running. This could be handled with DNS wildcarding or by updating DNS records automatically.
  • The FusionAuth tenant configuration and a FusionAuth application configuration must be created and configured. These objects allow the new data plane application to log users in and perform other login related tasks.
  • Creating any FusionAuth roles that might be applicable, such as an admin role.
  • Customizing the theme of the FusionAuth hosted login pages with custom colors and logos.
  • Creating an initial user in the data plane application, optionally.
  • Any needed metadata of the data plane application that the end user cannot modify.

Since the data plane application is not usable for the new tenant without these configuration settings, this configuration should take place at the moment of signup. There may be other optional configuration that can be performed asynchronously.

The Tenant Object

Within your control plane application’s database, store metadata about each tenant in a table. This metadata:

  • Allows your users to customize their data plane application without your intervention.
  • Configures the data plane application to deliver proper functionality. The hooli tenant might be on a premium plan and be allowed ten chat rooms, whereas the raviga tenant might be on a basic plan and have a lower limit.
  • Can be used for reporting as your business grows.

Some attributes you might need in this object when implementing multi-tenancy with FusionAuth:

  • The owning user: which user created this tenant. Depending on your business model, there could be a one to one or one to many mapping between users and tenants in your control plane application datastore.
  • Hostname: a hostname which users of a given organization will access to get to their data plane application. Something like hooli.piedpipervideochat.com or hooli if you will always append a domain name.
  • FusionAuth tenant Id: the tenant created in FusionAuth during setup.
  • FusionAuth Client Id and Client Secret: this allows you to build links for users to log into the data plane application and access other OAuth functionality.
  • Customization attributes: such as a logo or background color for the hosted login pages.
  • Internal attributes: about the tenant such as the plan level.
  • A tenant scoped FusionAuth API key and API key id: When the control plane application needs to modify FusionAuth configuration, it can use this API key. This API key could also be displayed to the administrators of the data plane application. Doing so will allow them to automate interactions with their users in FusionAuth. They could, for example, write a script to pull a list of their users.

Registration and Login Flows

Let’s get more concrete and continue to discuss the PPVC app. It is typical to allow users to self register for the control plane application and automatically set up a corresponding tenant. In this case, the below diagram outlines the registration flow:

BrowserPied Piper VideoChatFusionAuthRegistersSubmit tenant creation formCreates tenant objectCreates needed configurationCreate optional configurationTenant configuration detailsBrowserPied Piper VideoChatFusionAuth

Registration process for the control plane application.

After a user has registered in the control plane application, they can log in and view details of their tenant:

The control plane application interface.

Here’s the login flow for a data plane application. Such a login would happen after a user had registered in the control plane application and a tenant had been created and configured. Apart from the lookup of the tenant OAuth configuration by hostname (the Client Id and Client Secret), this is a typical Authorization Code grant.

BrowserPied Piper VideoChatFusionAuthRequest raviga.ppvc.comLooks up tenant fromhostnameSends user to tenant specific loginurlAuthenticatesSends the authorization codeAuthorization code to the redirectURLPresents code for tokenSends tokenDisplays chat pageBegins chattingBrowserPied Piper VideoChatFusionAuth

Login process for the data plane application.

The users of each data plane application can register or log in and see the chat window (or at least a note about the chat functionality that will later be built out).

The data plane application interface.

Each of the data plane applications have a corresponding FusionAuth tenant configuration. Different users can have the same email address, as you can see in the below screenshot, where the jian@fusionauth.io user exists in the default tenant and the ppvctest2 tenant:

FusionAuth user dashboard.

Setting Up the Multi-Tenant System

Let’s cover the steps to build the PPVC using an external identity provider like FusionAuth. FusionAuth will be used as the user database for the control plane application as well as all related data plane applications. Here are the tasks to set up an application skeleton, configure FusionAuth and integrate them both.

  • Configure a control plane application object in FusionAuth and set up your control plane application up to use that FusionAuth configuration for user sign up and login.
  • Create a tenant table in your database, an object in your web application codebase, and CRUD methods to manage it.
  • A way to create, modify, and delete FusionAuth configuration for each data plane application.
  • Building the URLs to allow user login, registration and logout requests to be routed to the correct FusionAuth tenant.
  • The callback handling for the Authorization Code grant within your web application codebase.

Let’s look at each of these from the perspective of PPVC. If you’d prefer, you can download, review and run a fully functional multi-tenant chat application, actual video chat functionality not included.

Initial FusionAuth Configuration

The control plane application needs a corresponding FusionAuth application configuration, as mentioned above. Create this in its own FusionAuth tenant to increase isolation. Perform the following configuration on the FusionAuth application object:

  • An authorized redirect URL.
  • Enable the Authorization Code grant.
  • Enable self service registration, if desired.
  • Specific roles, if desired.

Here’s what the OAuth tab will look like:

Setting up the control plane application in FusionAuth.

Below, basic self service registration is enabled. Doing this will allow a user to sign up for the control plane application themselves. If you do not enable this, create the users in some other fashion using the User API.

Allow registration on the control plane application in FusionAuth.

Then, you need to configure your control plane application to use FusionAuth for auth. This is framework dependent, but typically involves an OAuth library.

You also need to create a Key Manager API key. Navigate to Settings -> API Keys and create a global API key. Make sure you enable Key manager as this will be used to mint tenant scoped API keys.

Creating a key manager API key.

Finally, create a separate tenant to serve as a blueprint tenant. This tenant will provide default settings for all data plane FusionAuth tenants. Configure the email server, password complexity and other settings as you see fit. Of course, you may customize these settings further for each tenant, but having a blueprint tenant to copy makes the initial data plane tenant setup easier.

Creating a blueprint tenant.

Instead of doing all of these manually, you may also download a kickstart file, customize it, and run it using Kickstart.

Defining The Tenant Object

The tenant object exists in your control plane and data plane applications. As outlined in The Tenant Object , this entity stores all tenant related information, whether FusionAuth related or application specific. Here’s a sample tenant table definition:

Tenant object creation DDL

 CREATE TABLE tenant
             (
                          id                    INT NOT NULL auto_increment,
                          user_id               INT NOT NULL,
                          hostname              VARCHAR(255) collate utf8mb4_unicode_ci NOT NULL,
                          background_color_code VARCHAR(6) COLLATE utf8mb4_unicode_ci NOT NULL,
                          fusion_auth_tenant_id VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
                          api_key               VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
                          api_key_id            VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
                          client_id             VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
                          client_secret         VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL,
                          PRIMARY KEY (id),
                          UNIQUE KEY uniq_4e59c462a76ed395 (user_id),
                          UNIQUE KEY uniq_6459cd6da86ed443 (hostname),
                          CONSTRAINT fk_4e59c462a76ed395 FOREIGN KEY (user_id) REFERENCES user (id)
             )
             engine=innodb DEFAULT charset=utf8mb4 COLLATE=utf8mb4_unicode_ci 

This implementation maps each tenant to a single user, since user_id is a foreign key into the local user table. As mentioned above, you could choose to allow users to create multiple tenants as well.

You’ll need to create a user interface to manage this data.

You could use Advanced Registration Forms, a premium feature, to capture tenant setup information, such as hostname, on user registration. Then you could use a webhook to create the FusionAuth configuration and the tenant object in your web application’s database. This is an alternative implementation.

The hostname is an important field which will repeatedly be used in the PPVC application. The hostname is how the code differentiates between different data plane applications/tenants. hostname is forced to be unique. You may either let the user pick the hostname (such as hooli) or assign it (tenant123). In either case, the hostname will typically be prepended to the domain name (in this case, piedpipervideochat.com) to create an address that end users of PPVC can visit to chat their hearts out.

You could of course also let the user choose an entirely different host and domain name (chat.hooli.com). That is a great premium feature, but requires more setup, so this guide will leave that as an exercise for the reader.

The FusionAuth specific fields in the above table are:

  • fusion_auth_tenant_id: the UUID representing the tenant.
  • api_key: a tenant locked API key.
  • api_key_id: the Id of the tenant locked API key. This allows you to manage the API key.
  • client_id: the Client Id of the data plane FusionAuth application configuration.
  • client_secret: the corresponding Client Secret.

Let’s discuss how to obtain the FusionAuth configuration values and update the tenant data in your web application codebase next.

Updating the Tenant Object With FusionAuth Configuration

To keep FusionAuth and your application in sync, you need to be able to create and delete required FusionAuth configuration for the data plane applications as they are created and removed.

You can use any of the client libraries to create FusionAuth configuration. This document happens to use PHP, but feel free to use whatever language makes the most sense in the context of your development.

You can manipulate FusionAuth with client libraries by creating JSON objects as documented by the APIs and then calling the corresponding operation.

How you manage FusionAuth configuration depends on your framework and requirements. Options for calling the FusionAuth APIs to manage the new FusionAuth tenant configuration include:

  • In the same controller where you create the row in the tenant table, call the APIs and wait for them to return.
  • Use a queue. Put a message on the queue when a new tenant row is added, then have a listener retrieve the message and create the configuration.
  • Use a framework specific method to call the APIs before the tenant is saved or deleted.

For the PPVC application, the last method is used. Before the tenant information is saved to the database, the FusionAuth APIs are called and FusionAuth configuration is done. However, what makes sense depends on how many new tenants you expect to be created and how much configuration you need to do. If, for example, large numbers of PPVC users were being imported for each data plane application creation, asynchronous execution would work better.

The minimal amount of FusionAuth configuration required:

  • A FusionAuth tenant object based on the blueprint tenant set up initially.
  • A FusionAuth tenant locked API key.
  • A FusionAuth application object.

After these are created, the generated configuration values are stored in the data plane application’s tenant object for future reference. Let’s take a deeper look at each of the required FusionAuth settings.

Creating the FusionAuth Tenant

The easiest way to do this is to retrieve the blueprint tenant, extract the default configuration and then create the new tenant. Here’s example PHP code:

$fusionauthBase = 'http://login.piedpipervideochat.com/'; // or pull from config
$client = new FusionAuthClient($fusionauthKeyManagerKey, $fusionauthBase);

$result = $client->retrieveTenant($this->blueprintTenantId);
if (!$result->wasSuccessful()) {
  $this->logger->error('An error occurred!');
  $this->logger->error(var_export($result,TRUE));
  throw new FusionAuthException("Can't save: ".var_export($result,TRUE));
}

$blueprint_tenant = $result->successResponse;

// pick off what we know we want to minimize forward compatibility issues.

$tenant_object = array();
$tenant_object["name"] = $tenant->getHostname();
$tenant_object["themeId"] = $blueprint_tenant->tenant->themeId;
$tenant_object["issuer"] = 'https://'.$tenant->getHostname().".ppvc.com";

$tenant_email_configuration = $this->convertObjectToArray($blueprint_tenant->tenant->emailConfiguration);
$tenant_object["emailConfiguration"] = $tenant_email_configuration;

$tenant_jwt_configuration = $this->convertObjectToArray($blueprint_tenant->tenant->jwtConfiguration);
$tenant_object["jwtConfiguration"] = $tenant_jwt_configuration;

$tenant_externalId_configuration = $this->convertObjectToArray($blueprint_tenant->tenant->externalIdentifierConfiguration);
$tenant_object["externalIdentifierConfiguration"] = $tenant_externalId_configuration;

$tenant_request = array();
$tenant_request["tenant"] = $tenant_object;

$result = $client->createTenant('', $tenant_request);
if (!$result->wasSuccessful()) {
  $this->logger->error('An error occurred!');
  $this->logger->error(var_export($result,TRUE));
  throw new FusionAuthException("Can't save: ".var_export($result,TRUE));
} 

$new_tenant = $result->successResponse;
return $new_tenant->tenant->id;

In this code, you can see that the following configuration is extracted from the blueprint tenant:

  • themeId
  • emailConfiguration
  • jwtConfiguration
  • externalIdentifierConfiguration

There are only a few different values between the new data plane tenant and the blueprint tenant:

  • The name of the tenant, which is set to the hostname.
  • The issuer, which is set to the expected host.

An alternative implementation would be to copy the blueprint tenant and then patch the differences.

Either way, you could tweak any other FusionAuth tenant settings as needed. For example, if your tenants had different password complexity rules, setting them when the FusionAuth tenant object is created would ensure they were consistently applied.

Don’t forget to store the FusionAuth tenant Id in your database in the tenant table.

Next, let’s create an API key.

The Tenant Locked API Key

You should set up a tenant locked API key for two reasons:

  • You can use the new key for further configuration of this tenant. This follows the principle of least privilege and ensures you won’t accidentally affect any other tenants.
  • You can display it to your clients. They can then write scripts against FusionAuth for their own purposes, such as managing users. This is a low-cost value add that will increase the stickiness of the PPVC application.

Here’s code to create the tenant scoped API key:

$apikey_object = array();
$apikey_object["metaData"]["attributes"]["description"] = "API key for ".$hostname;
$apikey_object["tenantId"] = $fusionauth_tenant_id;

$apikey_request = array();
$apikey_request["apiKey"] = $apikey_object;

$result = $client->createAPIKey('', $apikey_request);
if (!$result->wasSuccessful()) {
  $this->logger->error('An error occurred!');
  $this->logger->error(var_export($result,TRUE));
  throw new FusionAuthException("Can't save: ".var_export($result,TRUE));
}

$apikey = $result->successResponse;

return [$apikey->apiKey->id, $apikey->apiKey->key];

Store this key in your web application’s database in the tenant table. Both the key and the key Id must be stored. The key can be used to make other API calls. The API key Id can be used to modify the key later.

The next action after creating the tenant scoped API key is create a new FusionAuth client that uses this less powerful key:

$fusionauthTenantLockedApiKey = '...'; // returned from the previous code block
$fusionauthBase = 'http://login.piedpipervideochat.com/'; // or pull from config
$client = null;
$client = new FusionAuthClient($fusionauthTenantLockedApiKey, $fusionauthBase);

Then, create the FusionAuth application configuration.

Creating a FusionAuth Application

At this point, you are creating the FusionAuth application configuration. A FusionAuth application configuration is required for anything a user can log in to. Users like Patrice or Jason Winter might be logging into the Hooli instance of the PPVC application. The hostname (hooli) is required for proper FusionAuth application object configuration, as there are a number of URLs which must be configured.

$saasRootDomain = '.piedpipervideochat.com'; // or pull from config
$ppvc_app_base = "https://".$hostname.$saasRootDomain; 

$application_object = array();
$application_object["name"] = "Default application for ".$hostname;

$application_oauthconfiguration = array();
$application_oauthconfiguration["authorizedRedirectURLs"] = [$ppvc_app_base."/login/callback"];
$application_oauthconfiguration["enabledGrants"] = ["authorization_code"];
$application_oauthconfiguration["logoutURL"] = $ppvc_app_base;
$application_object["oauthConfiguration"] = $application_oauthconfiguration;

$application_registrationconfiguration = array();
$application_registrationconfiguration["enabled"] = true;
$application_object["registrationConfiguration"] = $application_registrationconfiguration;

$application_request = array();
$application_request["application"] = $application_object;

$result = $client->createApplication('', $application_request);
if (!$result->wasSuccessful()) {
  $this->logger->error('An error occurred!');
  $this->logger->error(var_export($result,TRUE));
  throw new FusionAuthException("Can't save: ".var_export($result,TRUE));
}

$application = $result->successResponse;

return [$application->application->id, $application->application->oauthConfiguration->clientSecret];

There is a base URL constructed, something like https://hooli.piedpipervideochat.com. This is the base for any URLs or other data plane application specific configuration.

Users will be logging into the corresponding data plane application using the OAuth Authorization Code grant, so the oauthConfiguration field needs to be set up. In that section, the code:

  • Enables the authorization code grant by configuring the enabledGrants field.
  • Sets the value of logoutURL. This is where FusionAuth will send the user after they have logged out.
  • Adds needed values to authorizedRedirectURLs. This callback code will be discussed later.

Once this data plane application is live, the Hooli users need to be created. You have some options:

  • Allow users to sign in with a social provider.
  • Create users via the FusionAuth API and an automated process.
  • Allow users to sign in with an enterprise SSO provider such as an OAuth or SAML compatible identity provider.
  • Let users sign up for an account.

In this code, basic self service registration is enabled:

{/* ... */}
$application_registrationconfiguration = array();
$application_registrationconfiguration["enabled"] = true;
$application_object["registrationConfiguration"] = $application_registrationconfiguration;
{/* ... */}

If needed, any of the above options could be configured. For instance, to allow users to sign in with Google, you would create an identity provider and assign the identity provider to the FusionAuth application configuration for a new data plane application during the Initial FusionAuth Configuration step.

After the FusionAuth configuration is set up, store the FusionAuth application’s Id and Client Secret. These values will be needed later.

Other FusionAuth Configuration

The above sections outlined all required configuration to allow a user to sign up or log in to one of the PPVC data plane applications. However, there might be other FusionAuth configuration you’ll want to set up. You might want to:

  • Add a user or set of users.
  • Create FusionAuth registrations for them, so they’ll be able to log in.
  • Set up roles.
  • Set up groups.
  • Assign users to groups and roles.
  • Create FusionAuth API keys with limited permissions, such as a read-only user query key.

Now that FusionAuth tenant objects and application objects are automatically created every time a user registers in the control plane application, the next step is to look at routing user requests to the correct data plane application/tenant.

Routing Requests to the Correct Tenant

As mentioned in Data Plane Application Tenant Determination , you must map requests to a specific data plane application. Typically the hostname is used to determine the tenant: hooli.piedpipervideochat.com points to the Hooli data plane application.

Configure DNS and Your Web Server to Handle Wildcard Domains

You need to configure your DNS and web application server to send all requests ending in .piedpipervideochat.com to your running web application codebase. How to do so varies depending on your DNS provider and web application server.

Consult your DNS provider documentation on how to point a wildcard DNS entry to a given host. If you are developing locally, you can add multiple hostnames to your /etc/hosts file.

127.0.0.1       localhost app.piedpipervideochat.com raviga.piedpipervideochat.com hooli.piedpipervideochat.com

Consult your web server documentation to determine how to accept multiple hostname requests. Below is an example Apache configuration file to send traffic for multiple piedpipervideochat.com addresses to a proxied local web application running on port 8000.

<VirtualHost *:443>
  ServerName app.piedpipervideochat.com
  ServerAlias *.piedpipervideochat.com

  ProxyPreserveHost on
  ProxyPass / http://localhost:8000/ retry=1
  ProxyPassReverse / http://localhost:8000/ retry=1
</VirtualHost>

Make sure you are passing the Host header through to the web application code, since that is what the web application will use to look up the tenant object.

Look Up the OAuth Configuration Based On the Hostname

Once the request is received by your web application, look up the Client Id and Secret in the tenant table in your database based on the incoming request’s hostname.

When a request comes in to https://hooli.piedpipervideochat.com/login, map from the hostname (hooli) to the appropriate Client Id. Here is some sample code:

$client_id = ''; 
$client_secret = '';
    
if ($this->isControlPlaneHost($host)) { 
  $client_id = $this->controlPlaneClientId;
  $client_secret = $this->controlPlaneClientSecret;
} else { 
  $hostname = $this->hostname($host); // converts from hooli.piedpipervideochat.com to hooli
  $repository = $this->entityManager->getRepository(Tenant::class);
  $tenant = $repository->findOneBy(array('hostname'=>$hostname));
  if ($tenant) { 
    $client_id = $tenant->getApplicationId();
    $client_secret = $tenant->getClientSecret();
  } else { 
    // throw an error, this is not a valid hostname. Doing so will allow an attacker to enumerate your supported hostnames, however.
  }
} 
return [$client_id, $client_secret];

The code checks to see if the host matches the control plane application. Remember, in the PPVC multi-tenant application, the web application code responds to all requests for any users, both those logging into the control plane application to create video chat tenants and the users of the video chat application logging into a data plane application.

The OAuth configuration for the control plane application is static and was created in the Initial FusionAuth Configuration section. In contrast, for the data plane applications, the configuration is dynamic, is stored in the database, and was created in Creating a FusionAuth Application . The tenant object is retrieved by the hostname and the Client Id and Client Secret are returned.

Each FusionAuth application object lives in one and only one FusionAuth tenant object, so providing the Client Id, which identifies the FusionAuth application configuration, ensures FusionAuth determines the correct FusionAuth tenant object. The Client Id is used to create the login, logout and registration links displayed in the PPVC application. These links can be used in navigation or any other location where the user might want to log in, register, or log out. Let’s look at building those links next.

The sample PPVC application leverages an open source library for managing and creating OAuth URLs. This library handles setting the state parameter and other niceties. If your web application framework or language has such a library, using it is highly recommended.

The simplified login link generation, without using a library, uses FusionAuth’s well documented OAuth endpoints as well as the retrieved client_id:

$redirectURI = '...'; // discussed below.
$fusionauthBase = 'http://login.piedpipervideochat.com/'; // or pull from config
return $fusionauthBase . '/oauth2/authorize?client_id='.$client_id.'&redirect_uri='.$redirectURI.'&response_type=code'

The registration URL is the same format, but uses the path /oauth2/register instead of /oauth2/authorize.

$redirectURI = '...'; // discussed below.
$fusionauthBase = 'http://login.piedpipervideochat.com/'; // or pull from config
return $fusionauthBase . '/oauth2/register?client_id='.$client_id.'&redirect_uri='.$redirectURI.'&response_type=code'

Next up is logging out. In this case, you need to log the user out of FusionAuth and your control plane or data plane application. Because the logoutURL setting was configured in the FusionAuth application configuration, when the logout URL is visited, FusionAuth will first log the user out of FusionAuth and then redirect the user to that location. The code at that location should ensure it logs the user out of the control or data plane application (killing the session or otherwise doing so).

$fusionauthBase = 'http://login.piedpipervideochat.com/'; // or pull from config
$fusionauthBase.'/oauth2/logout?client_id='.$clientId;

This logout URL works with browsers. If you are using APIs for a mobile application, revoke the refresh token instead of using the /oauth2/logout endpoint.

Handling OAuth Callbacks

The final auth specific functionality for supporting multiple tenants is handling the OAuth callback. You may recall that in the Creating a FusionAuth Application section, you configured a FusionAuth application object with a given authorizedRedirectURLs value. It was something like: https://hooli.piedpipervideochat.com/login/callback. And in the most recent section, you saw code that looked like $redirectURI = '...';. These are the same values.

You must create a part of your web application codebase to respond to OAuth redirect requests. The code must do the following:

  • Determine the tenant from the hostname to retrieve the correct Client Id and Client Secret.
  • Exchange the authorization code for a token.
  • Log the user into your web application.

Let’s look at each of these steps.

Determine the Tenant’s OAuth Configuration

Mapping from the hostname like hooli to a certain Client Id and Secret should sound familiar. It is exactly the same logic as what was done in the Look Up the OAuth Configuration Based On the Hostname section.

Exchange the Authorization Code

To perform this exchange, it’s best to use an OAuth library for your language or framework. You may also use the FusionAuth client library:

$fusionauthBase = 'http://login.pipedpipervideochat.com/'; // or pull from config
$noApiKeyNeeded = '';
$client = new FusionAuthClient($noApiKeyNeeded, $fusionauthBase);

$result = $client->exchangeOAuthCodeForAccessToken($code, $client_id, $client_secret, $redirect_uri)
if (!$result->wasSuccessful()) {
  $this->logger->error('An error occurred!');
  $this->logger->error(var_export($result,TRUE));
  throw new FusionAuthException("Can't save: ".var_export($result,TRUE));
}

$oauthResult = $result->successResponse;

$token = $oauthResult->access_token;

Logging the User In

At this point you have an access token and a user who has successfully authenticated. You can examine the token to determine if the user is authorized for the FusionAuth application object. You can also examine the roles assigned to this user and expose or prohibit functionality based on them.

At this point, your next step depends on the framework or programming language you are using. Create a login session in your web application. This is specific to your implementation, so is left as an exercise for the reader.

You may also create or update a local user record in your database. If there are other parts of the web application functionality which will be associated with a user, this is a common task, and many frameworks expect a local user record.

After you have set up a session in your web application, redirect to the chat page for the data plane application. The control plane application should send the user to a profile page. Distinguish between these by checking the hostname.

API Calls

If you are not using a tenant scoped API key, provide a tenant Id when you call the FusionAuth API. There are cases where the FusionAuth tenant object can be inferred from another identifier, such as when you provide an FusionAuth application Id. But there are other times where it cannot, such as when you are creating a user. It’s better to be consistent and present the FusionAuth tenant Id every time.

Throughout the API documentation, you’ll see sections referencing X-FusionAuth-TenantId which will specify when the header should be used.

Additional Considerations

After you’ve set up multiple tenants, there’s still more work to do. After all, you need to build features, such as a real video chat application.

But you have a solid foundation. Your users will be able to create their own data plane applications. And your user’s users will be able to log in using FusionAuth.

There are a few other specific things worth considering, however.

Control Plane Codebase Vs Data Plane Codebase

The control plane and data plane applications are conceptually different, but do not need to be physically distinct. You could, in other words, implement them within the same web application, as this guide did.

Doing so makes sense initially. There will be database overlap between them and it may be easier to operate a single web application than to create two distinct web or mobile applications. Both the control plane and data plane applications will be using tenant data. The control plane application will write the data and the data plane application will read it. You’ll need to either share a database or have some other way of syncing up that data if you create separate web applications for the control and data plane applications.

If you use the same web application for both the control and data planes, limit access to functionality based on whether the host indicates the control plane or a data plane application is executing.

As you grow, you may want to split these apart. They will probably have different SLAs, and they’ll have different features and release cycles. For an initial implementation, however, it will be simpler to build them as a single deployment artifact.

Framework Specific User Objects

As mentioned above, oftentimes a web framework will require a local user record. This will be used for permissions checks within the framework or for relations with other objects.

They may even require a unique email address. However, FusionAuth tenants are a separate userspace. In other words, one can sign up for hooli.piedpipervideochat.com and raviga.piedpipervideochat.com with the same richard@fusionauth.io email address.

To handle this discrepancy, you can:

  • Use a compound value of email address and tenant Id for the email address.
  • Prepend the FusionAuth user id to the email address. This is guaranteed to be unique, since it is a UUID.
  • Pick any other unique string.

In all of these cases, you are searching FusionAuth if you need the actual user’s email address.

In addition, add a field in the local user object referencing the FusionAuth user Id. For FusionAuth, you should also add a data.applicationUserId field, to store the identifier for the local user object. This will let you map back and forth between the local user representation and the one stored in FusionAuth, as needed.

User Hostname Choice

If you allow users to pick their hostnames, ensure that you have a process for handling collisions.

You also should have a manual process for dealing with squatting. For example, someone may sign up with the hostname piedpiper, but you probably don’t want piedpiper.piedpipervideochat.com to be in the hands of anyone except the company who created PPVC.

Multi-Tenant Concepts

There are five general categories of modifiable objects in FusionAuth:

  • FusionAuth Tenant scoped objects
  • FusionAuth Tenant attached objects
  • FusionAuth Application scoped objects
  • FusionAuth Application attached objects
  • Global objects

A “scoped” object is contained within the enclosing object. If the latter is deleted, the former will be as well. For instance, both users and groups are scoped to a tenant, and when the enclosing tenant is deleted, the users and groups for that tenant are too. Scoped objects cannot be shared. If the PPVC application has groups for different user privileges, each tenant needs to have its own. For example, an “Admin Group” or “Moderator Group” would have to be created in each tenant.

An “attached” object, on the other hand, is linked to a different object, but if the latter is deleted, the former still exists. For instance, a signing key, created by using Key Master, can be associated with a tenant. When that tenant is deleted, the association is removed, but the signing key remains.

Attached objects can be shared between peer objects. For example, PPVC can use the same signing key for the hooli.piedpipervideochat.com and raviga.piedpipervideochat.com tenants.

Here are examples of configuration in each of these categories.

Tenant Scoped

Here is a partial list of FusionAuth tenant scoped objects:

  • Users
  • Groups
  • API Keys (optionally)
  • Applications
  • Entities

Tenant Attached

Here is a partial list of FusionAuth tenant attached objects:

  • Email templates (some of them)
  • Forms (some of them)
  • Themes
  • Connectors
  • Consents
  • User actions
  • Webhooks

Application Scoped

Here is a partial list of FusionAuth application scoped objects:

  • Registrations
  • Roles

Application Attached

Here is a partial list of FusionAuth application attached objects:

  • Identity providers
  • Email templates (some of them)
  • Forms (some of them)
  • Lambdas

Global Objects

There are a few objects which are globally available. APIs to manage these objects are usually available under the system path in the API namespace.

  • API Keys (optionally)
  • Login reports
  • Logs
  • CORS settings

If you need separate global configuration, run multiple FusionAuth instances. If you need to sync configuration between them, script your changes using the API, terraform provider, or client libraries.

Example Applications

You can download, review and run a fully functional multi-tenant application modeled on the PPVC application discussed in this guide. It is written in PHP and the Symfony framework. Actual video chat functionality is not included.

If you want a simpler multi-tenant application, here’s one written in node.

Limits

The main limits on tenants in FusionAuth are resources of the underlying server. The tenants are lightweight, but do take some resources. There are FusionAuth instances supporting thousands of FusionAuth tenants. At this scale the FusionAuth administrative user interface runs into some issues, so use one of the client libraries to manage the FusionAuth tenant and application configuration. There’s a GitHub issue to resolve these limits in the administrative user interface.

Another current limitation of FusionAuth with respect to tenants is the ability to limit users who log into the administrative user interface to a single tenant. This is not currently supported. There are tracking issues for this functionality. Both GitHub issue #91 and GitHub issue #1524 offer different ways to add support for this feature.

Login records, audit logs and other log files are not separated by tenant. You may be able to query based on FusionAuth tenant or application Ids, but if you need true isolation of log files, you should run separate instances. There’s a GitHub issue to address this.

CORS settings are not configurable by tenant, nor are other settings configured by navigating to Settings -> System.

FusionAuth application objects and other FusionAuth tenant scoped entities cannot be shared across tenants. If you need to keep such entities in sync, you can write scripts to sync up the configuration of multiple FusionAuth applications across tenants. For users, you may be able to have users federate to an external identity provider, and enable that provider for FusionAuth applications across tenants. This will allow your external identity provider to be the system of record for your users.

The default FusionAuth tenant, in which the FusionAuth administrative user interface application resides, cannot be removed.