Designing multi-tenant applications can be tricky. The previous sentence may have been an understatement.
The ability to quickly spin up a new instance of your application is a powerful business case, but getting there involves serious engineering. Partitioning user data (and making sure it stays partitioned) is critical. A common use case involves treating a subdomain of your application URL as a tenant identifier (the acme
in acme.yourapplication.com
). Incoming requests are then examined and connected to the correct data source based on the subdomain.
Designing for multi-tenancy involves modeling your data stores from the start with multiple tenants in mind. Data security should also be a major consideration. Anyone else have a headache yet?
Have no fear! Alongside support for token-based authentication, the latest release of our .NET SDK also includes full support for the Organization resource, which can make developing multi-tenant applications a lot simpler.
Build Multi-Tenant .NET Applications with Organizations
The high-level overview of using Stormpath to build multi-tenant applications is covered in the Multi-Tenancy with Stormpath guide.
The Organization resource represents a tenant of your application, and is a top-level container for Stormpath Account Stores (Directories and Groups). In addition to logically organizing (and separating) Account Stores, it includes some special sauce for working with multi-tenant applications. The nameKey
property must be unique across all of your Organizations, and must follow DNS hostname rules. This constraint specifically designed to support the “subdomain as a tenant identifier” use case.
The .NET SDK makes it easy to:
Create and Manage Organizations
Creating an Organization is a snap:
1 2 3 4 5 6 |
var rebels = client.Instantiate<IOrganization>() .SetName("Rebel Alliance") .SetNameKey("rebels") // must be unique and follow DNS hostname rules .SetDescription("The Rebellion Against the Empire"); // optional await client.CreateOrganizationAsync(rebels); |
Updating an existing Organization is also straightforward:
1 2 3 4 5 6 7 8 9 |
// organizationHref is the Stormpath href of the Organization var rebelsOrg = await client.GetOrganizationAsync(organizationHref); // Disable the Organization. Login attempts will be rejected. rebelsOrg.SetStatus(OrganizationStatus.Disabled); // Save changes await rebelsOrg.SaveAsync(); |
Add Directories and Groups to Organizations
Organizations represent top-level containers for Directories and Groups. Adding Account Stores to an Organization can be accomplished in a few ways:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Add an existing IDirectory or IGroup instance: await rebels.AddAccountStoreAsync(myGroupOrDirectory); // Or, add by name: await rebels.AddAccountStoreAsync("My Group Or Directory Name"); // Or, add by href: await rebels.AddAccountStoreAsync(hrefOfGroupOrDirectory); // Or, perform a lookup query that matches one Directory or Group: await rebels.AddAccountStoreAsync<IDirectory>( dirs => dirs.Where(d => d.Name.StartsWith("My Dir"))); |
Just like Applications, an Organization can have a default Account Store and a default Group Store, which are the default locations for newly-created Accounts and Groups. If the default store is null
, new Accounts or Groups cannot be created in the Organization. These properties can be updated with the appropriate methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Get the default Account Store for an Organization var defaultAccountStore = await rebels.GetDefaultAccountStoreAsync(); // defaultAccountStore is an IDirectory or IGroup, or null // Set the default Account Store await rebels.SetDefaultAccountStoreAsync(myGroupOrDirectory); // Get the default Group Store for an Organization var defaultGroupStore = await rebels.GetDefaultGroupStoreAsync(); // defaultGroupStore is an IDirectory, or null // Set the default Group Store await rebels.SetDefaultGroupStoreAsync(myDirectory); |
Assigning an Organization to an Application
The Organization itself is an Account Store, so it can be assigned to an Application:
1 2 |
await myApp.AddAccountStoreAsync(rebels); |
Adding an Organization to an Application will enable login for all of the Accounts contained in Directories and Groups assigned to the Organization, as well as any that are added in the future.
Authenticate Using Organizations
Stormpath handles login attempts using a specific login flow that iterates through all the Account Stores associated with an Application. In multi-tenant applications, you frequently need to restrict login to a specific tenant. This is possible by specifying an Organization (or Organization nameKey
) during login:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Using an IOrganization instance var loginResult1 = await myApp.AuthenticateAccountAsync(request => { request.SetUsernameOrEmail("lskywalker"); request.SetPassword("whataPieceofjunk$1138"); request.SetAccountStore(rebels); // restrict search to Rebel Alliance tenant only }); // Using an Organization href string var loginResult2 = await myApp.AuthenticateAccountAsync(request => { request.SetUsernameOrEmail("lskywalker"); request.SetPassword("whataPieceofjunk$1138"); request.SetAccountStore(rebels.Href); }); // Using an Organization nameKey // (This is handy if you're using a subdomain identifier // that you can quickly pull out of the request URL!) var loginResult3 = await myApp.AuthenticateAccountAsync(request => { request.SetUsernameOrEmail("lskywalker"); request.SetPassword("whataPieceofjunk$1138"); request.SetAccountStore("rebels"); // Tenant with nameKey "rebels" must exist }); |
That’s all there is to it! Using Organizations and the features available in the Stormpath .NET SDK can make the development of multi-tenant .NET web applications faster and easier.
Further Reading
For more reading, check out:
IOrganization
interface