Paid Feature
This is a paid feature. Email us to get a license key to start a SuperTokens subscription.
Example 2: Tenants use their sub domain to login
In this UX flow, all tenants login using their assigned sub domain (customer1.example.com
, customer2.example.com
and so on). The login method that's shown on the login page on each sub domain depends on that tenant's tenantId
configuration.
important
Throughout this page, we will assume that the tenant ID for a tenant is equal to their sub domain - so if the sub domain assigned to a tenant is customer1.example.com
, then their tenantId is customer1
.
#
Step 1: Creating a new tenantWhenever you want to onboard a new customer, you should create and configure a tenantId for them in the SuperTokens core.
websiteDomain
#
Step 2: Change CORS setting and #
CORS setup- In order for the browser to be able to make requests to the backend, the CORS setting on the backend needs to reflect the right set of allowed origins. For example, if you have
customer1.example.com
on the frontend, then the CORS setting on the backend should allowcustomer1.example.com
as an allowed origin. You can specifically whitelist the set of frontend sub domains on the backend, or you can use a regex like*.example.com
.
websiteDomain
setup#
- On the frontend, set the
websiteDomain
towindow.location.origin
- On the backend, you should set the
websiteDomain
to be your main domain (example.com
if your sub domains aresub.example.com
), and then you want to override thesendEmail
functions to change the domain of the link dynamically based on the tenant ID supplied to thesendEmail
function. See the Email Delivery section in our docs for how to override thesendEmail
function.
#
Step 3: Load login methods dynamically on the frontend based on the tenantIdModify the SuperTokens.init
to do the following:
- Set the
usesDynamicLoginMethods
to true. This will tell our frontend SDK that the login page is based on the tenantId and to fetch the tenant config from the backend before showing any login UI. - Initialize the
Multitenancy
recipe and providegetTenantId
config function.
- ReactJS
- Angular
- Vue
Important
supertokens-auth-react
SDK and will inject the React components to show the UI. Therefore, the code snippet below refers to the supertokens-auth-react
SDK.import React from 'react';
import SuperTokens from "supertokens-auth-react";
import ThirdPartyPasswordless from "supertokens-auth-react/recipe/thirdpartypasswordless";
import Session from "supertokens-auth-react/recipe/session";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
usesDynamicLoginMethods: true,
recipeList: [
ThirdPartyPasswordless.init({
contactMethod: "EMAIL",
}),
Session.init(),
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: async () => {
// We treat the sub domain as the tenant ID
return window.location.host.split('.')[0]
}
}
}
},
})
]
});
Important
supertokens-auth-react
SDK and will inject the React components to show the UI. Therefore, the code snippet below refers to the supertokens-auth-react
SDK.import React from 'react';
import SuperTokens from "supertokens-auth-react";
import ThirdPartyPasswordless from "supertokens-auth-react/recipe/thirdpartypasswordless";
import Session from "supertokens-auth-react/recipe/session";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
usesDynamicLoginMethods: true,
recipeList: [
ThirdPartyPasswordless.init({
contactMethod: "EMAIL",
}),
Session.init(),
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: async () => {
// We treat the sub domain as the tenant ID
return window.location.host.split('.')[0]
}
}
}
},
})
]
});
import React from 'react';
import SuperTokens from "supertokens-auth-react";
import ThirdPartyPasswordless from "supertokens-auth-react/recipe/thirdpartypasswordless";
import Session from "supertokens-auth-react/recipe/session";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
usesDynamicLoginMethods: true,
recipeList: [
ThirdPartyPasswordless.init({
contactMethod: "EMAIL",
}),
Session.init(),
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: async () => {
// We treat the sub domain as the tenant ID
return window.location.host.split('.')[0]
}
}
}
},
})
]
});
#
Step 4: Tell SuperTokens about tenant's sub domainsWe want to restrict users to only be able to access their (sub)domains. SuperTokens makes it easy for you to do this. We start by telling SuperTokens which domain each tenantId has access to:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node";
import Multitenancy from "supertokens-node/recipe/multitenancy"
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
Multitenancy.init({
getAllowedDomainsForTenantId: async (tenantId, userContext) => {
// query your db to get the allowed domain for the input tenantId
// or you can make the tenantId equal to the sub domain itself
return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"]
}
}),
// other recipes...
]
})
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
multitenancy.Init(&multitenancymodels.TypeInput{
GetAllowedDomainsForTenantId: func(tenantId string, userContext supertokens.UserContext}) ([]string, error) {
// query your db to get the allowed domain for the input tenantId
// or you can make the tenantId equal to the sub domain itself
return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil
},
}),
},
})
}
TODO
The config above will tell SuperTokens to add the list of domains returned by you into the user's session claims once they login. This claim can then be read on the frontend and backend to restrict user's access to the right domain(s).
#
Step 5: Sharing sessions across sub domainsYou may want to allow the user's session to be shearable across sub domains. This would lead to a better UX in which even if they visit the main domain (logged in via a.example.com
, and visit example.com
), the frontend app there can detect if the user has a session or not.
This can be achieved by setting the sessionTokenFrontendDomain
value in the Session recipe.
If the sub domain and the main website domain have different backends (on different sub domains), then you can also enable sharing of sessions across API domains.
note
This is not a security issue because we will anyway be restricting access to users based on their domain allow list as shown below.
#
Step 6: Limiting the user's access to their sub domain.We will be using session claim validators on the frontend to restrict sub domain access. Before proceeding, make sure that you have defined the GetAllowedDomainsForTenantId
function mentioned above. This will add the list of allowed domains into the user's access token payload.
On the frontend, we want to check if the tenant has access to the current sub domain. If not, we want to redirect them to the right sub domain. This can be done by using the hasAccessToCurrentDomain
session validator from the multi tenancy recipe:
- ReactJS
- Angular
- Vue
import Session from "supertokens-web-js/recipe/session";
import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy";
async function shouldLoadRoute(): Promise<boolean> {
if (await Session.doesSessionExist()) {
let validationErrors = await Session.validateClaims({
overrideGlobalClaimValidators: (globalValidators) =>
[...globalValidators,
AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
]
});
if (validationErrors.length === 0) {
// user is an admin
return true;
}
for (const err of validationErrors) {
if (err.validatorId === AllowedDomainsClaim.id) {
// multi tenancy domain check claim check failed
} else {
// some other claim check failed (from the global validators list)
}
}
}
return false
}
- We call the
validateClaims
function with theAllowedDomainsClaim
validator which makes sure that the user is on the right sub domain. - The
globalValidators
represents other validators that apply to all calls to thevalidateClaims
function. This may include a validator that enforces that the user's email is verified (if enabled by you).
import React from "react";
import { SessionAuth, useSessionContext } from 'supertokens-auth-react/recipe/session';
import { AllowedDomainsClaim } from 'supertokens-auth-react/recipe/multitenancy';
const ProtectedRoute = (props: React.PropsWithChildren<any>) => {
return (
<SessionAuth
overrideGlobalClaimValidators={(globalValidators) =>
[...globalValidators,
AllowedDomainsClaim.validators.hasAccessToCurrentDomain()
]
}
>
<InvalidClaimHandler>
{props.children}
</InvalidClaimHandler>
</SessionAuth>
);
}
function InvalidClaimHandler(props: React.PropsWithChildren<any>) {
let sessionContext = useSessionContext();
if (sessionContext.loading) {
return null;
}
if (sessionContext.invalidClaims.some(i => i.validatorId === AllowedDomainsClaim.id)) {
return <div>You cannot access this page.</div>
}
return <div>{props.children}</div>;
}
Above we are creating a generic component called ProtectedRoute
which enforces that its child components can only be rendered if the user is on the right sub domain based on the Multi tenancy claim.
import Session from "supertokens-web-js/recipe/session";
import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy";
async function shouldLoadRoute(): Promise<boolean> {
if (await Session.doesSessionExist()) {
let validationErrors = await Session.validateClaims({
overrideGlobalClaimValidators: (globalValidators) =>
[...globalValidators,
AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
]
});
if (validationErrors.length === 0) {
// user is an admin
return true;
}
for (const err of validationErrors) {
if (err.validatorId === AllowedDomainsClaim.id) {
// multi tenancy domain check claim check failed
} else {
// some other claim check failed (from the global validators list)
}
}
}
return false
}
- We call the
validateClaims
function with theAllowedDomainsClaim
validator which makes sure that the user is on the right sub domain. - The
globalValidators
represents other validators that apply to all calls to thevalidateClaims
function. This may include a validator that enforces that the user's email is verified (if enabled by you).