👋 Hi, and welcome to Part 2 in my two-part article dealing with the core topic of Multi-Tenancy in a B2B SaaS application 😁 In Part 1, I dealt with multi-tenant architecture and the benefits of integrating with the Auth0 platform to help architect a B2B SaaS solution, and now in Part 2, I'm going to take a look at how the various aspects discussed can be implemented in practice. If you've not already done so, then I would highly recommend reading the first part of this article before continuing 😎
Multi-Tenant Registration
Registration - or, more specifically, subscriber registration - is typically the catalyst for the provisioning of a tenant in a B2B SaaS application. I talked about subscribers (and also vendors) in Part 1, and the subscriber registration model being used is largely down to the type of solution being built. For example, you may decide to employ something like a free-tier subscription to start with - similar to what Auth0 does and what I’ve decided to offer in TheatricalPA (the B2B SaaS application that I'm building and introduced in Part 1) - or you may choose to only ever register “paying” subscribers. Whichever route you take, you’ll find plenty of resources available on the web that describe the merits of each.
In TheatricalPA - my B2B SaaS Application for Theatre Production Management - registration can take one of two forms: individual subscriber registration - a la B2C style - and registration as a subscriber on behalf of a Production Company, as in the typical B2B model. In this article, I'm going to be concentrating on the B2B registration aspects and will leave the B2C aspects of registration for another time.
The image above shows the dialogue accessed via the "Register Production Company" option in TheatricalPA and is where details about the registering subscriber can be entered. I’ve built TheatricalPA to leverage Invitation workflow within the Auth0 Organizations feature to provide a fluid user experience that will also mitigate against common attack scenarios. Let's see the code that gets executed as part of the registration process:
//
try {
$API = new company\API();
// 1: Create Organization
$response = $API->auth0()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html
management()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html
organizations()->create(
$domain,
$title);
$decoded = HttpResponse::decodeContent($response);
// Get the returned Organization ID
if (isset( $decoded['id'] )) {
$organization = $decoded['id'];
// 2: New to avoid cache inconsistency
$options = new \WP_Auth0_Options;
// 3: Default to 'Username-Password-Authentication' Connection for the created Organization
$response = $API->auth0()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html
management()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html
organizations()->addEnabledConnection(
$organization,
$connectionId = "con_omxh4cmdaRu3tQFk",
$body = [
'assign_membership_on_login' => false
]);
$user = wp_get_current_user();
// 4: Send an invite to the Administrator
$response = $API->auth0()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html
management()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html
organizations()->createInvitation(
$organization,
$options->get('client_id'), [
'name' => ($user->ID) ?
$user->display_name ?? $user->user_email :
"TheatricalPA"
], [
'email' => $email
], $body = [
'roles' => [
Role::$roles['administrator']['identifier']
]
]);
} else {
$error->add(
$decoded['statusCode'] ?? 0,
$decoded['message'] ?? '');
}
} catch( Auth0Exception $ex ) {
$error->add( $ex->getCode(), $ex->getMessage() );
} catch( \Exception $ex ) {
$error->add( $ex->getCode(), $ex->getMessage() );
} catch( \Throwable $th ) {
$error->add( $th->getCode(), $th->getMessage() );
} finally {
}
return( $error );
As I'm using WordPress Multisite as the technology stack framework/scaffolding the language I'm coding in is PHP. As a consequence, I'm using the Auth0 SDK for PHP, which you can find more about here, and I'm also leveraging the Auth0 Plugin for WordPress, which I'll talk more about later.
TheatricalPA also exposes an API, so I've created a bespoke API
class which: (a) wraps a number of the initialization operations associated with the Auth0 PHP SDK, (b) provides a subclassable context for declaring the numerous API functions/routes within the application, and (c) enables me to easily patch the various WordPress REST API extensions I create (like the company\API()
in the code above) to consume an OAuth 2.0 Access Token as well as utilize the standard authenticated WordPress session context.
As with most things "WordPress", TheatricalPA implements a Plugin, and code is typically executed as part of a WordPress Filter or a WordPress Action; all of which shall remain topics for another time 😎 In this article, I want to focus on the following things (where the notes below correspond to the numbered comments above):
- Here, an attempt is made to create a new Organization in Auth0. From an IAM perspective, this will be used to represent the application tenant that will be associated with the registering subscriber.
- If successful, an attempt is then made to get configuration information from the Auth0 WordPress Plugin (which will be used subsequently). At this point, we'll be executing in the context of the WordPress Primary Site, which is essentially the core part of my B2B SaaS application that deals with functionality outside of a specific subscription context.
- Enable the default Auth0 UserID & Password Connection on the newly created Organization, providing UserID/Password Signup and Login as standard.
- Send the Subscriber Administrator an Invitation
Multi-Tenant Invite Workflow
As previously discussed, in TheatricalPA, I use the invitation workflow provided by the Auth0 Organizations feature out of the box. An invitation doesn’t stay around forever in Auth0, so if there’s no response within a given period, then the invite will simply time out - which is a great deterrent against fake registrations! I actually use invitations at multiple points within TheatricalPA, and I'm going to be discussing more on invitation workflow in a separate blog post at some point in the future.
Multi-Tenant Subscriber Invitation
I'm using this capability in Auth0 to invite what is essentially the Subscriber Admin into TheatricalPA; Subscriber Administration is the topic of another blog post in this series, so stay tuned for more to come on that! 😎 To do this, I'm using the Auth0 Management API (via the Auth0 SDK for PHP). The Auth0 Management API is rate-limited, so as an application developer, I can leverage this to help protect my own infrastructure. For example, if someone with malicious intent is trying to perform large-scale fake registrations, then the number and frequency of these will be limited automatically by Auth0, and Auth0 can also provide me with data which could be used to potentially identify such attacks, too. Of course, I can’t rely on Auth0 API rate limiting for everything, and in other areas of the application, my own throttling will need to be implemented.
Building TheatricalPA to leverage invitation workflow as part of subscriber registration provides a fluid user experience that also mitigates against common attack scenarios. Additionally, everything in this workflow from an IAM UX perspective leverages Auth0 Universal Login - the de facto login/signup experience provided by Auth0 out-of-the-box and the interface we typically recommend using as a best practice with any integration. Universal Login integrates seamlessly with the Organizations feature, and it integrates seamlessly with other features in Auth0, too - like Auth0 Actions, for example.
Subscriber Authentication
As part of subscriber Authetication initiated by the invitation workflow, a couple of Auth0 Actions are imlemented to help provide TheatricalPA with the information it needs. The first Action (below) in deployed as part of the post-login
trigger and adds additional organizational attribute information to the Auth0 generated ID Token
exports.onExecutePostLogin = async (event, api) => {
var DEBUG = event.secrets['DEBUG'] ? console.log : function () {};
var LOG_TAG = '[ADD_ORGANIZATION_ATTRIBUTES]:';
// State Machine
DEBUG(LOG_TAG, "context =", event.client.client_id);
switch(event.client.client_id) {
// Applicable Clients?
case 'aJh1tCn1UtO8i5ZgIRf2aqicoSMCmVtO': // TheatricalPA Administration
case 'Nbe3RSU6myLRycWpPMF8RamQBKRaGtUi': // TheatricalPA Console
case 'u4X3xedMuasp1j8kXBkD0i1kOQjagll8': { // TheatricalPA
LOG_TAG += `[${event.client.client_id}]:[${event.connection.name}|${event.connection.strategy}]:`;
// Add Organization name to ID Token?
DEBUG(LOG_TAG, "Evaluating for Organization");
if (event.organization?.id) {
api.idToken.setCustomClaim('organization',event.organization?.name);
DEBUG(LOG_TAG, "Org name set: ", event.organization?.name);
}
} break;
default: {
LOG_TAG += `[${event.client.client_id}]:`;
DEBUG(LOG_TAG, 'Skipping');
} break;
}
};
Implementation uses logic to determine the conditions under which it will execute, and also demonstrates the use of conditional debugging using the Auth0 Real-time Webtask Logs extension. For more information about the event
object in an post-login
triggered action see the Auth0 documentation here.
The second Action in also deployed as part of the post-login
trigger, uses conditional logic mechanisms similar to the first, and adds information to the ID Token concerning the users' assigned role(s). Out-of-box Role Based Access Control (RBAC) functionality provided in Auth0 can be utilized as part of Auth0 Organization Membership - as described here - however, if you need to employ something more flexible than RBAC then check out the following article for a discussion concerning other mechanisms that can be leveraged:
exports.onExecutePostLogin = async (event, api) => {
var DEBUG = event.secrets['DEBUG'] ? console.log : function () {};
var LOG_TAG = '[ADD_ACCOUNT_ROLES]:';
// State Machine
DEBUG(LOG_TAG, "context =", event.client.client_id);
switch(event.client.client_id) {
// Applicable Clients?
case 'aJh1tCn1UtO8i5ZgIRf2aqicoSMCmVtO': // TheatricalPA Administration
case 'Nbe3RSU6myLRycWpPMF8RamQBKRaGtUi': // TheatricalPA Console
case 'u4X3xedMuasp1j8kXBkD0i1kOQjagll8': { // TheatricalPA
LOG_TAG += `[${event.client.client_id}]:[${event.connection.name}|${event.connection.strategy}]:`;
// Add Roles to tokens?
DEBUG(LOG_TAG, "Evaluating for Roles");
if (event.authorization) {
DEBUG(LOG_TAG, "Roles = ", event.authorization.roles);
api.idToken.setCustomClaim(`roles`, event.authorization.roles);
api.accessToken.setCustomClaim(`roles`, event.authorization.roles);
}
} break;
default: {
LOG_TAG += `[${event.client.client_id}]:`;
DEBUG(LOG_TAG, 'Skipping');
} break;
}
};
Multi-Tenant Creation
Creating the B2B SaaS Application tenant is an exercise that's largely down to the technology stack you employ and the hosting you use. In TheatricalPA, I'm leveraging WordPress Multisite hosted on AWS via the templates provided by Bitnami (see here for more details). WordPress Multisite provides a large portion of tenant provisioning out of the box - leastways when it comes to provisioning for Data, Compute, and Brand isolation at any rate - and the PHP code fragment below shows how I currently leverage wp_insert_site
function provided as part of the WordPress API.
try {
$API = new company\API();
/*
*/
$response = $API->auth0()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html
management()->
// https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html
organizations()->get($id = $userinfo->org_id);
$organization = HttpResponse::decodeContent($response);
$company = new Type::$library['Company']($organization);
/* Just-In-Time create the site. TODO: To make sure it's valid to
do this, we should check the user's role in their user information,
and we should also ensure the user has been through MFA (i.e. check
the 'amr' value; also in the user information).
*/
$site = wp_insert_site([
"user_id" => $user_id,
"domain" => $domain . '/',
"title" => $organization['display_name'],
"options" => [
// Stash the associated Company
"theatricalpa_company" => $company,
// Force to https
'siteurl' => untrailingslashit( 'https://' . $domain . '/' ),
'home' => untrailingslashit( 'https://' . $domain . '/' )
]
]);
if ( is_wp_error($site) ) {
// Redirect to Whoops!
} else {
// Redirect to the newly created site
wp_redirect('https://' . $domain);
exit;
}
} catch( \Throwable $th ) {
// Redirect to Whoops!
}
There are a few things to note here:
The above code is being triggered via a hook registered to the
auth0_user_login
WordPress action that's defined as part of the Auth0 Plugin for WordPress. As described in the commented section of the code above, this means that from a TheatricalPA perspective, the subscription tenant is created "just in time" and only once Auth0 has validated the user. This is a great way to mitigate excessive resource usage in a B2B SaaS application due to fake registrations!I'm not showing any provisioning of resources outside of a WordPress context. For example, as discussed previously in the Sub-domain Organization section above, a subdomain would likely need to be provisioned and/or additional capacity may need to be allocated to satisfy the likes of Vertical, Horizontal and/or Database scaling (either manually or via some automatic mechanism). The specifics of this will largely be down to your application needs.
The time it takes to complete the tenant creation process may require employing an asynchronous mode of operation - i.e. where a subscriber admin is prevented from fully accessing the new application tenant until after all provisioning is complete. In this situation, Auth0 Passwordless Authentication workflows - such as Passwordless with Magic Link, say - could also be employed to notify the (subscriber) admin as to when they can fully start to use their subscription.
What's Next?
In this article, I've walked through the details of Multi-Tenancy in a B2B SaaS Application and also provided some insight together with code examples for how I'm integrating with Auth0 to help build multi-tenancy in my application, TheatricalPA. I'll be writing more about the other topics I've mentioned in this article in the near future, so be sure to watch out for those. In the meantime, feel free to comment below and tell us what you think - we always love to hear feedback, positive or otherwise, as it helps us to improve our content! Thank you. Aside from that, here are some additional resources you might like to follow up on that can help you on your journey. Have fun, and I'll catch up with you next time! 🤗