Paid Feature
This is a paid feature.
For self hosted users, Sign up to get a license key and follow the instructions sent to you by email. Using the dev license key is free. We only start charging you once you enable the feature in production using the provided production license key.
For managed service users, you can click on the "enable paid features" button on our dashboard, and follow the steps from there on. Once enabled, this feature is free on the provided development environment.
OTP required for all users
In this page, we will show you how to implement an MFA policy that requires all users to complete a OTP challenge before they get access to your application. The OTP can be sent via emial or phone.
note
We assume that the first factor is email password or social login, but the same set of steps will be applicable for other first factor types as well.
#
Single tenant setup#
Backend setupTo start with, we configure the backend in the following way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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]
}
}
}
}
})
]
})
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
Notice that we have initialised the Passwordless recipe in the
recipeList
. In this example, we only want to enable email based OTP, so we set thecontactMethod
toEMAIL
andflowType
toUSER_INPUT_CODE
(i.e. otp). If instead, you want to use phone sms based OTP, you should set the contact method toPHONE
. If you want to give users both the options, or for some users use email, and for others use phone, you should 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 we have set
shouldRequireVerification: false
for account linking. It means that the second factor can be linked 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.We also override the
getMFARequirementsForAuth
function to indicate thatotp-email
must be completed before the user can access the app. Notice that we do not check for the userId there, and returnotp-email
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 will look 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 will look 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.
caution
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the contactMethod
and flowType
are set correctly.
#
Frontend setupWe start by modifying the init
function call on the frontend like so:
- ReactJS
- Angular
- Vue
You will have to make changes to the auth route config, as well as to the supertokens-web-js
SDK config at the root of your application:
This change is in your auth route config.
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
(window as any).supertokensUIThirdParty.init(/* ... */),
(window as any).supertokensUIEmailPassword.init( /* ... */),
(window as any).supertokensUIPasswordless.init({
contactMethod: "EMAIL"
}),
(window as any).supertokensUIMultiFactorAuth.init({
firstFactors: [
(window as any).supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD,
(window as any).supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY
]
})
]
})
This change goes in the supertokens-web-js
SDK config at the root of your application:
import SuperTokens from 'supertokens-web-js';
import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth';
import Passwordless from "supertokens-web-js/recipe/passwordless";
SuperTokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
// other recipes...
MultiFactorAuth.init(),
Passwordless.init()
],
});
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
]
})
]
})
You will have to make changes to the auth route config, as well as to the supertokens-web-js
SDK config at the root of your application:
This change is in your auth route config.
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
(window as any).supertokensUIThirdParty.init(/* ... */),
(window as any).supertokensUIEmailPassword.init( /* ... */),
(window as any).supertokensUIPasswordless.init({
contactMethod: "EMAIL"
}),
(window as any).supertokensUIMultiFactorAuth.init({
firstFactors: [
(window as any).supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD,
(window as any).supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY
]
})
]
})
This change goes in the supertokens-web-js
SDK config at the root of your application:
import SuperTokens from 'supertokens-web-js';
import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth';
import Passwordless from "supertokens-web-js/recipe/passwordless";
SuperTokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
// other recipes...
MultiFactorAuth.init(),
Passwordless.init()
],
});
- Just like on the backend, we init the
passwordless
recipe in therecipeList
. ThecontactMethod
needs to be consistent with the backend setting. - We also init the
MultiFactorAuth
recipe, and pass in the first factors that we want to use. In this case, that would beemailpassword
andthirdparty
- same as the backend.
Next, we need to add the Passwordless pre built UI when rendering the SuperTokens component:
- ReactJS
- Angular
- Vue
success
This step is not required for non React apps, since all the pre built UI components are already added into the bundle.
success
This step is not required for non React apps, since all the pre built UI components are already added into the bundle.
With the above configuration, users will see emailpassword or social login UI when they visit the auth page. After completing that, users will be redirected to /auth/mfa/otp-email
(assuming that the websiteBasePath
is /auth
) where they will be 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 our example, the first factor does provide an email since it's email password or social login).
- Verification UI.
caution
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the contactMethod
is set correctly.
#
Multi tenant setupIn 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 setupTo start, we will initialise the Passwordless and the MultiFactorAuth recipes in the following way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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()
]
})
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
Unlike the single tenant setup, we do not provide any config to the MultiFactorAuth
recipe cause all the necessary configuration will be done on a tenant level.
To configure otp-email requirement for a tenant, we can call the following API:
- NodeJS
- GoLang
- Python
- cURL
Important
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.
}
}
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
- Single app setup
- Multi app setup
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"firstFactors": ["emailpassword", "thirdparty"],
"requiredSecondaryFactors": ["otp-email"]
}'
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"firstFactors": ["emailpassword", "thirdparty"],
"requiredSecondaryFactors": ["otp-email"]
}'
- In the above, we set the
firstFactors
to["emailpassword", "thirdparty"]
to indicate that the first factor can be either emailpassword or thirdparty. We also configure thatemailPasswordEnabled
andthirdPartyEnabled
are enabled for the tenant. - We set the
requiredSecondaryFactors
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. We also setpasswordlessEnabled
totrue
since theotp-email
factor is provided by thePasswordless
recipe.
Once the user finishes the first factor (for example, with emailpassword), their session access token payload will look 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 will look 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.
caution
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the contactMethod
and flowType
are set correctly.
#
Frontend setupWe start by modifying the init
function call on the frontend like so:
- ReactJS
- Angular
- Vue
You will have to make changes to the auth route config, as well as to the supertokens-web-js
SDK config at the root of your application:
This change is in your auth route config.
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
(window as any).supertokensUIThirdParty.init({
//...
}),
(window as any).supertokensUIEmailPassword.init({
//...
}),
(window as any).supertokensUIPasswordless.init({
contactMethod: "EMAIL"
}),
(window as any).supertokensUIMultiFactorAuth.init(),
(window as any).supertokensUIMultitenancy.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getTenantId: async (context) => {
return "TODO"
}
}
}
}
})
]
})
This change goes in the supertokens-web-js
SDK config at the root of your application:
supertokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
Session.init(),
MultiFactorAuth.init(),
],
});
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"
}
}
}
}
})
]
})
You will have to make changes to the auth route config, as well as to the supertokens-web-js
SDK config at the root of your application:
This change is in your auth route config.
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
(window as any).supertokensUIThirdParty.init({
//...
}),
(window as any).supertokensUIEmailPassword.init({
//...
}),
(window as any).supertokensUIPasswordless.init({
contactMethod: "EMAIL"
}),
(window as any).supertokensUIMultiFactorAuth.init(),
(window as any).supertokensUIMultitenancy.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getTenantId: async (context) => {
return "TODO"
}
}
}
}
})
]
})
This change goes in the supertokens-web-js
SDK config at the root of your application:
supertokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
Session.init(),
MultiFactorAuth.init(),
],
});
- Just like on the backend, we init the
Passwordless
recipe in therecipeList
. Make sure that the config for it is consistent with whats on the backend. - We also init the
MultiFactorAuth
recipe. Notice that unlike the single tenant setup, we do not specify thefirstFactors
here. That information is fetched based on the tenantId you provide the SDK with. - We have set
usesDynamicLoginMethods: true
so that the SDK knows to fetch the login methods dynamically based on the tenantId. - Finally, we init the multi tenancy recipe and provide a method for getting the tenantId.
Next, we need to add the Passwordless pre built UI when rendering the SuperTokens component:
- ReactJS
- Angular
- Vue
success
This step is not required for non React apps, since all the pre built UI components are already added into the bundle.
success
This step is not required for non React apps, since all the pre built UI components are already added into the bundle.
With the above configuration, users will see the first and second factor based on the tenant configuration. For the tenant we configured above, users will see email password or social login first. After completing that, users will be redirected to /auth/mfa/otp-email
(assuming that the websiteBasePath
is /auth
) where they will be asked 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 our example, the first factor does provide an email since it's email password or social login).
- Verification UI.
caution
If you are already using Passwordless
or ThirdPartyPasswordless
in your app as a first factor, you do not need to explicitly initialise the Passwordless recipe again. You just have to make sure that the contactMethod
is set correctly.
#
Protecting frontend and backend routesSee the section on protecting frontend and backend routes.
#
Email / SMS sending and designBy 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 we provide.
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: