Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

OTP for specific users

Implement MFA policy for specific users using OTP via email or SMS based on defined criteria.

This page shows how to implement an MFA policy that requires certain users to do the OTP challenge via email or SMS. You can decide which users based on any criteria. For example:

  • Only users that have an admin role are required to complete OTP; OR
  • Only users that have enabled OTP on their account require to do OTP; OR
  • Only users that have a paid account require to do OTP.

Whatever the criteria, the steps for implementing this type of flow are the same.

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.

Single tenant setup

Backend setup

Example 1: Only enable OTP for users that have an admin role

To start with, configure the backend in the following way:

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 MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
import UserRoles from "supertokens-node/recipe/userroles";
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: [
    Session.init(),
    UserRoles.init(),
    ThirdParty.init({
      //...
    }),
    EmailPassword.init({
      //...
    }),
    Passwordless.init({
      contactMethod: "EMAIL",
      flowType: "USER_INPUT_CODE",
    }),
    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,
        };
      },
    }),
    MultiFactorAuth.init({
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            getMFARequirementsForAuth: async function (input) {
              let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id);
              if (roles.roles.includes("admin")) {
                // we only want otp-email for admins
                return [MultiFactorAuth.FactorIds.OTP_EMAIL];
              } else {
                // no MFA for non-admin users.
                return [];
              }
            },
          };
        },
      },
    }),
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import (
    accountlinking,
    emailpassword,
    multifactorauth,
    passwordless,
    session,
    thirdparty,
    userroles,
)
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
from supertokens_python.recipe.multifactorauth.types import (
    FactorIds,
    OverrideConfig,
    MFARequirementList,
)
from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.accountlinking.types import (
    AccountInfoWithRecipeIdAndUserId,
    ShouldNotAutomaticallyLink,
    ShouldAutomaticallyLink,
)
from supertokens_python.types import User
from typing import Dict, Any, Callable, Awaitable, List, Optional, Union
from supertokens_python.recipe.userroles.asyncio import get_roles_for_user


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
        # new_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()


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:
        # Get roles for the user
        roles = await get_roles_for_user(tenant_id, (await user()).id)

        if "admin" in roles.roles:
            # We only want OTP_EMAIL for admins
            return [FactorIds.OTP_EMAIL]
        else:
            # No MFA for non-admin users
            return []

    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(),
        userroles.init(),
        emailpassword.init(),
        thirdparty.init(),
        passwordless.init(
            contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"
        ),
        accountlinking.init(
            should_do_automatic_account_linking=should_do_automatic_account_linking
        ),
        multifactorauth.init(
            first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
            override=OverrideConfig(functions=override_functions),
        ),
    ],
)

Override the getMFARequirementsForAuth function to indicate that otp-email applies only to users with the admin role. You can also have any other criteria here.

Example 2: Ask for OTP only for users that have enabled OTP on their account

To start with, configure the backend in the following way:

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 MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
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: [
    Session.init(),
    ThirdParty.init({
      //...
    }),
    EmailPassword.init({
      //...
    }),
    Passwordless.init({
      contactMethod: "EMAIL",
      flowType: "USER_INPUT_CODE",
      override: {
        apis: (oI) => {
          return {
            ...oI,
            consumeCodePOST: async function (input) {
              let response = await oI.consumeCodePOST!(input);
              if (response.status === "OK" && input.session !== undefined) {
                // We do this only if a session exists, which means that it's not being called for first factor login.

                // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata.
                // The multifactorauth recipe will pick this value up next time the user is trying to login, and
                // ask them to enter the OTP code.
                await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(
                  input.session.getUserId(),
                  MultiFactorAuth.FactorIds.OTP_EMAIL,
                );
              }
              return response;
            },
          };
        },
      },
    }),
    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,
        };
      },
    }),
    MultiFactorAuth.init({
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
    }),
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import (
    accountlinking,
    emailpassword,
    multifactorauth,
    passwordless,
    session,
    thirdparty,
)
from supertokens_python.recipe.multifactorauth.types import (
    FactorIds,
)
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.accountlinking.types import (
    AccountInfoWithRecipeIdAndUserId,
    ShouldNotAutomaticallyLink,
    ShouldAutomaticallyLink,
)
from supertokens_python.types import User
from typing import Dict, Any, Optional, Union
from supertokens_python.recipe.passwordless.interfaces import (
    RecipeInterface as PasswordlessRecipeInterface,
    ConsumeCodeOkResult,
)
from supertokens_python.recipe.multifactorauth.asyncio import (
    add_to_required_secondary_factors_for_user,
)


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
        # new_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()


def override_functions(original_implementation: PasswordlessRecipeInterface):
    original_consume_code = original_implementation.consume_code

    async def consume_code(
        pre_auth_session_id: str,
        user_input_code: Union[str, None],
        device_id: Union[str, None],
        link_code: Union[str, None],
        session: Optional[SessionContainer],
        should_try_linking_with_session_user: Union[bool, None],
        tenant_id: str,
        user_context: Dict[str, Any],
    ):
        response = await original_consume_code(
            pre_auth_session_id,
            user_input_code,
            device_id,
            link_code,
            session,
            should_try_linking_with_session_user,
            tenant_id,
            user_context,
        )

        if isinstance(response, ConsumeCodeOkResult) and session is not None:
            await add_to_required_secondary_factors_for_user(
                session.get_user_id(), FactorIds.OTP_EMAIL
            )

        return response

    original_implementation.consume_code = consume_code
    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=passwordless.ContactEmailOnlyConfig(),
            flow_type="USER_INPUT_CODE",
            override=passwordless.InputOverrideConfig(functions=override_functions),
        ),
        accountlinking.init(
            should_do_automatic_account_linking=should_do_automatic_account_linking
        ),
        multifactorauth.init(
            first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
        ),
    ],
)
  • Initialize the multi-factor auth recipe here without any override to getMFARequirementsForAuth. The default implementation of this function already checks what factors a user has enabled and returns those. All that is needed is to mark otp-email as enabled for a user as soon as they have completed the OTP challenge successfully. This happens in the consumeCodePOST API override as shown above. Once the code is consumed successfully, mark the otp-email factor as enabled for the user, and the next time they login, they will be asked to complete the OTP challenge.
  • Notice that before calling addToRequiredSecondaryFactorsForUser, check if there is an input session or not. Only call addToRequiredSecondaryFactorsForUser function if there is a session which indicates that the user has finished some first factor already.

In both of the examples above, notice that the Passwordless recipe initializes in the recipeList. In this example, only email-based OTP is enabled, set the contactMethod to EMAIL and flowType to USER_INPUT_CODE (that is, OTP). If instead, you want to use phone SMS-based OTP, set the contact method to PHONE. If you want to give users both options, or for some users use email, and for others use phone, set contactMethod to EMAIL_OR_PHONE.

We have also enabled the account linking feature since it’s required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see this section.

shouldRequireVerification: true prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session.

Once the user finishes the first factor (for example, with emailpassword), their session access token payload looks like this (for those that require OTP):

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

The v being false indicates that there are still factors that are pending. After the user has finished otp-email, the payload looks like:

{
  "st-mfa": {
    "c": {
      "emailpassword": 1702877939,
      "otp-email": 1702877999
    },
    "v": true
  }
}

This indicates that the user has finished all required factors and should be allowed to access the app.

Frontend setup

This consists of two parts:

  • Configuring the frontend to show the OTP challenge UI when required during login / sign up
  • Allowing users to enable / disable OTP challenge on their account via the settings page (If you are following Example 2 from above).

The first part is identical to the steps mentioned in this section, please follow that.

The second part, which is only applicable in case you want to allow users to enable / disable OTP themselves, can be done by creating the following flow on your frontend:

  • When the user navigates to their settings page, you can show them if OTP challenge is active or not.
  • If enabled, you can allow them to disable it, or vice versa.

To know if the user has enabled OTP, you can create an API on your backend that calls the following function:

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

async function isOTPEmailEnabledForUser(userId: string) {
  let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId);
  return factors.includes(MultiFactorAuth.FactorIds.OTP_EMAIL);
}
from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user
from supertokens_python.recipe.multifactorauth.types import FactorIds

async def is_otp_email_factor_enabled_for_user(user_id: str) -> bool:
    factors = await get_required_secondary_factors_for_user(user_id, {})
    return FactorIds.OTP_EMAIL in factors

If the user wants to enable or disable otp-email, you can create an API on your backend that calls the following function:

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

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

async function disableMFAForUser(userId: string) {
  await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL);
}
from supertokens_python.recipe.multifactorauth.asyncio import (
    add_to_required_secondary_factors_for_user,
    remove_from_required_secondary_factors_for_user,
)
from supertokens_python.recipe.multifactorauth.types import FactorIds

async def enable_mfa_for_user(user_id: str) -> None:
    await add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL)


async def disable_mfa_for_user(user_id: str) -> None:
    await remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL)

Multi tenant setup

Backend setup

A user can be a part of multiple tenants. If you want OTP to be active for a specific user across all the tenants that they are a part of, the steps are the same as in the Backend setup section above.

However, if you want OTP to be active for a specific user, for a specific tenant (or a subset of tenants that the user is a part of), then additional logic must be added to the getMFARequirementsForAuth function override. Modifying the example code from the Backend setup section above:

Example 1: Only enable OTP for users that have an admin role

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 MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
import UserRoles from "supertokens-node/recipe/userroles";
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: [
    Session.init(),
    UserRoles.init(),
    ThirdParty.init({
      //...
    }),
    EmailPassword.init({
      //...
    }),
    Passwordless.init({
      contactMethod: "EMAIL",
      flowType: "USER_INPUT_CODE",
    }),
    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,
        };
      },
    }),
    MultiFactorAuth.init({
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            getMFARequirementsForAuth: async function (input) {
              let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id);
              if (
                roles.roles.includes("admin") &&
                (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)
              ) {
                // we only want otp-email for admins
                return [MultiFactorAuth.FactorIds.OTP_EMAIL];
              } else {
                // no MFA for non-admin users.
                return [];
              }
            },
          };
        },
      },
    }),
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import (
    accountlinking,
    emailpassword,
    multifactorauth,
    passwordless,
    session,
    thirdparty,
    userroles,
)
from supertokens_python.recipe.multifactorauth.types import (
    FactorIds,
    OverrideConfig,
    MFARequirementList,
)
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.accountlinking.types import (
    AccountInfoWithRecipeIdAndUserId,
    ShouldNotAutomaticallyLink,
    ShouldAutomaticallyLink,
)
from supertokens_python.types import User
from typing import Dict, Any, Callable, Awaitable, List, Optional, Union
from supertokens_python.recipe.userroles.asyncio import get_roles_for_user


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
        # new_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()


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:
        # Get roles for the user
        roles = await get_roles_for_user(tenant_id, (await user()).id)

        if (
            "admin" in roles.roles
            and FactorIds.OTP_EMAIL in await required_secondary_factors_for_tenant()
        ):
            # We only want OTP_EMAIL for admins
            return [FactorIds.OTP_EMAIL]
        else:
            # No MFA for non-admin users
            return []

    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(),
        userroles.init(),
        emailpassword.init(),
        thirdparty.init(),
        passwordless.init(
            contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"
        ),
        accountlinking.init(
            should_do_automatic_account_linking=should_do_automatic_account_linking
        ),
        multifactorauth.init(
            first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
            override=OverrideConfig(functions=override_functions),
        ),
    ],
)
  • This override requires otp-email only when the user has the admin role and the tenant’s requiredSecondaryFactors includes otp-email.

Example 2: Ask for OTP only for users that have enabled OTP on their account

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 MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
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: [
    Session.init(),
    ThirdParty.init({
      //...
    }),
    EmailPassword.init({
      //...
    }),
    Passwordless.init({
      contactMethod: "EMAIL",
      flowType: "USER_INPUT_CODE",
      override: {
        apis: (oI) => {
          return {
            ...oI,
            consumeCodePOST: async function (input) {
              let response = await oI.consumeCodePOST!(input);
              if (response.status === "OK" && input.session !== undefined) {
                // We do this only if a session exists, which means that it's not being called for first factor login.

                // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata.
                // The multifactorauth recipe will pick this value up next time the user is trying to login, and
                // ask them to enter the OTP code.
                await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(
                  input.session.getUserId(),
                  MultiFactorAuth.FactorIds.OTP_EMAIL,
                );
              }
              return response;
            },
          };
        },
      },
    }),
    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,
        };
      },
    }),
    MultiFactorAuth.init({
      firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            getMFARequirementsForAuth: async function (input) {
              if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) {
                if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) {
                  return [MultiFactorAuth.FactorIds.OTP_EMAIL];
                }
              }
              // no otp-email required for input.user, with the input.tenant.
              return [];
            },
          };
        },
      },
    }),
  ],
});
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import (
    accountlinking,
    emailpassword,
    multifactorauth,
    passwordless,
    session,
    thirdparty,
)
from supertokens_python.recipe.multifactorauth.types import (
    FactorIds,
    OverrideConfig,
    MFARequirementList,
)
from supertokens_python.recipe.multifactorauth.asyncio import (
    add_to_required_secondary_factors_for_user,
)
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig
from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.accountlinking.types import (
    AccountInfoWithRecipeIdAndUserId,
    ShouldNotAutomaticallyLink,
    ShouldAutomaticallyLink,
)
from supertokens_python.types import User
from typing import Dict, Any, Callable, Awaitable, List, Optional, Union
from supertokens_python.recipe.passwordless.interfaces import (
    APIInterface,
    APIOptions,
    ConsumeCodePostOkResult,
)


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
        # new_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()


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:
        if FactorIds.OTP_EMAIL in await required_secondary_factors_for_user():
            if FactorIds.OTP_EMAIL in await required_secondary_factors_for_tenant():
                return [FactorIds.OTP_EMAIL]
        # no otp-email required for input.user, with the input.tenant.
        return []

    original_implementation.get_mfa_requirements_for_auth = (
        get_mfa_requirements_for_auth
    )
    return original_implementation


def passwordless_override(original_implementation: APIInterface):

    original_consume_code_post = original_implementation.consume_code_post

    async def consume_code_post(
        pre_auth_session_id: str,
        user_input_code: Union[str, None],
        device_id: Union[str, None],
        link_code: Union[str, None],
        session: Optional[SessionContainer],
        should_try_linking_with_session_user: Union[bool, None],
        tenant_id: str,
        api_options: APIOptions,
        user_context: Dict[str, Any],
    ):
        response = await original_consume_code_post(
            pre_auth_session_id,
            user_input_code,
            device_id,
            link_code,
            session,
            should_try_linking_with_session_user,
            tenant_id,
            api_options,
            user_context,
        )

        if isinstance(response, ConsumeCodePostOkResult) and session is not None:
            await add_to_required_secondary_factors_for_user(
                session.get_user_id(), FactorIds.OTP_EMAIL
            )
        return response

    original_implementation.consume_code_post = consume_code_post

    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",
            override=passwordless.InputOverrideConfig(apis=passwordless_override),
        ),
        accountlinking.init(
            should_do_automatic_account_linking=should_do_automatic_account_linking
        ),
        multifactorauth.init(
            first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
            override=OverrideConfig(functions=override_functions),
        ),
    ],
)

The getMFARequirementsForAuth override requires otp-email only when it is enabled for the user and the tenant’s requiredSecondaryFactors includes otp-email. This lets the same user require OTP in selected tenants only.

Frontend setup

The frontend setup is identical to the frontend setup section above.

Protecting frontend and backend routes

See the section on protecting frontend and backend routes.

Email / SMS sending and design

By default, the email template used for otp-email login is as shown here, and the default SMS template is as shown here. The method for sending them is via an email and SMS sending service that is available.

If you would like to learn more about this, change the content of the email, or change the method by which messages are sent, check out the email / SMS delivery section in the recipe docs:

API reference

API schema and response details