Implement common domain login
Authenticate users across different tenants through a common domain.
Overview
This guide shows you how to authenticate users through the same page, https://example.com/auth, and then redirect them to their subdomain after sign-in.
The login page adjusts the authentication method based on the tenant’s configuration.
You can determine the tenant in several ways.
A common approach is to ask the user for their organization name and use it as the tenantId configured in SuperTokens.
Before you start
The tutorial assumes that you already have a working application integrated with SuperTokens. If you have not, please check the Quickstart Guide.
You also need to create the tenants that your application requires. View the previous tutorial for more information on how to do this.
Steps
1. Ask for the tenant ID on the login page
If you have followed the pre-built UI setup, when you visit the login screen, you see the login screen immediately. The flow needs to change to first ask the user to enter their tenant ID and then display the login UI based on the tenant ID.
To do that, first obtain the tenant ID from the user.
You can achieve this by building a UI that asks them to enter their tenant ID or organization name (which can serve as the tenant ID).
This example implements the UI in a component called AuthPage.
import { useState } from "react";
import * as reactRouterDom from "react-router-dom";
import { Routes } from "react-router-dom";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
import { useSessionContext } from "supertokens-auth-react/recipe/session";
export const AuthPage = () => {
const location = reactRouterDom.useLocation();
const [inputTenantId, setInputTenantId] = useState("");
const tenantId = localStorage.getItem("tenantId") ?? undefined;
const session = useSessionContext();
if (session.loading) {
return null;
}
if (
tenantId !== undefined || // if we have a tenantId stored
session.doesSessionExist === true || // or an active session (it'll contain the tenantId)
new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId
) {
// Since this component (AuthPage) is rendered in the /auth route in the main Routes component,
// and we are rendering this in a sub route as shown below, the third arg to getSuperTokensRoutesForReactRouterDom
// tells SuperTokens to create Routes without /auth prefix to them, otherwise they would
// render on /auth path.
return <Routes>{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI], "/auth")}</Routes>;
} else {
return (
<form
onSubmit={() => {
// this value will be read by SuperTokens as shown in the next steps.
localStorage.setItem("tenantId", inputTenantId);
}}
>
<h2>Enter your organization's name:</h2>
<input type="text" value={inputTenantId} onChange={(e) => setInputTenantId(e.target.value)} />
<br />
<button type="submit">Next</button>
</form>
);
}
};import { useState } from "react";
import { getRoutingComponent } from "supertokens-auth-react/ui";
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
import { useSessionContext } from "supertokens-auth-react/recipe/session";
export const AuthPage = () => {
const [inputTenantId, setInputTenantId] = useState("");
const tenantId = localStorage.getItem("tenantId") ?? undefined;
const session = useSessionContext();
if (session.loading) {
return null;
}
if (
tenantId !== undefined || // if we have a tenantId stored
session.doesSessionExist === true || // or an active session (it'll contain the tenantId)
new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId
) {
return getRoutingComponent([EmailPasswordPreBuiltUI]);
} else {
return (
<form
onSubmit={() => {
// this value will be read by SuperTokens as shown in the next steps.
localStorage.setItem("tenantId", inputTenantId);
}}
>
<h2>Enter your organization's name:</h2>
<input type="text" value={inputTenantId} onChange={(e) => setInputTenantId(e.target.value)} />
<br />
<button type="submit">Next</button>
</form>
);
}
};The example creates a simple UI that asks the user for their organization’s name. Their input serves as their tenant ID. When the user submits that form, the value is stored in local storage.
You need to build a UI that asks the user to enter their tenant ID or organization name (which can serve as the tenant ID). The input value is then used in function calls, as shown below.
Once you have the user’s tenant ID, you can fetch their list of configured providers and render the third party login buttons accordingly:
import Multitenancy from "supertokens-web-js/recipe/multitenancy";
async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) {
const loginMethods = await Multitenancy.getLoginMethods({
tenantId,
});
if (loginMethods.firstFactors.includes("thirdparty")) {
const providers = loginMethods.thirdParty.providers;
if (providers.find((i) => i.id === "active-directory")) {
// render sign in with Active Directory button
} else {
// more checks for other providers
}
} else {
// thirdparty login is disabled for the tenant
}
}import Multitenancy from "supertokens-web-js/recipe/multitenancy";
async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) {
const loginMethods = await Multitenancy.getLoginMethods({
tenantId,
});
if (loginMethods.firstFactors.includes("thirdparty")) {
const providers = loginMethods.thirdParty.providers;
if (providers.find((i) => i.id === "active-directory")) {
// render sign in with Active Directory button
} else {
// more checks for other providers
}
} else {
// thirdparty login is disabled for the tenant
}
}curl --location --request GET '<YOUR_API_DOMAIN>/auth/loginmethods'- The code snippet fetches the login methods for the tenant ID.
- It then renders the login UI buttons based on the configured
thirdPartyIdvalues in the response.
The response body from the API call has a status property in it:
status: "OK": Therecipesfield contains information about which login methods are active along with the list of third party providers configured for this tenant.status: "GENERAL_ERROR": This is only possible if you have overridden the backend API to send back a custom error message which should appear on the frontend.
2. Include the tenant ID in authentication flow
You need to tell SuperTokens how to resolve the tenant ID.
To do this, set the getTenantId function in the Multitenancy recipe.
In the current example, local storage provides the tenantId.
import React from "react";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
apiBasePath: "...",
websiteBasePath: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
},
};
},
},
}),
// other recipes...
],
});// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded)
supertokensUIInit({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
apiBasePath: "...",
websiteBasePath: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
supertokensUIMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
},
};
},
},
}),
// other recipes...
],
});Initialize the multitenancy recipe with the following callback. You can get the tenant ID from wherever you stored it after asking the user for it.
All the steps for mobile app login are similar to the social login steps. However, when you are calling the sign in up API, you also need to pass in the tenantId in the request path. An example of this appears below:
import SuperTokens from "supertokens-web-js";
import Multitenancy from "supertokens-web-js/recipe/multitenancy";
SuperTokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
},
recipeList: [
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
},
};
},
},
}),
// other recipes...
],
});supertokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
},
recipeList: [
supertokensMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
let tid = localStorage.getItem("tenantId");
return tid === null ? undefined : tid;
},
};
},
},
}),
// other recipes...
],
});curl --location --request POST '<YOUR_API_DOMAIN>/auth/signinup' \
--header 'Content-Type: application/json' \
--data-raw '{
"thirdPartyId": "...",
"clientType": "...",
"oAuthTokens": {
"access_token": "...",
"id_token": "..."
},
}'3. Redirect users based on tenant subdomain (optional)
If each tenant has access to specific subdomains in your application, redirect users after sign-in.
3.1 Restrict subdomain access
Before redirecting users, restrict which subdomains their sessions can be used on.
To do this configure the SDK to know which domain each tenantId has access to.
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 subdomain 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 subdomain itself
return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil
},
}),
},
})
}from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import multitenancy
from typing import Dict, Any, List
async def get_allowed_domains_for_tenant_id(tenant_id: str, user_context: Dict[str, Any]) -> List[str]:
return [tenant_id + ".myapp.com", "myapp.com", "www.myapp.com"]
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="django", # Change this to "flask" or "fastapi" if you are using Flask or FastAPI
recipe_list=[
multitenancy.init(
get_allowed_domains_for_tenant_id=get_allowed_domains_for_tenant_id
)
],
)The code sample tells SuperTokens to add the returned domains to the user’s session claims when they sign in. The claim is available on the frontend and backend and can restrict where the session is used.
3.2 Redirect the user to their subdomain after sign-in
After sign-in, the frontend SDK redirects the user to the / route by default.
You can instead redirect them to their subdomain based on their tenant ID.
import SuperTokens from "supertokens-auth-react";
import Session from "supertokens-auth-react/recipe/session";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
getRedirectionURL: async (context) => {
if (context.action === "SUCCESS" && context.newSessionCreated) {
let claimValue: string[] | undefined = await Session.getClaimValue({
claim: Multitenancy.AllowedDomainsClaim,
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default subdomain
}
}
return undefined;
},
recipeList: [
/* Recipe init here... */
],
});// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded)
supertokensUIInit({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
getRedirectionURL: async (context) => {
if (context.action === "SUCCESS" && context.newSessionCreated) {
let claimValue: string[] | undefined = await supertokensUISession.getClaimValue({
claim: supertokensUIMultitenancy.AllowedDomainsClaim,
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default subdomain
}
}
return undefined;
},
recipeList: [
/* Recipe init here... */
],
});On the frontend, after the user signs in, you can read the domain from their session and redirect them accordingly.
import Session from "supertokens-web-js/recipe/session";
import Multitenancy from "supertokens-web-js/recipe/multitenancy";
async function redirectToSubDomain() {
if (await Session.doesSessionExist()) {
let claimValue: string[] | undefined = await Session.getClaimValue({
claim: Multitenancy.AllowedDomainsClaim,
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default subdomain
}
} else {
window.location.href = "/auth";
}
}async function redirectToSubDomain() {
if (await supertokensSession.doesSessionExist()) {
let claimValue: string[] | undefined = await supertokensSession.getClaimValue({
claim: supertokensMultitenancy.AllowedDomainsClaim,
});
if (claimValue !== undefined) {
window.location.href = "https://" + claimValue[0];
} else {
// there was no configured allowed domain for this user. Throw an error cause of
// misconfig or redirect to a default subdomain
}
} else {
window.location.href = "/auth";
}
}- The
AllowedDomainsClaimclaim is auto added to the session by the backend SDK if you provide theGetAllowedDomainsForTenantIdconfiguration from the previous step. - This claim contains the domains configured for the session’s tenant ID. It is not proof of user membership or authorization to business data.
6. Share sessions across subdomains (optional)
If the user authenticates on your main website domain (https://example.com/auth) and is redirected to a subdomain, update the Session recipe to share sessions across subdomains.
You can do this by setting the sessionTokenFrontendDomain value in the Session recipe.
If the subdomains assigned to your tenants have their own backends on separate subdomains (one per tenant), you can also enable sharing of sessions across API domains.
7. Limit session use to the tenant’s subdomain (optional)
The frontend uses session claim validators to restrict session use by subdomain.
Before proceeding, make sure that you define the GetAllowedDomainsForTenantId function mentioned above.
This adds the list of allowed domains into the user’s access token payload.
On the frontend, check whether the current subdomain is in the session’s allowed domains.
If it is not, redirect the user to the correct subdomain.
You can achieve this by using the hasAccessToCurrentDomain session validator from the multitenancy recipe.
You need to make changes to the auth route configuration, as well as to the supertokens-web-js SDK configuration at the root of your application:
This change is in your auth route configuration.
import React from "react";
import Session from "supertokens-auth-react/recipe/session";
import { AllowedDomainsClaim } from "supertokens-auth-react/recipe/multitenancy";
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await Session.getClaimValue({
claim: AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
});// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded)
supertokensUISession.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await supertokensUISession.getClaimValue({
claim: supertokensMultitenancy.AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
});Above, in Session.init on the frontend, add the hasAccessToCurrentDomain claim validator to the global validators. This means that whenever a route requires protection, it checks if hasAccessToCurrentDomain has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the AllowedDomainsClaim session claim.
This change goes in the supertokens-web-js SDK configuration at the root of your application:
import Session from "supertokens-web-js/recipe/session";
import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy";
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await Session.getClaimValue({
claim: AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
});Above, in Session.init on the frontend, add the hasAccessToCurrentDomain claim validator to the global validators. This means that whenever a route requires protection, it checks if hasAccessToCurrentDomain has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the AllowedDomainsClaim session claim.
import Session from "supertokens-web-js/recipe/session";
import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy";
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await Session.getClaimValue({
claim: AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
});supertokensSession.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [
...claimValidatorsAddedByOtherRecipes,
{
...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(),
onFailureRedirection: async () => {
let claimValue = await supertokensSession.getClaimValue({
claim: supertokensMultitenancy.AllowedDomainsClaim,
});
return "https://" + claimValue![0];
},
},
],
}),
},
});Above, in Session.init on the frontend, add the hasAccessToCurrentDomain claim validator to the global validators. This means that whenever a route requires protection, it checks if hasAccessToCurrentDomain has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the AllowedDomainsClaim session claim.