Initial setup - SuperTokens Docs

Initial setup

Configure your application for multi-factor authentication with account linking and tenant-specific configurations.

Design and implement MFA for an existing application.

Copy promptCopied

Design and implement SuperTokens MFA in this application. Inspect the existing recipes, tenant model, and deployment type first. Ask which first factors, second factors, and enforcement policy are required if they cannot be inferred. Check managed-service and SDK support, configure account linking, tenant-specific factors, email verification dependencies, frontend and backend recipes, and protected routes. Do not commit secrets. Validate first-factor login, second-factor enrollment, challenge completion, recovery, and route protection with the relevant tests and build.

Overview

To integrate multi-factor authentication, MFA, in your application, you first need to decide on what factors you want to support and when to ask for them. The following guide shows you how to implement a basic setup while also covering customization methods.

Before you start

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.

These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the MFA concepts page.

If you plan to use the otp-email factor as a form of email verification, you also need to initialize the emailverification recipe in REQUIRED mode on the backend. This configuration ensures that the email verification process passes only if the originally provided email has been verified.

Steps

Tenant setup

Single tenantMulti-tenant

1. Set up the backend

1.1 Enable account linking

MFA requires account linking to be active. You can enable it in the following way:

Go

Node.jsGoPython

Language

Node.js▼

import SuperTokens, { User, RecipeUserId } from "supertokens-node";
import { UserContext } from "supertokens-node/types";
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types";
import { SessionContainerInterface } from "supertokens-node/recipe/session/types";

SuperTokens.init({
  supertokens: {
    connectionURI: "...",
  },
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [\
    // ...\
    AccountLinking.init({\
      shouldDoAutomaticAccountLinking: async (\
        newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId },\
        user: User | undefined,\
        session: SessionContainerInterface | undefined,\
        tenantId: string,\
        userContext: UserContext,\
      ) => {\
        if (session === undefined) {\
          // we do not want to do first factor account linking by default. To enable that,\
          // please see the automatic account linking docs in the recipe docs for your first factor.\
          return {\
            shouldAutomaticallyLink: false,\
          };\
        }\
        if (user === undefined || session.getUserId() === user.id) {\
          // if it comes here, it means that a session exists, and we are trying to link the\
          // newAccountInfo to the session user, which means it's an MFA flow, so we enable\
          // linking here.\
          return {\
            shouldAutomaticallyLink: true,\
            shouldRequireVerification: true,\
          };\
        }\
        return {\
          shouldAutomaticallyLink: false,\
        };\
      },\
    }),\
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import accountlinking
from supertokens_python.types import User
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink
from typing import Dict, Any, Optional, Union

async def should_do_automatic_account_linking(
    new_account_info: AccountInfoWithRecipeIdAndUserId,
    user: Optional[User],
    session: Optional[SessionContainer],
    tenant_id: str,
    user_context: Dict[str, Any]
) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]:
    if session is None:
        # We do not want to do first factor account linking by default.
        # To enable that, please see the automatic account linking docs
        # in the recipe docs for your first factor.
        return ShouldNotAutomaticallyLink()

if user is None or session.get_user_id() == user.id:
        # If it comes here, it means that a session exists, and we are trying to link the
        # account_info to the session user, which means it's an MFA flow, so we enable
        # linking here.
        return ShouldAutomaticallyLink(should_require_verification=True)

return ShouldNotAutomaticallyLink()

init(
    app_info=InputAppInfo(
        app_name="...",
        api_domain="...",
        website_domain="...",
    ),
    supertokens_config=SupertokensConfig(
        connection_uri="...",
    ),
    framework='...',
    recipe_list=[\
        accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking)\
    ],
)

1.2 Configure the first factors

We start by initializing the MFA recipe on the backend and specifying the list of first factors using their factor IDs. You still have to initialize all the auth recipes in the recipeList, and configure them based on your needs.

For example, the code below initializes thirdparty, emailpassword and passwordless recipes and sets the firstFactor array to be ["emailpassword", "thirdparty"]. This means that email password and social login appear to the user as the first factor (using the thirdparty + emailpassword recipe), and passwordless serves as the second factor.

Go

Node.jsGoPython

Language

Node.js▼

import supertokens from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Passwordless from "supertokens-node/recipe/passwordless";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

supertokens.init({
  supertokens: {
    connectionURI: "...",
  },
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [\
    // ...\
    ThirdParty.init({\
      //...\
    }),\
    EmailPassword.init({\
      //...\
    }),\
    Passwordless.init({\
      contactMethod: "EMAIL",\
      flowType: "USER_INPUT_CODE",\
    }),\
    MultiFactorAuth.init({\
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],\
    }),\
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import (
    emailpassword,
    multifactorauth,
    passwordless,
    session,
    thirdparty,
)
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
from supertokens_python.recipe.multifactorauth.types import FactorIds

init(
    app_info=InputAppInfo(
        app_name="...",
        api_domain="...",
        website_domain="...",
    ),
    supertokens_config=SupertokensConfig(
        connection_uri="...",
    ),
    framework='...',
    recipe_list=[\
        session.init(),\
        emailpassword.init(),\
        thirdparty.init(),\
        passwordless.init(contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"),\
        multifactorauth.init(first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY])\
    ],
)

Other combinations of first factors exists. For example, if you want passwordless as the first factor, then you would init the passwordless recipe and add "passwordless" in the firstFactors array.

For a multi-tenancy setup, where each tenant can have a different set of first factors, you can leave the firstFactors array as undefined in the MultiFactorAuth.init. Configure the firstFactors on a per-tenant basis when creating or updating a tenant as shown below:

Go

Dashboard

In the above setting, Email Password is active in the Login Methods section. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears.

By default, no login methods activate for a tenant.

Node.jsGoPythoncURLDashboard

Language

Node.js▼

import Multitenancy from "supertokens-node/recipe/multitenancy";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

async function createNewTenant() {
  let resp = await Multitenancy.createOrUpdateTenant("customer1", {
    firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD],
  });

if (resp.createdNew) {
    // Tenant created successfully
  } else {
    // Existing tenant's config was modified.
  }
}

Asyncio

from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
from supertokens_python.recipe.multifactorauth.types import FactorIds

async def create_new_tenant():
    resp = await create_or_update_tenant(
        "customer1", TenantConfigCreateOrUpdate(first_factors=[FactorIds.EMAILPASSWORD])
    )

if resp.created_new:
        # Tenant created successfully
        pass
    else:
        # Existing tenant's config was modified
        pass

Syncio

from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
from supertokens_python.recipe.multifactorauth.types import FactorIds

def create_new_tenant():
    resp = create_or_update_tenant(
        "customer1", TenantConfigCreateOrUpdate(first_factors=[FactorIds.EMAILPASSWORD])
    )

if resp.created_new:
        # Tenant created successfully
        pass
    else:
        # Existing tenant's config was modified
        pass
curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \
--header 'api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
    "tenantId": "customer1",
    "firstFactors": ["emailpassword"]
}'

Node.js

firstFactors includes only "emailpassword". This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears.

cURL

1.3 Configure the second factor

This section explains how to configure SuperTokens such that a second factor is necessary for all users during sign up and during sign in. TOTP serves as an example for the second factor.

The following code snippet accomplishes this:

Go

Node.jsGoPython

Language

Node.js▼

import SuperTokens, { User, RecipeUserId } from "supertokens-node";
import { UserContext } from "supertokens-node/types";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Passwordless from "supertokens-node/recipe/passwordless";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import totp from "supertokens-node/recipe/totp";
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types";
import { SessionContainerInterface } from "supertokens-node/recipe/session/types";

SuperTokens.init({
  supertokens: {
    connectionURI: "...",
  },
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [\
    // ...\
    ThirdParty.init({\
      //...\
    }),\
    EmailPassword.init({\
      //...\
    }),\
    Passwordless.init({\
      contactMethod: "EMAIL",\
      flowType: "USER_INPUT_CODE",\
    }),\
    totp.init(),\
    MultiFactorAuth.init({\
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],\
      override: {\
        functions: (originalImplementation) => {\
          return {\
            ...originalImplementation,\
            getMFARequirementsForAuth: async function (input) {\
              return [MultiFactorAuth.FactorIds.TOTP];\
            },\
          };\
        },\
      },\
    }),\
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import (
    emailpassword,
    multifactorauth,
    passwordless,
    session,
    thirdparty,
    totp,
)
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig
from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface
from typing import Dict, Any, Callable, Awaitable, List
from supertokens_python.types import User
from supertokens_python.recipe.multifactorauth.types import MFARequirementList

def override_functions(original_implementation: RecipeInterface):
    async def get_mfa_requirements_for_auth(
        tenant_id: str,
        access_token_payload: Dict[str, Any],
        completed_factors: Dict[str, int],
        user: Callable[[], Awaitable[User]],
        factors_set_up_for_user: Callable[[], Awaitable[List[str]]],
        required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]],
        required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]],
        user_context: Dict[str, Any],
    ) -> MFARequirementList:
        return [FactorIds.TOTP]

original_implementation.get_mfa_requirements_for_auth = (
        get_mfa_requirements_for_auth
    )
    return original_implementation

init(
    app_info=InputAppInfo(
        app_name="...",
        api_domain="...",
        website_domain="...",
    ),
    supertokens_config=SupertokensConfig(
        connection_uri="...",
    ),
    framework="...",
    recipe_list=[\
        session.init(),\
        emailpassword.init(),\
        thirdparty.init(),\
        passwordless.init(\
            contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"\
        ),\
        totp.init(),\
        multifactorauth.init(\
            first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],\
            override=OverrideConfig(functions=override_functions),\
        ),\
    ],
)

In the above snippet, you configure email password and social login as the first factor, followed by TOTP as the second factor.

After sign in or sign up, SuperTokens calls the getMFARequirementsForAuth function to get a list of secondary factors for the user. The returned value determines the boolean value of v that’s stored in the session’s access token payload. If the returned factor is already completed (it’s in the c object of the session’s payload), then the value of v is true, else false.

In the above example, "totp" returns as a required factor for all users. However, you can also dynamically decide which factor to return based on the input arguments, which contains the User object, the tenantId, and the current session’s access token payload. The default implementation of getMFARequirementsForAuth returns the set of factors specifically enabled for this user (see next section) or for the tenant (see later section).

The output of this function can be more complex than a string[]. You can also return an object which tells SuperTokens that any one of the factors must satisfy:

Require one of multiple factors

Go

Node.jsGoPython

Language

Node.js▼

import supertokens from "supertokens-node";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

supertokens.init({
  supertokens: {
    connectionURI: "...",
  },
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [\
    // ...\
    MultiFactorAuth.init({\
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],\
      override: {\
        functions: (originalImplementation) => {\
          return {\
            ...originalImplementation,\
            getMFARequirementsForAuth: async function (input) {\
              return [\
                {\
                  oneOf: [MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL],\
                },\
              ];\
            },\
          };\
        },\
      },\
    }),\
  ],
});

original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation


Require multiple factors in any order

Go

Node.jsGoPython

Language

Node.js▼

import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [
// ...
MultiFactorAuth.init({
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
return [
{
allOfInAnyOrder: [MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL],
},
];
},
};
},
},
}),
], });


original_implementation.get_mfa_requirements_for_auth = (
        get_mfa_requirements_for_auth
    )
    return original_implementation

Require multiple factors in a specific order

Go

Node.jsGoPython

Language

Node.js▼

import supertokens from "supertokens-node";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

supertokens.init({
  supertokens: {
    connectionURI: "...",
  },
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [\
    // ...\
    MultiFactorAuth.init({\
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],\
      override: {\
        functions: (originalImplementation) => {\
          return {\
            ...originalImplementation,\
            getMFARequirementsForAuth: async function (input) {\
              let currentCompletedFactors = MultiFactorAuth.MultiFactorAuthClaim.getValueFromPayload(\
                input.accessTokenPayload,\
              );\
              if (MultiFactorAuth.FactorIds.TOTP in currentCompletedFactors.c) {\
                // this means the totp factor is completed\
                return [MultiFactorAuth.FactorIds.OTP_EMAIL];\
              } else {\
                // this means we have not finished totp yet, and we want\
                // to do that right after first factor login\
                return [MultiFactorAuth.FactorIds.TOTP];\
              }\
            },\
          };\
        },\
      },\
    }),\
  ],
});

def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: current_completed_factors = MultiFactorAuthClaim.get_value_from_payload( access_token_payload ) if current_completed_factors and FactorIds.TOTP in current_completed_factors.c: # this means the totp factor is completed return [FactorIds.OTP_EMAIL] else: # this means we have not finished totp yet, and we want # to do that right after first factor login return [FactorIds.TOTP]

original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation


For a multi tenant setup, you can configure a list of secondary factors when creating / modifying a tenant as shown below:

Go

Dashboard

As shown above, you turn on `OTP - Email` in the **Secondary Factors** section which means that all users who log into that tenant must complete `otp-email` as a second factor.

You can also turn off all factors to have no secondary factors required for the tenant.

If you turn on more than one factor, it means that the user must complete any one of factors that are active. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below:

Node.jsGoPythoncURLDashboard

Language

Node.js▼

import Multitenancy from "supertokens-node/recipe/multitenancy"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL], });

if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } }


Asyncio

async def create_new_tenant():
    resp = await create_or_update_tenant(
        "customer1", TenantConfigCreateOrUpdate(
            first_factors=[FactorIds.EMAILPASSWORD],
            required_secondary_factors=[FactorIds.OTP_EMAIL],
        )
    )

if resp.created_new:
        # Tenant created successfully
        pass
    else:
        # Existing tenant's config was modified
        pass

Syncio

def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.OTP_EMAIL], ) )

if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass


curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2'
--header 'api-key: YOUR_API_KEY'
--header 'Content-Type: application/json'
--data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword"], "requiredSecondaryFactors": ["otp-email"] }'


Node.js

In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor.

To remove the `requiredSecondaryFactors` configuration for a tenant, you can pass a `null` value for the `requiredSecondaryFactors` key in the tenant configuration.

If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below:

cURL

To remove the `requiredSecondaryFactors` configuration for a tenant, you can turn off all the toggles.

Go

Node.jsGoPythoncURL

Language

Node.js▼

import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [
// ...
MultiFactorAuth.init({
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
return [
{
allOfInAnyOrder: await input.requiredSecondaryFactorsForTenant,
},
];
},
};
},
},
}),
], });


from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList

original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation

init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[
session.init(),
emailpassword.init(),
thirdparty.init(),
passwordless.init(
contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"
),
multifactorauth.init(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
override=OverrideConfig(functions=override_functions),
),
], )


Notice that the input to the function contains the `requiredSecondaryFactorsForTenant` array. This would be the same list that you passed to the tenant configuration when creating / modifying the tenant as shown in the previous steps.

#### [1.4 Remove the second factor requirement (optional)](/content/docs/additional-verification/mfa/initial-setup?q=BcZjrHzAE\#14-remove-the-second-factor-requirement-optional/index.html)

Instead of configuring a factor for all users in your app, or for all users within a tenant, you may want to implement a flow in which users do MFA only if they have enabled it for themselves. Here, users may also want to choose what factors they would like to enable for themselves.

This flow allows users to configure their MFA preferences in the settings page in your app’s frontend. A pre-built UI for this is not yet provided, but in this section, we explain the setup on the backend.

You want to start by creating an API that does [session verification](/content/docs/additional-verification/session-verification/protect-api-routes/index.html), and then enable the desired factor for the user. For example, if the user wants to enable TOTP, then you would call the following function in your API:

Go

Node.jsGoPython

Language

Node.js▼

import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); }


Asyncio

from supertokens_python.recipe.multifactorauth.asyncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds

async def enable_mfa_for_user(user_id: str): await add_to_required_secondary_factors_for_user( user_id, FactorIds.TOTP )


Syncio

from supertokens_python.recipe.multifactorauth.syncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds

def enable_mfa_for_user(user_id: str): add_to_required_secondary_factors_for_user( user_id, FactorIds.TOTP )


The effect of the above function call is that in the default implementation of `getMFARequirementsForAuth`, the factors specifically enabled for the input user are considered. By default, if you add multiple factors for a user ID, then it would require them to complete any one of those secondary factors during login.

If you want to change the default behavior from “any one of” to something else (like “all of”), you can do this by overriding the `getMFARequirementsForAuth` function:

Go

Node.jsGoPython

Language

Node.js▼

import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [
// ...
MultiFactorAuth.init({
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
return [
{
allOfInAnyOrder: await input.requiredSecondaryFactorsForUser,
},
];
},
};
},
},
}),
], });


from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList

original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation

init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[
multifactorauth.init(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
override=OverrideConfig(functions=override_functions),
),
], )


Once you call the `addToRequiredSecondaryFactorsForUser` function for a user, SuperTokens stores this preference in the user metadata JSON of the user. For example, if you add `"totp"` as a required secondary factor for a user, this preference is stored in the metadata JSON as:

{ "_supertokens": { "requiredSecondaryFactors": ["totp"] } }


You can view this JSON on the [user details page of the user management dashboard](/content/docs/post-authentication/dashboard/user-management/index.html) and modify it manually if you like.

To know the factors that a user has enabled, you can use the following function:

Go

Node.jsGoPython

Language

Node.js▼

import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.TOTP); }


Asyncio

from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds

async def is_totp_enabled_for_user(user_id: str): factors = await get_required_secondary_factors_for_user( user_id ) return FactorIds.TOTP in factors


Syncio

from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds

def is_totp_enabled_for_user(user_id: str): factors = get_required_secondary_factors_for_user( user_id ) return FactorIds.TOTP in factors


Using the above function, you can build your settings page on the frontend which displays the existing enabled factors for the user. Allow users to enable or disable factors as they like.

Once you have enabled a factor for a user, you take them to that factor setup screen if they have not previously already setup the factor. To know if a factor is setup, you can call the following function (on the backend):

Go

Node.jsGoPython

Language

Node.js▼

import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

async function isTotpSetupForUser(userId: string) { let factors = await MultiFactorAuth.getFactorsSetupForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.TOTP); }


Asyncio

from supertokens_python.recipe.multifactorauth.asyncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds

async def is_totp_enabled_for_user(user_id: str): factors = await get_factors_setup_for_user( user_id ) return FactorIds.TOTP in factors


Syncio

from supertokens_python.recipe.multifactorauth.syncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds

def is_totp_enabled_for_user(user_id: str): factors = get_factors_setup_for_user( user_id ) return FactorIds.TOTP in factors


Or you can call the [`MFAInfo` endpoint](/content/docs/additional-verification/mfa/initial-setup#the-mfa-info-endpoint/index.html) from the frontend which returns information indicating which factors have already been setup for the user and which not.

A factor is considered setup if the user has gone through that factor’s flow at least once. For example, if the user has created and verified a TOTP device, only then does the `getFactorsSetupForUser` function return `totp` as part of the array. Likewise, if the user has completed `otp-email` or `link-email` once, only then do these factors become a part of the returned array. Let’s take two examples:

- The first time the user enables TOTP, then the result of `getFactorsSetupForUser` does not contain `"totp"`. You should redirect the user to the TOTP setup screen. Once they add and verify a device, then `getFactorsSetupForUser` returns `["totp"]` even if they later disable TOTP from the settings page and re-enable it.
- Let’s say that the first factor for a user is `emailpassword`, and the second factor is `otp-email`. Once they sign up, SuperTokens already knows the email for the user, when they are doing the `otp-email` step, then they are not asked to enter their email again (that is, an OTP is directly sent to them). However, until they actually complete the OTP flow, `getFactorsSetupForUser` does not return `["otp-email"]` as part of the output.

The subsequent sections in this doc walk through frontend setup, and also specific examples of common MFA flows.

### [2\. Set up the frontend](/content/docs/additional-verification/mfa/initial-setup?q=BcZjrHzAE\#2-set-up-the-frontend/index.html)

UI type

Pre-built UIDrop-in components that handle the flows for you.Custom UIBuild your own UI and call the SDK directly.

The pre-built UI provides support for the following MFA methods:

- TOTP
- Email / phone OTP

If you want other types of MFA (like magic links, or password), please consider checking out the custom UI second.

We start by initialising the MFA recipe on the frontend and providing the list of first factors as shown below:

Angular

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.

ReactjsAngular

Frontend framework

React▼

import supertokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";

supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [
EmailPassword.init(/* ... */),
Passwordless.init({
contactMethod: "EMAIL_OR_PHONE",
}),
MultiFactorAuth.init({
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
}),
], });


// 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: "...", }, recipeList: [
supertokensUIEmailPassword.init(/* ... */),
supertokensUIPasswordless.init({
contactMethod: "EMAIL_OR_PHONE",
}),
supertokensUIMultiFactorAuth.init({
firstFactors: [
supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD,
supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY,
],
}),
], });


Angular

This change goes in the `supertokens-web-js` SDK configuration at the root of your application:

Angular

import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Session from "supertokens-web-js/recipe/session";

SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [Session.init(), MultiFactorAuth.init()], });


In the above snippet, `thirdparty` and email password are configured as first factors. The second factor is determined [on the backend](/content/docs/additional-verification/mfa/initial-setup#1-set-up-the-backend/index.html), based on the boolean value of [`v` in the MFA claim in the session](/content/docs/additional-verification/mfa/important-concepts#factors/index.html). If the `v` is `false` in the session, it means that there are still factors pending before the user has completed login. In this case, the frontend SDK calls the `MFAInfo` endpoint (see more about this later) on the backend which returns the list of factors (`string[]`) that the user must complete next. For example:

- If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login.
- If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with.
- If the `next` is empty, it means that:

- A misconfiguration exists on the backend. This would show an access denied screen to the user. OR;
  - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`’s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified.

If you notice, in the above code snippet, `Passwordless.init` is also included, and this handles cases where the second factor is `otp-email` or `otp-phone`. For TOTP, a different recipe is used as shown later in this guide.

For a multi-factor setup, the first factors are selected based on [the configuration of the tenant](/content/docs/additional-verification/mfa/initial-setup#12-configure-the-first-factors/index.html). Each tenant has a `firstFactors` array configuration which determines the login options shown for that tenant. For MFA, the login options are determined by the [`requiredSecondaryFactors` configuration on the tenant](/content/docs/additional-verification/mfa/initial-setup#13-configure-the-second-factor/index.html), or based on the customisations for `getMFARequirementsForAuth` on the backend.

To tell the frontend to dynamically load the factors based on the tenant, four things need to supply:

- The current `tenantId`
- Enable dynamic login methods
- Add `MultiFactorAuth.init` to the recipe list without any configured `firstFactors`
- Init all the recipes that can be possibly used by any tenant as the first or second factor.

Angular

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.

ReactjsAngular

import supertokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy";

supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [
Multitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
// Implement the following based on the UX flow you want for
// tenant discovery
return "TODO..";
},
};
},
},
}),
EmailPassword.init(/* ... */),
Passwordless.init({
contactMethod: "EMAIL_OR_PHONE",
}),
MultiFactorAuth.init(),
], });


// 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: "...", }, usesDynamicLoginMethods: true, recipeList: [
supertokensUIMultitenancy.init({
override: {
functions: (oI) => {
return {
...oI,
getTenantId: (input) => {
// Implement the following based on the UX flow you want for
// tenant discovery
return "TODO..";
},
};
},
},
}),
supertokensUIEmailPassword.init(/* ... */),
supertokensUIPasswordless.init({
contactMethod: "EMAIL_OR_PHONE",
}),
supertokensUIMultiFactorAuth.init(),
], });


Angular

This change goes in the `supertokens-web-js` SDK configuration at the root of your application:

Angular

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    apiBasePath: "...",
    appName: "...",
  },
  recipeList: [Session.init(), MultiFactorAuth.init()],
});

The second factor for a tenant is selected based on the secondaryFactors configuration for the tenant, or based on any custom implementation for the getMFARequirementsForAuth function. If the current user has specific MFA methods enabled for them, those are also shown as options as well. Overall, the list of secondary factors is used to build the next array returned from the MFAInfo endpoint (see more about this later). For example:

In the subsequent sections, specific MFA setup examples are given for your reference.

Usage with email verification

If you are also requiring email verification, the user must verify the email first, and then all the MFA challenges. For example, if the user has email password as the first factor, and then TOTP as a second factor, SuperTokens prompts the user to do email password login, followed by email verification, followed by TOTP.

To switch the order such that email verification happens after the secondary factors of MFA, follow the next code snippet.

ReactjsAngular

Frontend framework

React▼

import supertokens from "supertokens-auth-react";
import EmailVerification from "supertokens-auth-react/recipe/emailverification";
import Session from "supertokens-auth-react/recipe/session";

supertokens.init({
  appInfo: {
    appName: "...",
    apiDomain: "...",
    websiteDomain: "...",
  },
  recipeList: [\
    // other recipes...\
    EmailVerification.init({\
      mode: "REQUIRED",\
    }),\
    Session.init({\
      override: {\
        functions: (originalImplementation) => {\
          return {\
            ...originalImplementation,\
            getGlobalClaimValidators: (input) => {\
              let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(\
                (v) => v.id === EmailVerification.EmailVerificationClaim.id,\
              )!;\
              let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(\
                (v) => v.id !== EmailVerification.EmailVerificationClaim.id,\
              );\
              return [...filteredValidators, emailVerificationClaimValidator];\
            },\
          };\
        },\
      },\
    }),\
  ],
});
// 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: "...",
  },
  recipeList: [\
    // other recipes...\
    supertokensUIEmailVerification.init({\
      mode: "REQUIRED",\
    }),\
    supertokensUISession.init({\
      override: {\
        functions: (originalImplementation) => {\
          return {\
            ...originalImplementation,\
            getGlobalClaimValidators: (input) => {\
              let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(\
                (v) => v.id === supertokensUIEmailVerification.EmailVerificationClaim.id,\
              )!;\
              let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(\
                (v) => v.id !== supertokensUIEmailVerification.EmailVerificationClaim.id,\
              );\
              return [...filteredValidators, emailVerificationClaimValidator];\
            },\
          };\
        },\
      },\
    }),\
  ],
});

In the snippet above, the getGlobalClaimValidators function in the Session recipe is overridden to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which redirects the user to complete the MFA factors.

Handle misconfigurations

There can be situations of misconfigurations. For example you may have enabled otp-email for a user as a secondary factor, but did not add Passwordless (or ThirdPartyPasswordless) in the recipeList on the frontend. In such (and similar) situations, the pre-built UI on the frontend throws an error which is sent to the error boundary of your app. The way to solve these errors is to recheck the recipeList on the frontend, and make sure that it has all the recipes initialized that are necessary for any factor configured on the backend.

The access denied screen

Sometimes, users may end up seeing an access denied screen during the login flow. This appears if there is a 500 (backend sends a 500 status code) error during the MFA flow for API calls that are automatically initiated (without user action). For example:

You can override this component in the following way:

Angular

ReactjsAngular

Frontend framework

React▼

Do you use react-router-dom?

With React Router▼

With React Router

import React from "react";
import { SuperTokensWrapper } from "supertokens-auth-react";
import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session";

function App() {
  return (
    <SuperTokensWrapper>
      <SessionComponentsOverrideProvider
        components={{
          SessionAccessDenied_Override: ({ DefaultComponent, ...props }) => {
            return (
              <div>
                Access denied!
                {props.error === undefined ? null : props.error}
              </div>
            );
          },
        }}
      >
        {/* Rest of the JSX */}
      </SessionComponentsOverrideProvider>
    </SuperTokensWrapper>
  );
}
export default App;

Without React Router

import React from "react";
import { SuperTokensWrapper } from "supertokens-auth-react";
import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui";
import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session";

function App() {
  if (
    canHandleRoute([\
      /*...*/\
    ])
  ) {
    return (
      <SessionComponentsOverrideProvider
        components={{
          SessionAccessDenied_Override: ({ DefaultComponent, ...props }) => {
            return (
              <div>
                Access denied!
                {props.error === undefined ? null : props.error}
              </div>
            );
          },
        }}
      >
        {getRoutingComponent([\
          /*...*/\
        ])}
      </SessionComponentsOverrideProvider>
    );
  }
  return <SuperTokensWrapper>{/* Rest of the JSX */}</SuperTokensWrapper>;
}
export default App;

After the first factor sign in is over, to know the next auth challenge, the frontend should rely on the session’s access token payload MFA claim’s n array. For example, the access token payload may have the following content:

{
  "st-mfa": {
    "c": {
      "emailpassword": 1702877939
    },
    "v": false
  }
}

This means that the user has completed the email password login, and that there are still MFA login challenge(s) remaining (v is false).

1.1 Initialize the MFA recipe

Mobile

WebMobile

Platform

Web▼

npm

import SuperTokens from "supertokens-web-js";
import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth";

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    apiBasePath: "...",
    appName: "...",
  },
  recipeList: [\
    // other recipes...\
    MultiFactorAuth.init(),\
  ],
});

Script tag

supertokens.init({
  appInfo: {
    apiDomain: "...",
    apiBasePath: "...",
    appName: "...",
  },
  recipeList: [\
    // other recipes...\
    supertokensMultiFactorAuth.init(),\
  ],
});

1.2 Add the MFA flow

The overall lifecycle of a factor post sign in is as follows:

Asking for the first factor

This is the same as setting up a recipe per the other recipe guides. Please follow those.

Checking the v boolean value in the MFA claim

After the first factor is complete, the frontend needs to check if there are any pending MFA challenges. This can be done by reading the v claim from the session as shown below:

WebMobile

Platform

Web▼

Installation method

npm▼

npm

import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth";
import Session from "supertokens-web-js/recipe/session";

async function isAllMFACompleted() {
  if (await Session.doesSessionExist()) {
    let mfaClaim = await Session.getClaimValue({
      claim: MultiFactorAuth.MultiFactorAuthClaim,
    });
    if (mfaClaim === undefined) {
      // this can happen during migration where the session is an older one
      // that was created before MFA was introduced on the backend
      return true;
    } else {
      return mfaClaim.v;
    }
  } else {
    throw new Error("Illegal function call: For first factor setup, you do not need to call this function");
  }
}

Script tag

async function isAllMFACompleted() {
  if (await supertokensSession.doesSessionExist()) {
    let mfaClaim = await supertokensSession.getClaimValue({
      claim: supertokensMultiFactorAuth.MultiFactorAuthClaim,
    });
    if (mfaClaim === undefined) {
      // this can happen during migration where the session is an older one
      // that was created before MFA was introduced on the backend
      return true;
    } else {
      return mfaClaim.v;
    }
  } else {
    throw new Error("Illegal function call: For first factor setup, you do not need to call this function");
  }
}

ReactNative

import SuperTokens from "supertokens-react-native";

async function isAllMFACompleted() {
  if (await SuperTokens.doesSessionExist()) {
    let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v;
    return isMFACompleted;
  }
}

Android

import android.app.Application
import com.supertokens.session.SuperTokens
import org.json.JSONObject

class MainApplication: Application() {
    fun isAllMFACompleted(): Boolean {
        val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this);
        val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean;
        return isMFACompleted;
    }
}

iOS

import UIKit
import SuperTokensIOS

fileprivate class ViewController: UIViewController {
    func isAllMFACompleted() -> Bool {
        // Attempt to retrieve the access token payload securely
        if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(),
            // Extract the mfaObject from the accessTokenPayload
            let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any],
            // Determine if MFA has been completed
            let isMFACompleted: Bool = mfaObject["v"] as? Bool {
            // Return the MFA completion status
            return isMFACompleted
        }
        // Return false if any of the unwrapping fails, indicating MFA completion status cannot be confirmed
        return false
    }
}

Flutter

import 'package:supertokens_flutter/supertokens.dart';

Future<bool> isAllMFACompleted() async {
  var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely();

if (accessTokenPayload.containsKey("st-mfa")) {
    Map<String, dynamic> mfaObject = accessTokenPayload["st-mfa"];

if (mfaObject.containsKey("v")) {
      bool isMFACompleted = mfaObject["v"];

return isMFACompleted;
    }
  }
  return false; // Return false if "st-mfa" is not present or "v" is not found
}

Checking the next array

Once it is verified that MFA is still pending, the list of factors the user must do next needs to be retrieved. This can be done by calling the MFA Info endpoint which returns a list of next (string[]) factors:

Checking for factor setup

Once the user has picked a specific factor (or if next contains only one item), you need to check if that factor has already been setup for that user. A factor is setup already if:

If the user has the factor already setup, you can skip the setup step and directly ask them for the challenge:

In case the user does not have the factor setup, you need to ask them to set it up first:

In the later guides of this recipe, the use cases are described. If you want to know the status of any factor, you can get that by calling the MFA Info endpoint.

References

The MFA info endpoint

This is an important endpoint which can be utilized to:

Our pre-built UI uses this API automatically, but you can also always call this API manually if you are building a custom UI:

Mobile

Call the following API when you want to know the status of any factor. Notice that the API call requires the session’s access token as an input (this should be included by the frontend SDK automatically):

WebMobile

Platform

Web▼

Installation method

npm▼

npm

import MultifactorAuth from "supertokens-web-js/recipe/multifactorauth";
import Session from "supertokens-web-js/recipe/session";

async function fetchMFAInfo() {
  if (await Session.doesSessionExist()) {
    try {
      let mfaInfo = await MultifactorAuth.resyncSessionAndFetchMFAInfo();
      let factorEmails = mfaInfo.emails;
      let factorPhoneNumbers = mfaInfo.phoneNumbers;

let emailsForOTPEmail = factorEmails["otp-email"];
      let phoneNumbersForOTPPhone = factorPhoneNumbers["otp-phone"];
      let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp");
      let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email");
      let isOTPPhoneSetup = mfaInfo.factors.alreadySetup.includes("otp-phone");
      let next = mfaInfo.factors.next;

let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup;
    } catch (err: any) {
      if (err.isSuperTokensGeneralError === true) {
        // this may be a custom error message sent from the API by you.
        window.alert(err.message);
      } else {
        window.alert("Oops! Something went wrong.");
      }
    }
  } else {
    throw new Error("Illegal function call: For first factor setup, you do not need to call this function");
  }
}

Script tag

async function fetchMFAInfo() {
  if (await supertokensSession.doesSessionExist()) {
    try {
      let mfaInfo = await supertokensMultiFactorAuth.resyncSessionAndFetchMFAInfo();
      let factorEmails = mfaInfo.emails;
      let factorPhoneNumbers = mfaInfo.phoneNumbers;
curl --location --request PUT '<YOUR_API_DOMAIN>/auth/mfa/info' \
--header 'Authorization: Bearer ...'

Web

{
    "status": "OK",
    "factors": {
      "alreadySetup": ["totp", "otp-email", "..."],
      "allowedToSetup": ["otp-phone", "otp-email", "..."],
      "next": ["otp-phone", "..."]
    },
    "emails": {
      "otp-email": ["user1@example.com", "user2@example.com"],
      "link-email": ["user1@example.com", "user2@example.com"]
    },
    "phoneNumbers": {
      "otp-phone": ["+1234567890", "+1098765432"],
      "link-phone": ["+1234567890", "+1098765432"]
    }
}

If the array is empty, it means that there is no email associated with the user for that factor. This can happen only if the factor was not already setup. In this case, you should take the user to a screen to ask them to first enter an email, and then to the challenge screen.

The flow is further explained in the common flows guide later on.

Handle support cases

Some situations exist in which users may be locked out of their accounts and would need you to do certain steps to unlock their accounts. These cases are:

ERRCODE009

    {
      "status": "SIGN_IN_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_009)"
    }

ERRCODE010

    {
      "status": "SIGN_IN_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_010)"
    }

ERRCODE011

    {
      "status": "SIGN_IN_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_011)"
    }

ERRCODE012

    {
      "status": "SIGN_IN_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_012)"
    }

ERRCODE013

    {
      "status": "SIGN_UP_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_013)"
    }

ERRCODE014

    {
      "status": "SIGN_UP_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)"
    }

ERRCODE015

    {
      "status": "SIGN_UP_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)"
    }

ERRCODE016

    {
      "status": "SIGN_UP_NOT_ALLOWED",
      "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)"
    }

ERRCODE017

    {
      "status": "SIGN_IN_UP_NOT_ALLOWED",
      "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_017)"
    }

ERRCODE018

    {
      "status": "SIGN_IN_UP_NOT_ALLOWED",
      "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_018)"
    }

ERRCODE019

    {
      "status": "SIGN_IN_UP_NOT_ALLOWED",
      "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_019)"
    }

If you are using otp-email MFA factor as a form of email verification, you should also have emailverification recipe initialised in REQUIRED mode on the backend (no need to add it on the frontend since users won’t see that UI). This is for security reasons wherein during the sign up process, when asking for the otp-email challenge, the email the OTP is sent to is determined on the frontend (automatically). In this case, the following scenario is possible:

Of course, this is not the desired flow when you want to use otp-email as a form of email verification. To prevent this, you should have the emailverification recipe initialised in REQUIRED mode on the backend. This ensures that the email verification claim validator only passes if the email that’s verified is the one from the first factor (email A).

The above case is only possible during sign up, and not sign in. :::

Security considerations

SuperTokens enforces that a user has completed all the required factors by keeping track of and checking them in the user’s access token payload.


See also

Email & SMS OTP TOTP Step-up Authentication Backup Codes Protect Routes with MFA