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.
Backend setup
#
Step 1: Enable account linkingMFA requires account linking to be enabled (see here to understand why). You can enable it in the following way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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: false
}
}
return {
shouldAutomaticallyLink: false
};
}
}),
]
})
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.
- The above snippet enables auto account linking only during the second factor and not for the first factor login. This means that if a user has an email password account, and then they login via Google separately (with the same email), those two accounts will not be linked. However, if the second factor for logging in is email or phone OTP, then that passwordless account will be linked to the first factor login method of that session.
- 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. - If you also want to enable first factor automatic account linking, see this link.
important
Account linking is a paid feature, and you need to generate a license key to enable it. Enabling the MFA feature also enables account linking automatically, so you don't need to check the account linking feature separately.
#
Step 2: Configuring the first factors#
Single tenant setupWe start by intialising the MFA recipe on the backend and specifying the list of first factors using their factor IDs. You still have to initialise all the auth recipes in the recipeList
, and configure them based on your needs.
For example, the code below inits thirdpartyemailpassword
and passwordless
recipes and sets the firstFactor
array to be ["emailpassword", "thirdparty"]
. This means that we will show email password and social login to the user as the first factor (using the thirdpartyemailpassword
recipe), and use passwordless
for the second factor.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import supertokens from "supertokens-node";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"
import Passwordless from "supertokens-node/recipe/passwordless"
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"
supertokens.init({
supertokens: {
connectionURI: "..."
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "..."
},
recipeList: [
// ...
ThirdPartyEmailPassword.init({
//...
}),
Passwordless.init({
contactMethod: "EMAIL",
flowType: "USER_INPUT_CODE"
}),
MultiFactorAuth.init({
firstFactors: [
MultiFactorAuth.FactorIds.EMAILPASSWORD,
MultiFactorAuth.FactorIds.THIRDPARTY
]
})
]
})
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.
There are of course other combinations of first factors that you may want to add. For example, if you want passwordless as the first factor, then you would init the passwordless recipe and add "passwordless"
in the firstFactors
array.
#
Multi tenant setupFor 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
and configure the firstFactors on a per tenant basis when you are creating / updating a tenant as shown below:
- 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", {
emailPasswordEnabled: true,
passwordlessEnabled: true,
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD]
});
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.
curl --location --request PUT '/appid-<APP_ID>/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"emailPasswordEnabled": true,
"passwordlessEnabled": true,
"firstFactors": ["emailpassword"]
}'
In the above, we are enabling email password and passwordless for the tenant, however, we have set firstFactors
only to include "emailpassword"
. This means that users who login to this tenant will only be able to use email password as the first factor, even though passwordless is enabled. Later on, we will see how we can configure passwordless as a second factor for this tenant.
important
- If you do not configure a
firstFactors
array on a tenant config, then it will pick up the values from thefirstFactors
array in theMultiFactorAuth.init
from the backend'sinit
config. - To remove the
firstFactors
configuation for a tenant, you can simply pass anull
value for thefirstFactors
key in the tenant config. For that tenant, this will make SuperTokens default to thefirstFactors
array in theMultiFactorAuth.init
from the backend'sinit
config.
#
Step 3: Configuring a second factorIn this section, we will see how to configure SuperTokens so that a second factor is required for all users during sign up and during sign in. We will use TOTP as an example for the second factor.
#
Single tenant setupThis can be achieved easily by the following code snippet:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens, { User, RecipeUserId, } from "supertokens-node";
import { UserContext } from "supertokens-node/types";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"
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: [
// ...
ThirdPartyEmailPassword.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]
}
}
}
}
})
]
})
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.
In the above snippet, we have configured 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 is used to determine 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
will be true
, else false
.
In the above example, we are simply returning "totp"
as a required factor for all users, but 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. In fact, the default implementation of getMFARequirementsForAuth
returns the set of factors that are 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 just a string[]
. You can also return an object which tells SuperTokens that any one of the factors need to be completed:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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
]
}]
}
}
}
}
})
]
})
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.
Or that all of the factors in the returned array need to be completed:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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
]
}]
}
}
}
}
})
]
})
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.
In the above, the user will have to complete both the factors in any order.
You can enforce order of factors in the auth flow by returning different values from the function based on what's already completed. For example, if you want to have email password / social login as the first factor followed by TOTP and then email otp-email (in that order), you could do the following:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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) {
// 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]
}
}
}
}
}
})
]
})
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.
You can return an empty array from getMFARequirementsForAuth
if you don't want any further MFA done for the current user.
If you return more than one item from the array, it would mean that the user has to satisfy the criteria for each of the items. For example, if we returnt the following from getMFARequirementsForAuth
:
let requirements = [{
oneOf: ["f1", "f2"]
}, {
allOfInAnyOrder: ["f3", "f4"]
}, "f5"]
Then the user will have to complete (f1
or f2
) and f3
, f4
, f5
to complete login, in that order. Note that f3
and f4
can be done in any order, but they both have to be done before doing f5
.
All of the above is only useful to populate the values of the v
and c
values in the session's access token payload. However, you still need to protect the frontend and API routes to ensure that those resources are given access to only if the user has the v
boolean set to true
(indicating that all factors have been completed).
#
Multi tenant setupFor a multi tenant setup, you can configure a list of secondary factors when creating / modifying a tenant as shown below:
- 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", {
emailPasswordEnabled: true,
passwordlessEnabled: true,
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD],
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.
curl --location --request PUT '/appid-<APP_ID>/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"emailPasswordEnabled": true,
"passwordlessEnabled": true,
"firstFactors": ["emailpassword"],
"requiredSecondaryFactors": ["otp-email"]
}'
In the above code, we add a propery called requiredSecondaryFactors
for a tenant whose value is a string[]
. We 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. This factor comes from the passwordless recipe, and so we have also set passwordlessEnabled: true
.
In order to remove the requiredSecondaryFactors
configuration for a tenant, you can simply pass a null
value for the requiredSecondaryFactors
key in the tenant config.
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 behaviour for the tenant, you can achieve that by overriding the getMFARequirementsForAuth
function as shown below:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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
}]
}
}
}
}
})
]
})
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 the input to the function contains the requiredSecondaryFactorsForTenant
array. This would be the same list that you passed to the tenant config when creating / modifying the tenant as shown in the previous steps.
#
Functions to help with configuring a second factor for each user optionallyInstead 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 is usually achieved by allowing users to configure their MFA preferences in the settings page in your app's frontend. We don't yet provide a pre built UI for this, but in this section, we will talk about how to setup this up on the backend.
You want to start by creating an API that does session verification, 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:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
async function enableMFAForUser(userId: string) {
await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP)
}
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.
The effect of the above function call is that in the default implementation of getMFARequirementsForAuth
, we take into account the factors that are specifically enabled for the input user. By default, if you add several 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 behaviour from "any one of" to something else (like "all of"), you can do so by overriding the getMFARequirementsForAuth
function:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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
}]
}
}
}
}
})
]
})
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.
Once you call the addToRequiredSecondaryFactorsForUser
function for a user, SuperTokens will save this preference in the usermetadata JSON of the user. For example, if you add "totp"
as a requred secondary factor for a user, this will be saved in the metadata JSON as:
{
"_supertokens": {
"requiredSecondaryFactors": ["totp"]
}
}
You can view this JSON on the user details page of the user management dashboard and modify it manually if you like.
In order to know the factors that have been enabled for a user, you can use the following function:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
async function isTotpEnabledForUser(userId: string) {
let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId)
return factors.includes(MultiFactorAuth.FactorIds.TOTP)
}
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.
Using the above function, you can build your settings page on the frontend which displays the existing enabled factors for the user, and allow users to enable / 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):
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
async function isTotpSetupForUser(userId: string) {
let factors = await MultiFactorAuth.getFactorsSetupForUser(userId)
return factors.includes(MultiFactorAuth.FactorIds.TOTP)
}
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.
Or you can call the MFAInfo endpoint 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 will the getFactorsSetupForUser
function return totp
as part of the array. Likewise, if the user has completed otp-email
or link-email
once, only then will these factors be a part of the returned array. Let's take two examples:
- The first time the user enables TOTP, then the result of
getFactorsSetupForUser
will not contain"totp"
. So you should redirect the user to the totp setup screen. Once they add and verify a device, thengetFactorsSetupForUser
will return["totp"]
even if they later disable totp from the settings page and renable it. - Let's say that the first factor for a user is
emailpassword
, and the second factor isotp-email
. Once they sign up, SuperTokens already knows the email for the user, so when they are doing theotp-email
step, then they will not be asked to enter their email again (i.e. an OTP will be sent to them directly). However, until they actually complete the OTP flow,getFactorsSetupForUser
will not return["otp-email"]
as part of the output.
caution
In the edge case that the a factor is enabled for a user, but they sign out before setting it up, then when they login next, SuperTokens will still ask them to complete the factor at that time. If SuperTokens doesn't have the required information (like no TOTP device for totp auth), then users will be asked to setup a device at that point in time.
If you would like to change how this works and only want users to setup their factor via the settings page, and not during sign in, you can do so by overriding the getMFARequirementsForAuth
function, which takes as an input the list of factors that are setup for the current user.
The subsequent sections in this doc will walk through frontend setup, and also specific examples of common MFA flows.
#
Effect on post sign up / sign in overridesIt's a very common use case to want to override the default behaviour of SuperTokens after a user signs up or signs in. For example, you may want to changes your database state whenever someone signs up. This is done by overriding the sign up / sign in recipe functions in the backend SDK:
- ThirdPartyEmailPassword recipe
- Passwordless recipe
- EmailPassword recipe
- ThirdPartyPasswordless recipe
- ThirdParty recipe
Now since the sign up / sign in APIs are shared for first factor and second factor login, your override will be called for both first and second factor login. So if you want to have different behaviour for first and second factor login, you can use the input
argument to the function to determine if the user is doing first or second factor login.
The input
argument contains the session
object using which you can determine if the user is doing first or second factor login. If the session
property is undefined
, it means it's a first factor login, else it's a second factor login. In the links above, the code snippets we have check for input.session === undefined
to determine if it's a first factor login.