Skip to main content
Which UI do you use?
Custom UI
Pre built UI
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.

TOTP for specific users

In this page, we will show you how to implement an MFA policy that requires certain users to do TOTP. You can decide which those users are based on any criteria. For example:

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

Whatever the criteria is, the steps to implementing this type of a flow is the same.

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 setup#

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

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

import supertokens from "supertokens-node";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"
import totp from "supertokens-node/recipe/totp"
import Session from "supertokens-node/recipe/session"
import UserRoles from "supertokens-node/recipe/userroles"

supertokens.init({
supertokens: {
connectionURI: "..."
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "..."
},
recipeList: [
Session.init(),
UserRoles.init(),
ThirdPartyEmailPassword.init({
//...
}),
totp.init(),
MultiFactorAuth.init({
firstFactors: ["emailpassword", "thirdparty"],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id)
if (roles.roles.includes("admin")) {
// we only want totp for admins
return ["totp"]
} else {
// no MFA for non admin users.
return []
}
}
}
}
}
})
]
})

We override the getMFARequirementsForAuth function to indicate that totp must be completed only for users that have the admin role. You can also have any other criteria here.

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

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

import supertokens from "supertokens-node";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"
import MultiFactorAuth, { MultiFactorAuthClaim } 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(),
ThirdPartyEmailPassword.init({
//...
}),
totp.init({
override: {
apis: (oI) => {
return {
...oI,
verifyDevicePOST: async function (input) {
let response = await oI.verifyDevicePOST!(input);
if (response.status === "OK") {
// device successfully verified. We save that this user has enabled TOTP in the user metadata.
// The multifactorauth recipe will pick this value up next time the user is trying to login, and
// ask them to enter the TOTP code.
await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP);
}
return response;
}
}
}
}
}),
MultiFactorAuth.init({
firstFactors: ["emailpassword", "thirdparty"],
})
]
})

We simply initialise the multi factor auth recipe here without any override to getMFARequirementsForAuth. The default implementation of this function already checks what factors are enabled for a user and returns those. So all we need to do is mark totp as enabled for a user as soon as they have setup a device successfuly. This happens in the verifyDevicePOST API override as shown above. Once a device is verified, we mark the totp factor as enabled for the user, and the next time they login, they will be asked to complete the TOTP challenge.

In both of the examples above, notice that we have initialised the TOTP recipe in the recipeList. Here are some of the configrations you can add to the totp.init function:

  • issuer: This is the name that will show up in the TOTP app for the user. By default, this is equal to the appName config, however, you can change it to something else using this property.
  • defaultSkew: The default value of this is 1, 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 is 30, which means that the current tick is valie for 30 seconds. So by default, a TOTP code that's just shown to the user, is valid for 60 seconds (defaultPeriod + defaultSkew*defaultPeriod seconds)

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

{
"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.

Frontend setup#

There are two parts to this:

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

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

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

  • When the user navigates to their settings page, you can show them if TOTP is enabled or not.
  • If enabled, you can show them a list of current TOTP devices with options to remove any.
  • If enabled, you can show them an option to add a new TOTP device.

In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function:

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

async function isTotpEnabledForUser(userId: string) {
let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId)
return factors.includes("totp")
}

If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function:

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

async function enableMFAForUser(userId: string) {
await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, "totp")
}

async function disableMFAForUser(userId: string) {
await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, "totp")
}

In order to list existing TOTP devices on the frontend, you can call the following API:

import Session from "supertokens-web-js/recipe/session"
import Totp from "supertokens-web-js/recipe/totp"

async function fetchTOTPDevices() {
if (await Session.doesSessionExist()) {
try {
let totpDevicesResponse = await Totp.listDevices();
for (let i = 0; i < totpDevicesResponse.devices.length; i++) {
let currDevice = totpDevicesResponse.devices[i];
console.log(currDevice.name) // by default, this will be like "TOTP Device 1"
console.log(currDevice.verified)
}
} 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("Illegal function call: Please only call this function if a session exists")
}
}
  • A status: OK will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are verified to the user.
  • A status: GENERAL_ERROR: This is possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend

In order to remove a device, you can call the following API from the frontend:

import Session from "supertokens-web-js/recipe/session"
import Totp from "supertokens-web-js/recipe/totp"

async function removeTOTPDevices(deviceName: string) {
if (await Session.doesSessionExist()) {
try {
await Totp.removeDevice({
deviceName
});
// device is removed
} 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("Illegal function call: Please only call this function if a session exists")
}
}

In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page:

import MultiFactorAuth from 'supertokens-auth-react/recipe/multifactorauth';

async function redirectToTotpSetupScreen() {
MultiFactorAuth.redirectToFactor("totp", true, true)
}
  • In the snippet above, we redirect to the TOTP factor setup screen. The second argument represents a boolean for forceSetup which we set to true since we want the user to setup a new TOTP device. The third arg is also true since we want to redirect back to the current page after the user has finished setting up the device.
  • You can also just redirect the user to /{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath} if you don't want to use the above function.

After the user has finished creating a device, our backend override for verifyDevicePOST (see "Example 2" in Backend setup section above) will add totp as a required factor for this user, so that next time they login, they will be asked to complete the TOTP challenge.

Multi tenant setup#

Backend setup#

A user can be a part of several tenants. So if you want TOTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the Backend setup section above.

However, if you want TOTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the getMFARequirementsForAuth function override. Modifying the example code from the Backend setup section above:

Example 1: Only enable TOTP for users that have an `admin` role
import supertokens from "supertokens-node";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"
import totp from "supertokens-node/recipe/totp"
import Session from "supertokens-node/recipe/session"
import UserRoles from "supertokens-node/recipe/userroles"

supertokens.init({
supertokens: {
connectionURI: "..."
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "..."
},
recipeList: [
Session.init(),
UserRoles.init(),
ThirdPartyEmailPassword.init({
//...
}),
totp.init(),
MultiFactorAuth.init({
firstFactors: ["emailpassword", "thirdparty"],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id)
if (roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes("totp")) {
// we only want totp for admins
return ["totp"]
} else {
// no MFA for non admin users.
return []
}
}
}
}
}
})
]
})
  • The implementation of shouldRequireTotpForTenant is entirely up to you.
Example 2: Ask for TOTP only for users that have enabled TOTP on their account
import supertokens from "supertokens-node";
import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"
import MultiFactorAuth, { MultiFactorAuthClaim } 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(),
ThirdPartyEmailPassword.init({
//...
}),
totp.init({
override: {
apis: (oI) => {
return {
...oI,
verifyDevicePOST: async function (input) {
let response = await oI.verifyDevicePOST!(input);
if (response.status === "OK") {
// device successfully verified. We save that this user has enabled TOTP in the user metadata.
// The multifactorauth recipe will pick this value up next time the user is trying to login, and
// ask them to enter the TOTP code.
await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP);
}
return response;
}
}
}
}
}),
MultiFactorAuth.init({
firstFactors: ["emailpassword", "thirdparty"],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
if ((await input.requiredSecondaryFactorsForUser).includes("totp")) {
// this means that the user has finished setting up a device from their settings page.
if ((await input.requiredSecondaryFactorsForTenant).includes("totp")) {
return ["totp"]
}
}
// no totp required for input.user, with the input.tenant.
return []
}
}
}
}
})
]
})
  • We provide an override for getMFARequirementsForAuth which checks if TOTP is enabled for the user, and also take into account the tenantId to decide if we want to have this user go through the TOTP flow whilst logging into this tenant. The implementation of shouldRequireTotpForTenant is entirely up to you.

Frontend setup#

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

Protecting frontend and backend routes#

See the section on protecting frontend and backend routes.

Frontend events, pre and post API hooks#

TODO..