Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

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

Enable paid features

This feature is only available to paid users. Follow the instructions below to enable it.

Managed Service

  1. Sign in to the SuperTokens dashboard.
  2. Select the managed service option from the service type select component.
  3. Select your core instance from the next elemenet or create a new one.
  4. Open Features sub-page and enable the required ones.

Self Hosted

  1. Sign in to the SuperTokens dashboard.
  2. Select the self-hosted option from the service type select component.
  3. Select your license key from the next elemenet or create a new one. Then enable the required features.
  4. If the key is not yet configured, add it to your Core service. If your Core already uses this key, no configuration changes are required.

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

UI type

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.

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...
  ],
});

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... */
  ],
});
  • The AllowedDomainsClaim claim is auto added to the session by the backend SDK if you provide the GetAllowedDomainsForTenantId configuration 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:

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.


See also

API reference

API schema and response details