Require TOTP for all users
Implement a TOTP-based MFA policy for all users to enhance application security.
Overview
This guide shows you how to implement an MFA policy that requires all users to use TOTP before they get access to your application.
Before you start
The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types.
Steps
1. Configure the backend
To start with, we configure the backend in the following way:
import supertokens from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import totp from "supertokens-node/recipe/totp";
import Session from "supertokens-node/recipe/session";
supertokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
Session.init(),
ThirdParty.init({
//...
}),
EmailPassword.init({
//...
}),
totp.init(),
MultiFactorAuth.init({
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
return [MultiFactorAuth.FactorIds.TOTP];
},
};
},
},
}),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import multifactorauth, totp
from supertokens_python.recipe.multifactorauth.types import (
FactorIds,
OverrideConfig,
MFARequirementList,
)
from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface
from supertokens_python.types import User
from typing import Dict, Any, Callable, Awaitable, List
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
return [FactorIds.TOTP]
original_implementation.get_mfa_requirements_for_auth = (
get_mfa_requirements_for_auth
)
return original_implementation
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="...",
recipe_list=[
totp.init(),
multifactorauth.init(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
override=OverrideConfig(functions=override_functions),
),
],
)- Notice that we have initialised the TOTP recipe in the
recipeList. By default, no configs are required for it, but you can provide:issuer: This is the name that will show up in the TOTP app for the user. By default, this is equal to theappNameconfig, however, you can change it to something else using this property.defaultSkew: The default value of this is1, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course).defaultPeriod: The default value of this is30, which means that the current tick is value for 30 seconds. So by default, a TOTP code that’s just shown to the user, is valid for 60 seconds (defaultPeriod + defaultSkew*defaultPeriodseconds)
- We also override the
getMFARequirementsForAuthfunction to indicate thattotpmust be completed before the user can access the app. Notice that we do not check for the userId there, and returntotpfor all users.
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 totp, the payload will look like:
{
"st-mfa": {
"c": {
"emailpassword": 1702877939,
"totp": 1702877999
},
"v": true
}
}Indicating that the user has finished all required factors, and should be allowed to access the app.
2. Configure the frontend
We start by modifying the init function call on the frontend like so:
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.
import supertokens from "supertokens-auth-react";
import Passwordless from "supertokens-auth-react/recipe/passwordless";
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
import totp from "supertokens-auth-react/recipe/totp";
supertokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
// other recipes..
totp.init(),
MultiFactorAuth.init({
firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
}),
],
});// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded)
supertokensUIInit({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
// other recipes..
supertokensUITOTP.init(),
supertokensUIMultiFactorAuth.init({
firstFactors: [
supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD,
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 Totp from "supertokens-web-js/recipe/totp";
SuperTokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
// other recipes...
MultiFactorAuth.init(),
Totp.init(),
],
});- Just like on the backend, we init the
totprecipe in therecipeList. - We also init the
MultiFactorAuthrecipe, and pass in the first factors that we want to use. In this case, that would beemailpasswordandthirdparty- same as the backend.
Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component:
import { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui";
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom";
function App() {
return (
<SuperTokensWrapper>
<div className="App">
<Router>
<div className="fill">
<Routes>
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [
/* ... */ TOTPPreBuiltUI,
MultiFactorAuthPreBuiltUI,
])}
// ... other routes
</Routes>
</div>
</Router>
</div>
</SuperTokensWrapper>
);
}import { SuperTokensWrapper } from "supertokens-auth-react";
import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui";
import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui";
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
function App() {
if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) {
return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]);
}
return <SuperTokensWrapper>{/*Your app*/}</SuperTokensWrapper>;
}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/totp (assuming that the websiteBasePath is /auth) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like:
- Factor Setup UI
- Verification UI (In case the factor is already setup before).
We start by initialising the MFA and TOTP recipe on the frontend like so:
import SuperTokens from "supertokens-web-js";
import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth";
import Totp from "supertokens-web-js/recipe/totp";
SuperTokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
// other recipes...
MultiFactorAuth.init(),
Totp.init(),
],
});supertokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [
// other recipes...
supertokensMultiFactorAuth.init(),
supertokensTotp.init(),
],
});After the first factor login, you should start by checking the access token payload and see if the MFA claim’s v boolean is false. If it’s not, then we can redirect the user to the application page.
If it’s false, the frontend then needs to call the MFA endpoint to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the next array will contain ["totp"].
Two possibilities exist here:
- Case 1: The user needs to setup a TOTP device cause they don’t have any.
- Case 2: The user already has a verified device setup and needs to complete the TOTP challenge.
We can know which case it is by checking if "totp" is one of the items in the factorsThatAreAlreadySetup array that is returned from the API call above. If it is in the array, then it’s case 2, otherwise it’s case 1.
Case 1 implementation: User needs to setup a new TOTP device
In this case, we do two things:
- Call an API on the backend to create a device. This returns the device secret that can be displayed to the user. The user is supposed to scan this using their authenticator app, to add a new entry for your app in their authenticator app.
- Then the user needs to enter the TOTP code that’s displayed to them in the app, and this needs to be sent to the backend to mark the device as verified. Once a device is marked as verified, only then will the
factorsThatAreAlreadySetuparray contain"totp"the next time they login.
To create a new device, call the following API:
The above API call returns the following response:
import Totp from "supertokens-web-js/recipe/totp";
import Session from "supertokens-web-js/recipe/session";
async function createNewTotpDevice() {
if (await Session.doesSessionExist()) {
try {
let deviceResponse = await Totp.createDevice();
if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") {
// this should only come here if you are passing a custom device name when calling the above function.
throw new Error("Should never come here");
// device created successfully
}
// device created successfully
let qrCodeString = deviceResponse.qrCodeString;
let secret = deviceResponse.secret;
// TODO: display a QR code based on qrCodeString, and also an option to view
// the secret if the user is unable to scan the QR code.
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
} else {
throw new Error(
"TOTP device creation can only happen after the first factor is complete and when a session exists",
);
}
}async function createNewTotpDevice() {
if (await supertokensSession.doesSessionExist()) {
try {
let deviceResponse = await supertokensTotp.createDevice();
if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") {
// this should only come here if you are passing a custom device name when calling the above function.
throw new Error("Should never come here");
// device created successfully
}
// device created successfully
let qrCodeString = deviceResponse.qrCodeString;
let secret = deviceResponse.secret;
// TODO: display a QR code based on qrCodeString, and also an option to view
// the secret if the user is unable to scan the QR code.
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
} else {
throw new Error(
"TOTP device creation can only happen after the first factor is complete and when a session exists",
);
}
}{
"status": "OK",
"issuerName": "...",
"deviceName": "TOTP Device 1",
"secret": "....",
"userIdentifier": "user@example.com",
"qrCodeString": "..."
} | {
"status": "DEVICE_ALREADY_EXISTS_ERROR" | "GENERAL_ERROR"
}- When device registration is successful, the API returns:
- The
secretandqrCodeStringwhich are to be displayed to the user. For React apps, we recommend using the react-qr-code library to display the QR code. - The
issuerNameis the name will show up on the TOTP app for the user. By default, this is equal to theappNameconfig on the backend SDK, however, you can change it to something else in the backendtotp.initconfig. - The
userIdentifieris the email / phone number of the user based on the first factor. This will also be shown in the TOTP app along with theissuerName.
- The
- The API call can also take a
deviceName(as a POST body prop) which attempts to create a TOTP device with the provided name. A status of"DEVICE_ALREADY_EXISTS_ERROR"is returned in case a verified device with the input name already exists. In this case, you should ask the user to enter a different name. Note that this status is only returned in case you are passing in a custom device name. The default naming strategy is to name the device “TOTP Device N”, where we start N from 1, and keep increasing it. This value can be used to identify a device from the backend point of view, for operations like deleting a device. - A status of
"GENERAL_ERROR"is returned in case you specifically return that from a backend API override.
Once a device has been created, and scanned, you need to ask the user to enter the TOTP and call the API below to verify it:
The above API call returns the following response:
import Totp from "supertokens-web-js/recipe/totp";
import Session from "supertokens-web-js/recipe/session";
async function verifyTotpDevice(deviceName: string, userInputTotp: string) {
if (await Session.doesSessionExist()) {
try {
let verifyResponse = await Totp.verifyDevice({
deviceName,
totp: userInputTotp,
});
if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") {
// this can happen due to a race condition wherein the device is deleted before verifying.
window.alert("Something went wrong. Please reload and try again");
} else if (verifyResponse.status === "LIMIT_REACHED_ERROR") {
// this can happen if the user has entered a wrong TOTP too many times.
window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds");
} else if (verifyResponse.status === "INVALID_TOTP_ERROR") {
window.alert("Totp incorrect. Please try again");
} else {
// Device verified successfully
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
} else {
throw new Error(
"TOTP device verification can only happen after the first factor is complete and when a session exists",
);
}
}async function verifyTotpDevice(deviceName: string, userInputTotp: string) {
if (await supertokensSession.doesSessionExist()) {
try {
let verifyResponse = await supertokensTotp.verifyDevice({
deviceName,
totp: userInputTotp,
});
if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") {
// this can happen due to a race condition wherein the device is deleted before verifying.
window.alert("Something went wrong. Please reload and try again");
} else if (verifyResponse.status === "LIMIT_REACHED_ERROR") {
// this can happen if the user has entered a wrong TOTP too many times.
window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds");
} else if (verifyResponse.status === "INVALID_TOTP_ERROR") {
window.alert("Totp incorrect. Please try again");
} else {
// Device verified successfully
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
} else {
throw new Error(
"TOTP device verification can only happen after the first factor is complete and when a session exists",
);
}
}{
"status": "OK",
"wasAlreadyVerified": false
} | {
"status": "INVALID_TOTP_ERROR",
"currentNumberOfFailedAttempts": 1,
"maxNumberOfFailedAttempts": 5
} | {
"status": "LIMIT_REACHED_ERROR",
"retryAfterMs": 900000
} | {
"status": "UNKNOWN_DEVICE_ERROR" | "GENERAL_ERROR"
}- The
deviceName, which is an input to the API is one of the props returned from the previous API call to create a device. - When verification is successful (
status: "OK"), the device is marked as verified in the database and can be used for the TOTP challenge next time around. The booleanwasAlreadyVerifiedindicates if the device was already verified before this call was made. - A status of
INVALID_TOTP_ERRORmeans that the user has entered an incorrect TOTP and needs to retry. The response contains two other props:currentNumberOfFailedAttempts: The number of times the user has entered an incorrect TOTP so far.maxNumberOfFailedAttempts: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (seestatus: LIMIT_REACHED_ERROR). This is set to 5 by default in the core. You can change this value by setting thetotp_max_attemptsin the core config.
- A status of
LIMIT_REACHED_ERRORindicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise valid TOTPs will fail). The waiting period is indicated by theretryAfterMsprop in the response body. By default, it is 15 minutes, but it can be changed by setting the value fortotp_rate_limit_cooldown_secin the core config. - A status of
UNKNOWN_DEVICE_ERRORis possible due to a race condition in which the device is somehow deleted before the verification call is made. - A status of
GENERAL_ERRORis possible if you specifically return that from a backend API override.
On successful verification of a device, the totp factor is marked as completed and the v value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this v value in the MFA claim and redirect the user to the application page, or get information about the next factor using the MFA info endpoint.
Case 2 implementation: User needs to complete the TOTP challenge
This case is when the user already has a device setup (totp is in factorsThatAreAlreadySetup), and needs to complete the TOTP challenge. In this case, you should show the user an input box asking them to enter their TOTP from the authenticator app and then call the following API:
The above API call returns the following response:
import Totp from "supertokens-web-js/recipe/totp";
import Session from "supertokens-web-js/recipe/session";
async function verifyTotpCode(userInputTotp: string) {
if (await Session.doesSessionExist()) {
try {
let verifyResponse = await Totp.verifyCode({
totp: userInputTotp,
});
if (verifyResponse.status === "LIMIT_REACHED_ERROR") {
// this can happen if the user has entered a wrong TOTP too many times.
window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds");
} else if (verifyResponse.status === "INVALID_TOTP_ERROR") {
window.alert("Totp incorrect. Please try again");
} else {
// Code verified successfully
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
} else {
throw new Error(
"TOTP code verification can only happen after the first factor is complete and when a session exists",
);
}
}async function verifyTotpCode(userInputTotp: string) {
if (await supertokensSession.doesSessionExist()) {
try {
let verifyResponse = await supertokensTotp.verifyCode({
totp: userInputTotp,
});
if (verifyResponse.status === "LIMIT_REACHED_ERROR") {
// this can happen if the user has entered a wrong TOTP too many times.
window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds");
} else if (verifyResponse.status === "INVALID_TOTP_ERROR") {
window.alert("Totp incorrect. Please try again");
} else {
// Code verified successfully
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
} else {
throw new Error(
"TOTP code verification can only happen after the first factor is complete and when a session exists",
);
}
}{
"status": "OK" | "UNKNOWN_USER_ID_ERROR"
} | {
"status": "INVALID_TOTP_ERROR",
"currentNumberOfFailedAttempts": 1,
"maxNumberOfFailedAttempts": 5,
} | {
"status": "LIMIT_REACHED_ERROR",
"retryAfterMs": 900000,
} | {
"status": "GENERAL_ERROR"
}- A
status: OKindicates that verification was successful. SuperTokens tries and verifies the input TOTP against all verified devices that belong to this user. - A status of
INVALID_TOTP_ERRORmeans that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props:currentNumberOfFailedAttempts: The number of times the user has entered an incorrect TOTP so far.maxNumberOfFailedAttempts: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (seestatus: LIMIT_REACHED_ERROR). This is set to 5 by default in the core. You can change this value by setting thetotp_max_attemptsin the core config.
- A status of
LIMIT_REACHED_ERRORindicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even value TOTPs will fail). The waiting period is indicated by theretryAfterMsprop in the response body. By default, it is 15 minutes, but it can be changed by setting the value fortotp_rate_limit_cooldown_secin the core config. - A status of
UNKNOWN_USER_ID_ERRORis possible due to a race condition in which all devices that the user had are deleted by the time this API is called. In this case, you can ask users to setup a new device. - A status of
GENERAL_ERRORis possible if you specifically return that from a backend API override.
On successful verification of the code, the totp factor is marked as completed and the v value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this v value in the MFA claim and redirect the user to the application page, or get information about the next factor using the MFA info endpoint.
In a multi tenancy setup, you may want to enable TOTP 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 TOTP for all users within specific tenants.
1. Configure the backend
To start, we will initialise the TOTP and the MultiFactorAuth recipes in the following way:
import supertokens from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import totp from "supertokens-node/recipe/totp";
import Session from "supertokens-node/recipe/session";
supertokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
Session.init(),
ThirdParty.init({
//...
}),
EmailPassword.init({
//...
}),
totp.init(),
MultiFactorAuth.init(),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import multifactorauth, totp
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="...",
recipe_list=[
totp.init(),
multifactorauth.init(),
],
)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 TOTP requirement for a tenant, we can call the following API:
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.TOTP],
});
if (resp.createdNew) {
// Tenant created successfully
} else {
// Existing tenant's config was modified.
}
}from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
from supertokens_python.recipe.multifactorauth.types import FactorIds
async def create_new_tenant():
resp = await create_or_update_tenant(
"customer1", TenantConfigCreateOrUpdate(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
required_secondary_factors=[FactorIds.TOTP],
)
)
if resp.created_new:
# Tenant created successfully
pass
else:
# Existing tenant's config was modified
passfrom supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
from supertokens_python.recipe.multifactorauth.types import FactorIds
def create_new_tenant():
resp = create_or_update_tenant(
"customer1", TenantConfigCreateOrUpdate(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
required_secondary_factors=[FactorIds.TOTP],
)
)
if resp.created_new:
# Tenant created successfully
pass
else:
# Existing tenant's config was modified
pass- In the above, we set the
firstFactorsto["emailpassword", "thirdparty"]to indicate that the first factor can be eitheremailpasswordorthirdparty. - We set the
requiredSecondaryFactorsto["totp"]to indicate that TOTP is required for all users in this tenant. The default implementation ofgetMFARequirementsForAuthin theMultiFactorAuthtakes this into account.
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 totp, the payload will look like:
{
"st-mfa": {
"c": {
"emailpassword": 1702877939,
"totp": 1702877999
},
"v": true
}
}Indicating that the user has finished all required factors, and should be allowed to access the app.
2. Configure the frontend
We start by modifying the init function call on the frontend like so:
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.
import supertokens from "supertokens-auth-react";
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
import totp from "supertokens-auth-react/recipe/totp";
import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
supertokens.init({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
// other recipes...
totp.init(),
MultiFactorAuth.init(),
Multitenancy.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getTenantId: async (context) => {
return "TODO";
},
};
},
},
}),
],
});// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded)
supertokensUIInit({
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
usesDynamicLoginMethods: true,
recipeList: [
// other recipes...
supertokensUITOTP.init(),
supertokensUIMultiFactorAuth.init(),
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
totprecipe in therecipeList. - We also init the
MultiFactorAuthrecipe. Notice that unlike the single tenant setup, we do not specify thefirstFactorshere. That information is fetched based on the tenantId you provide the SDK with. - We have set
usesDynamicLoginMethods: trueso 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 TOTP pre-built UI when rendering the SuperTokens component:
import { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui";
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom";
function App() {
return (
<SuperTokensWrapper>
<div className="App">
<Router>
<div className="fill">
<Routes>
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [
/* ... */ TOTPPreBuiltUI,
MultiFactorAuthPreBuiltUI,
])}
// ... other routes
</Routes>
</div>
</Router>
</div>
</SuperTokensWrapper>
);
}import { SuperTokensWrapper } from "supertokens-auth-react";
import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui";
import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui";
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
function App() {
if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) {
return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]);
}
return <SuperTokensWrapper>{/*Your app*/}</SuperTokensWrapper>;
}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/totp (assuming that the websiteBasePath is /auth) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like:
- Factor Setup UI
- Verification UI (In case the factor is already setup before).
The steps here are the same as in the single tenant setup above.