-
Notifications
You must be signed in to change notification settings - Fork 162
Sign up for a new tenant, with versioning
Since version 3.3.0 of the AuthP library contains a service that allows an external user to sign up for a new tenant - this is known as "Sign up for a new tenant, with versioning" (shortened to "Sign up a new tenant" in this document). This makes it easy for anyone sign up to your application - this type of approach is called self-service provisioning. The service also contains an optional feature called versioning where you can provide tenants with different features - this a bit like GitHub Plans. This allows an new user to:
- Create a new tenant for their company / organisation
- If your application uses versioning, the the new user can choose for the version they suite them
- The new user is registered to your application and is linked to new tenant
- And optionally the new user can designated as an tenant admin, which allows that user to manage the users within their tenant
The service supports all types of tenants (single-level, hierarchical, hybrid or sharding-only modes) and via the add New User adapter it can work with different ASP.NET Core authentication handlers.
AuthP version 6.2.0 improves the "Sign up a new tenant" feature, mainly around better error handing. See the UpdateToVersion620 file for more information. This document is also updated to version 6.2.0.
NOTE: For someone who new to this feature I recommend this article which shows why and how the "Sign up a new tenant" can be very useful.
The "sign up / versioning" feature uses two services in the AuthPermissions.SupportCode namespace.
- The
ISignInAndCreateTenant
\SignInAndCreateTenant
, which implements the “sign up / versioning” service. - The
IAddNewUserManager
service which theSignInAndCreateTenant
relies on for adding a new user - see Add New User adapter for more on this service. - And if you Sharding turned on, then you need to register your implementation of the
ISignUpGetShardingEntry
Version 6.2.0 interface (Before Version 6.2.0 seeIGetDatabaseForNewTenant
). NOTE: This section shows examples of this extra service.
Any code in the AuthPermissions.SupportCode project has to be manually, and the code shown below is taken from Example 3 during the registering of the service to use in your ASP.NET Core application.
//Add the SupportCode services
services.AddTransient<IAddNewUserManager, IndividualUserAddUserManager<IdentityUser>>();
services.AddTransient<ISignInAndCreateTenant, SignInAndCreateTenant>();
//If Sharding is turned on then include the following registration
services.AddTransient<ISignUpGetShardingEntry, YourVersionOfThisService>(); //Version 6.2.0
Note that the IAddNewUserManager
service uses an implementation called IndividualUserAddUserManager<IdentityUser>
which works with applications using the individual user accounts authenticate provider. Version 3.3.0 only contains two implementations of the IAddNewUserManager
interface. They are:
-
IndividualUserAddUserManager<TIdentity>
which works with the individual user accounts authenticate provider. -
AzureAdUserManager
which works with Azure AD authenticate provider (NOTE: won't work with Azure AD B2C with social logins).
More implementations may be added, or you can build your own by implementing the IAddNewUserManager
interface.
If you want to use the versioning feature you need to create a MultiTenantVersionData
class containing the different features for each version. There are three parts to the versioning:
-
TenantRolesForEachVersion
that defines the Tenant Roles which will add extra features to the different versions. -
TenantAdminRoles
which defines the Roles that the new user should have - this allows you to decide if the users gets admin Roles. -
HasOwnDbForEachVersion
is required if Sharding is turned on. It allows you to define whether the tenant will have its own database (i.e. Sharding) or are in a database with other tenants.
Each of these properties are a Dictionary
, where the string Key holds the name of the version. The code below comes from Example 7
public static readonly MultiTenantVersionData TenantSetupData = new()
{
TenantRolesForEachVersion = new Dictionary<string, List<string>>()
{
{ "Free", null },
{ "Pro", new List<string> { "Tenant Admin" } },
{ "Enterprise", new List<string> { "Tenant Admin", "Enterprise" } },
},
TenantAdminRoles = new Dictionary<string, List<string>>()
{
{ "Free", new List<string> { "Invoice Reader", "Invoice Creator" } },
{ "Pro", new List<string> { "Invoice Reader", "Invoice Creator", "Tenant Admin" } },
{ "Enterprise", new List<string> { "Invoice Reader", "Invoice Creator", "Tenant Admin" } }
},
HasOwnDbForEachVersion = new Dictionary<string, bool?>()
{
{ "Free", false }, //false: Tenant data goes in a database containing other tenants
{ "Pro", true }, //true: Tenant data has its own database
{ "Enterprise", true } //true: Tenant data has its own database
}
};
You need an action / page which can be accessed by a user who isn't logged in to sign up for a new tenant - see Example 3 with the "Sign up now!" link on the Home navbar. The data your will need are:
- The email of the new user (optionally their UserName too)
- The name they want for the tenant
- Depending on which authentication provider you selected (see Add New User adapter) you might need the password the new user wants to use.
The SignInAndCreateTenant
service has two methods, one for no versioning and one that uses versioning. The two subsections describe how to use each approach.
In this case you use the SignUpNewTenantAsync(AddNewUserDto newUser, AddNewTenantDto tenantData)
method which takes in:
-
newUser
which contains the new user's information, e.g. email, username, password, plus the Roles that the new user should have. -
tenantData
which should contain theTenantName
for the new tenant. If Sharding is turned on you must also provide set theHasOwnDb
property to true or false and theRegion
property can be used if you have geographically spread out database servers and is used byISignUpGetShardingEntry
service to pick the correct database server location.
In this case you use the SignUpNewTenantWithVersionAsync(AddNewUserDto newUser, AddNewTenantDto tenantData, MultiTenantVersionData versionData)
method which takes in:
-
newUser
which contains the new user's information, e.g. email, username, password, plus the Roles that the new user should have. -
tenantData
which should contain theTenantName
for the new tenant and theVersion
property should contain a valid version name. If Sharding is turned on and your database servers are geographically spread out, then you can set theRegion
property which will be used byISignUpGetShardingEntry
service to pick the correct database server location. -
versionData
which defines the configuration of the tenant (seeExample3CreateTenantVersions
example shown earlier in this document).
Once the new tenant is created the new user is registered as a valid user for the new tenant. Then the new user is logged in.
NOTE: If the registering of the new user fails the new tenant is deleted so that the user can try again and the tenant name is still available to them.
This service has one method called FindBestDatabaseInfoNameAsync
which is used in the Sign up for a new tenant, with versioning feature when the multi-tenant application is using sharding and its job is to return the name of the DatabaseInformation
which defines the database the tenant should created in. This is a complex calculation because there many options:
- Does the tenant need a dedicated database, or does it go into a database that has multiple tenants in it? (see this diagram)
- Are you using multiple database servers spread of geographically? In this case you want to pick the nearest server / database.
There is no one solution to this as it depends on your application, so the AuthP library defines the ISignUpGetShardingEntry
interface and you need to implement and then manually register your service, e.g.
//manually add services from the AuthPermissions.SupportCode project
builder.Services.AddTransient<ISignUpGetShardingEntry, YourGetDatabaseCode>();
As explained, there isn't a standard solution so you need to implement your own, but help you there is some demo code with examples of what you might do.
The DemoGetDatabaseForNewTenant
class provides demo code supports the hybrid approach (e.g. both shared and shard-only tenants) but doesn't have multiple database servers spread of geographically. Its goals are:
- Pack all the shared tenants into a database, but not more than 50 tenants.
- Shard-only tenants take any empty database.
- If no databases are available it returns an error (you could create a new database and also update the sharding entries, but that depends on your production system)
NOTE: This demo isn't used in any of the Examples
If your multi-tenant app uses sharding, then you need to provide a service that follows the ISignUpGetShardingEntry
interface and register the service (see Setup the “sign up / versioning” service above). These will finding or create a sharding entry to define what database the new tenant's data.
There are two demo versions in the code before release of version 6.2.0 and they have been updated to the new version. See the updated demos below:
- For hybrid sharding see the
DemoGetDatabaseForNewTenant
for the changes. - For sharding-only see the
DemoShardOnlyGetDatabaseForNewTenant
for the changes. This is used in Example7 applciation in the AuthP repo.
The 6.2.0 version of the "Sign up a new tenant" service is designed so that an error won't stop the new user to trying again. For instance it uses a temporary name for the tenant until all the stages have successfully completed. This means the new user should be able retry if they get something wrong, like the password being too short, then they their tenant is still available.
However, if an fatal error occurs, i.e. an Exception in the code, then its likely that the new user can't try again. In this case the user is given a message something like this
$"Failed to create a new tenant due to an internal error. Contact the support team and provide the string '{createTimestamp}' to help them fix your problem."
The createTimestamp
string is formed by DateTime.UtcNow.ToString("yyyyMMddHHmmss-fff");
(example "20240111091446-697") and you can use createTimestamp to find what went wrong and what the new user was trying to do:
- The logger of the Exception has a message attached that says "Critical error in SignOn. The timestamp of this Exception is {createTimestamp}." so that you can find it easily.
- If you are sharding, then the sharding name will contain the timestamp, e.g. "SignOn-20240111091446-697"
- If it gets to the creating the tenant, but not finished, then the name of the tenant will be
$"TempSignIn-{_createTimestamp}"
.
If the tenant has be created it has all the Roles etc. so you have most of the data to fix the user's sign in - all you need to is change the name of the tenant and its good to go. But if it was bad you might want to delete the tenant and the Sharding Entry if you are using sharding.
- Intro to multi-tenants (ASP.NET video)
- Articles in date order:
- 0. Improved Roles/Permissions
- 1. Setting up the database
- 2. Admin: adding users and tenants
- 3. Versioning your app
- 4. Hierarchical multi-tenant
- 5. Advanced technique with claims
- 6. Sharding multi-tenant setup
- 7. Three ways to add new users
- 8. The design of the sharding data
- 9. Down for maintenance article
- 10: Three ways to refresh claims
- 11. Features of Multilingual service
- 12. Custom databases - Part1
- Videos (old)
- Authentication explained
- Permissions explained
- Roles explained
- AuthUser explained
- Multi tenant explained
- Sharding explained
- How AuthP handles sharding
- How AuthP handles errors
- Languages & cultures explained
- JWT Token refresh explained
- Setup Permissions
- Setup Authentication
- Startup code
- Setup the custom database feature
- JWT Token configuration
- Multi tenant configuration
- Using Permissions
- Using JWT Tokens
- Creating a multi-tenant app
- Supporting multiple languages
- Unit Test your AuthP app