OTP required for all users
This page shows how to implement an MFA policy that requires all users to complete an OTP challenge before accessing your application. The OTP can be sent via email or phone.
Assume that the first factor is email password or social login, but the same set of steps applies to other first factor types as well.
Single tenant setup
Backend setup
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 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"
}),
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: false
}
}
return {
shouldAutomaticallyLink: false
};
}
}),
MultiFactorAuth.init({
firstFactors: [
MultiFactorAuth.FactorIds.EMAILPASSWORD,
MultiFactorAuth.FactorIds.THIRDPARTY
],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
return [MultiFactorAuth.FactorIds.OTP_EMAIL]
}
}
}
}
})
]
})
-
Notice that the Passwordless recipe initializes in the
recipeList
. In this example, only email-based OTP is enabled, withcontactMethod
set toEMAIL
andflowType
toUSER_INPUT_CODE
(that is,otp
). If you want to use phone SMS-based OTP, set the contact method toPHONE
. If you want to give users both options, or for some users use email, and for others use phone, setcontactMethod
toEMAIL_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.
-
Notice that
shouldRequireVerification: false
configures account linking. It means that the second factor can link to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then you can set this boolean totrue
, and also init the email verification recipe on the frontend and backend inREQUIRED
mode. -
The
getMFARequirementsForAuth
function is overridden to indicate thatotp-email
must be completed before the user can access the app. Notice thatuserId
is not checked there, andotp-email
is returned for all users. You can also returnotp-phone
instead if you want users to complete the OTP challenge via a phone SMS. Finally, if you want to give users an option for email or phone, you can return the following array from the function:[{
"oneOf": ["otp-email", "otp-phone"]
}]
Once the user finishes the first factor (for example, with emailpassword
), their session access token payload looks like this:
{
"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
}
}
Indicating that the user has finished all required factors, and should be allowed to access the app.
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the contactMethod
and flowType
are set correctly.
Frontend setup
What type of UI are you using?
We start by modifying the init
function call on the frontend like this:
import supertokens from "supertokens-auth-react"
import ThirdParty from "supertokens-auth-react/recipe/thirdparty"
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: [
ThirdParty.init(/* ... */),
EmailPassword.init( /* ... */),
Passwordless.init({
contactMethod: "EMAIL"
}),
MultiFactorAuth.init({
firstFactors: [
MultiFactorAuth.FactorIds.EMAILPASSWORD,
MultiFactorAuth.FactorIds.THIRDPARTY
]
})
]
})
- Like on the backend, the
passwordless
recipe initializes in therecipeList
. ThecontactMethod
needs to be consistent with the backend setting. - The
MultiFactorAuth
recipe is also initialized, and the first factors to use are included. In this case, that would beemailpassword
andthirdparty
- same as the backend.
Next, add the Passwordless pre-built UI when rendering the SuperTokens component:
Do you use react-router-dom?
With the above configuration, users see emailpassword
or social login UI when they visit the auth page. After completing that, users redirect to /auth/mfa/otp-email
(assuming that the websiteBasePath
is /auth
) where they are asked to complete the OTP challenge. The UI for this screen looks like:
- Factor Setup UI (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login).
- Verification UI.
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the contactMethod
is set correctly.
Multi tenant setup
In a multi-tenancy setup, you may want to enable email / phone OTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the single tenant setup section above, so in this section, we will focus on enabling OTP for all users within specific tenants.
Backend setup
To start, initialize the Passwordless and the MultiFactorAuth recipes 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 { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types";
import { SessionContainerInterface } from "supertokens-node/recipe/session/types";
import AccountLinking from "supertokens-node/recipe/accountlinking";
supertokens.init({
supertokens: {
connectionURI: "..."
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "..."
},
recipeList: [
Session.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: false
}
}
return {
shouldAutomaticallyLink: false
};
}
}),
MultiFactorAuth.init()
]
})
Unlike the single tenant setup, no configuration is provided to the MultiFactorAuth
recipe because all the necessary configuration is done on a tenant level.
To configure otp-email requirement for a tenant, the following API can be called:
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,
MultiFactorAuth.FactorIds.THIRDPARTY
],
requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL]
});
if (resp.createdNew) {
// Tenant created successfully
} else {
// Existing tenant's config was modified.
}
}
- In the above, the
firstFactors
are set to["emailpassword", "thirdparty"]
to indicate that the first factor can be eitheremailpassword
orthirdparty
. - The
requiredSecondaryFactors
is set to["otp-email"]
to indicate that OTP email is required for all users in this tenant. The default implementation ofgetMFARequirementsForAuth
in theMultiFactorAuth
takes this into account.
Once the user finishes the first factor (for example, with emailpassword
), their session access token payload looks like this:
{
"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 challenge, the payload looks like:
{
"st-mfa": {
"c": {
"emailpassword": 1702877939,
"otp-email": 1702877999
},
"v": true
}
}
Indicating that the user has finished all required factors, and should be allowed to access the app.
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the contactMethod
and flowType
are set correctly.
Frontend setup
We start by modifying the init
function call on the frontend like this:
import supertokens from "supertokens-auth-react"
import ThirdParty from "supertokens-auth-react/recipe/thirdparty"
import EmailPassword from "supertokens-auth-react/recipe/emailpassword"
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"
import Passwordless from "supertokens-auth-react/recipe/passwordless"
import Multitenancy from "supertokens-auth-react/recipe/multitenancy"
supertokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
ThirdParty.init({
//...
}),
EmailPassword.init({
//...
}),
Passwordless.init({
contactMethod: "EMAIL"
}),
MultiFactorAuth.init(),
Multitenancy.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getTenantId: async (context) => {
return "TODO"
}
}
}
}
})
]
})
- Like on the backend, the
Passwordless
recipe initializes in therecipeList
. Make sure that the configuration for it is consistent with what's on the backend. - The
MultiFactorAuth
recipe is also initialized. Notice that unlike the single tenant setup, thefirstFactors
are not specified here. That information is fetched based on thetenantId
you provide the SDK with. usesDynamicLoginMethods: true
is set so that the SDK knows to fetch the login methods dynamically based on thetenantId
.- Finally, the multi-tenancy recipe initializes and a method for getting the
tenantId
is provided.
Next, add the Passwordless pre-built UI when rendering the SuperTokens component:
Do you use react-router-dom?
With the above configuration, users see the first and second factor based on the tenant configuration. For the tenant configured above, users see email password or social login first. After completing that, users redirect to /auth/mfa/otp-email
(assuming that the websiteBasePath
is /auth
) where they are asked to complete the OTP challenge. The UI for this screen looks like:
- Factor Setup UI (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login).
- Verification UI.
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the contactMethod
is set correctly.
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 provided.
If you would like to learn more about this, or change the content of the email, or the method by which they are sent, checkout the email / SMS delivery section in the recipe docs: