Require TOTP for specific users
Implement a TOTP-based MFA policy for specific users based on customizable criteria.
Overview
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
adminrole 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.
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
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 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";
import UserRoles from "supertokens-node/recipe/userroles";
supertokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
Session.init(),
UserRoles.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) {
let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id);
if (roles.roles.includes("admin")) {
// we only want totp for admins
return [MultiFactorAuth.FactorIds.TOTP];
} else {
// no MFA for non admin users.
return [];
}
},
};
},
},
}),
],
});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
from supertokens_python.recipe.userroles.asyncio import get_roles_for_user
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
roles = await get_roles_for_user(tenant_id, (await user()).id)
if "admin" in roles.roles:
# We only want TOTP for admins
return [FactorIds.TOTP]
else:
# No MFA for non-admin users
return []
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),
),
],
)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.
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 ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
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(),
ThirdParty.init({
//...
}),
EmailPassword.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: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
}),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import multifactorauth, totp
from supertokens_python.recipe.multifactorauth.types import (
FactorIds,
OverrideConfig,
)
from typing import Dict, Any
from supertokens_python.recipe.totp.types import (
TOTPConfig,
OverrideConfig,
VerifyDeviceOkResult,
)
from supertokens_python.recipe.totp.interfaces import APIInterface, APIOptions
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.multifactorauth.asyncio import (
add_to_required_secondary_factors_for_user,
)
def totp_override(original_implementation: APIInterface):
original_verify_device_post = original_implementation.verify_device_post
async def verify_device_post(
device_name: str,
totp: str,
options: APIOptions,
session: SessionContainer,
user_context: Dict[str, Any],
):
response = await original_verify_device_post(
device_name, totp, options, session, user_context
)
if isinstance(response, VerifyDeviceOkResult):
await add_to_required_secondary_factors_for_user(
session.get_user_id(), FactorIds.TOTP
)
return response
original_implementation.verify_device_post = verify_device_post
return original_implementation
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="...",
recipe_list=[
totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))),
multifactorauth.init(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY]
),
],
)We 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. Therefore all we need to do is mark totp as enabled for a user as soon as they have setup a device successfully. 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 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. By default, a TOTP code that’s shown to the user, is valid for 60 seconds (defaultPeriod + defaultSkew*defaultPeriodseconds)
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.
2. Configure the frontend
Two parts exist 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 in Configure the frontend.
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(MultiFactorAuth.FactorIds.TOTP);
}from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user
from supertokens_python.recipe.multifactorauth.types import FactorIds
async def is_totp_factor_enabled_for_user(user_id: str) -> bool:
factors = await get_required_secondary_factors_for_user(user_id, {})
return FactorIds.TOTP in factorsfrom supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user
from supertokens_python.recipe.multifactorauth.types import FactorIds
def is_totp_factor_enabled_for_user(user_id: str) -> bool:
factors = get_required_secondary_factors_for_user(user_id, {})
return FactorIds.TOTP in factorsIf 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, MultiFactorAuth.FactorIds.TOTP);
}
async function disableMFAForUser(userId: string) {
await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP);
}from supertokens_python.recipe.multifactorauth.asyncio import (
add_to_required_secondary_factors_for_user,
remove_from_required_secondary_factors_for_user,
)
from supertokens_python.recipe.multifactorauth.types import FactorIds
async def enable_mfa_for_user(user_id: str) -> None:
await add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP)
async def disable_mfa_for_user(user_id: str) -> None:
await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP)from supertokens_python.recipe.multifactorauth.syncio import (
add_to_required_secondary_factors_for_user,
remove_from_required_secondary_factors_for_user,
)
from supertokens_python.recipe.multifactorauth.types import FactorIds
def enable_mfa_for_user(user_id: str) -> None:
add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP)
def disable_mfa_for_user(user_id: str) -> None:
remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP)In order to list existing TOTP devices on the frontend, you can call the following API:
Notice that the API call requires the session’s access token as an input (this should be added by our frontend SDK automatically):
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");
}
}async function fetchTOTPDevices() {
if (await supertokensSession.doesSessionExist()) {
try {
let totpDevicesResponse = await supertokensTotp.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");
}
}curl --location --request GET '<YOUR_API_DOMAIN>/auth/totp/device/list' \
--header 'Authorization: Bearer ...'The output from the API call is as follows:
{
"status": "OK",
"devices": {
"name": "TOTP Device 1",
"period": 30,
"skew": 1,
"verified": true
}[];
} | {
"status": "GENERAL_ERROR"
}- A
status: OKwill 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 areverifiedto the user. - A
status: GENERAL_ERROR: This is possible if you have overridden 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:
Notice that the API call requires the session’s access token as an input (this should be added by our frontend SDK automatically):
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");
}
}async function removeTOTPDevices(deviceName: string) {
if (await supertokensSession.doesSessionExist()) {
try {
await supertokensTotp.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");
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/totp/device/remove' \
--header 'Authorization: Bearer ...'
--header 'Content-Type: application/json' \
--data-raw '{
"deviceName": "..."
}'The output from the API call is as follows:
{
"status": "OK",
"didDeviceExist": true;
} | {
"status": "GENERAL_ERROR"
}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:
In order to add a new device, you can redirect the user to /{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath} from your settings page. This will show the TOTP factor setup screen to the user:
- We add the query param of
setup=truebecause we want to create a new device. - The
redirectToPathquery param will also tell our SDK to redirect the user back to the current page after they have finished creating the device.
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
async function redirectToTotpSetupScreen() {
MultiFactorAuth.redirectToFactor({
factorId: "totp",
forceSetup: true,
redirectBack: true,
});
}- In the snippet above, we redirect to the TOTP factor setup screen. We set the
forceSetuptotruesince we want the user to setup a new TOTP device. TheredirectBackboolean is alsotruesince we want to redirect back to the current page after the user has finished setting up the device. - You can also 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, ensuring that next time they login, they will be asked to complete the TOTP challenge.
To create a new device, redirect the user to a page that creates a TOTP device on the backend, asks the user to scan the QR code, and then verifies a TOTP. Use the functions in Case 1: Set up a new TOTP device.
1. Configure the backend
A user can be a part of multiple tenants. 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:
Only enable TOTP for users that have an admin role
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";
import UserRoles from "supertokens-node/recipe/userroles";
supertokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
appName: "...",
apiDomain: "...",
websiteDomain: "...",
},
recipeList: [
Session.init(),
UserRoles.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) {
let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id);
if (
roles.roles.includes("admin") &&
(await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)
) {
// we only want totp for admins
return [MultiFactorAuth.FactorIds.TOTP];
} else {
// no MFA for non admin users.
return [];
}
},
};
},
},
}),
],
});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
from supertokens_python.recipe.userroles.asyncio import get_roles_for_user
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
roles = await get_roles_for_user(tenant_id, (await user()).id)
if (
"admin" in roles.roles
and FactorIds.TOTP in await required_secondary_factors_for_tenant()
):
# We only want TOTP for admins
return [FactorIds.TOTP]
else:
# No MFA for non-admin users
return []
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=[
multifactorauth.init(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
override=OverrideConfig(functions=override_functions),
),
totp.init(),
],
)- The override checks
requiredSecondaryFactorsForTenant(Python:required_secondary_factors_for_tenant) so TOTP is required only when the tenant configuration includes it.
Ask for TOTP only for users that have enabled TOTP on their account
import supertokens from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
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(),
ThirdParty.init({
//...
}),
EmailPassword.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: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getMFARequirementsForAuth: async function (input) {
if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.TOTP)) {
// this means that the user has finished setting up a device from their settings page.
if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) {
return [MultiFactorAuth.FactorIds.TOTP];
}
}
// no totp required for input.user, with the input.tenant.
return [];
},
};
},
},
}),
],
});from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import multifactorauth, totp
from supertokens_python.recipe.multifactorauth.types import (
FactorIds,
OverrideConfig as MFAOverrideConfig,
MFARequirementList,
)
from supertokens_python.recipe.multifactorauth.asyncio import (
add_to_required_secondary_factors_for_user,
)
from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.types import User
from typing import Dict, Any, Callable, Awaitable, List
from supertokens_python.recipe.totp.interfaces import APIInterface, APIOptions
from supertokens_python.recipe.totp.types import (
TOTPConfig,
OverrideConfig,
VerifyDeviceOkResult,
)
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:
if FactorIds.TOTP in await required_secondary_factors_for_user():
if FactorIds.TOTP in await required_secondary_factors_for_tenant():
return [FactorIds.TOTP]
# no otp-email required for input.user, with the input.tenant.
return []
original_implementation.get_mfa_requirements_for_auth = (
get_mfa_requirements_for_auth
)
return original_implementation
def totp_override(original_implementation: APIInterface):
original_verify_device_post = original_implementation.verify_device_post
async def verify_device_post(
device_name: str,
totp: str,
options: APIOptions,
session: SessionContainer,
user_context: Dict[str, Any],
):
response = await original_verify_device_post(
device_name, totp, options, session, user_context
)
if isinstance(response, VerifyDeviceOkResult):
await add_to_required_secondary_factors_for_user(
session.get_user_id(), FactorIds.TOTP
)
return response
original_implementation.verify_device_post = verify_device_post
return original_implementation
init(
app_info=InputAppInfo(
app_name="...",
api_domain="...",
website_domain="...",
),
supertokens_config=SupertokensConfig(
connection_uri="...",
),
framework="...",
recipe_list=[
totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))),
multifactorauth.init(
first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY],
override=MFAOverrideConfig(functions=override_functions),
),
],
)- The
getMFARequirementsForAuthoverride checks both the user’s required factors andrequiredSecondaryFactorsForTenant(Python:required_secondary_factors_for_tenant). TOTP is required only when it is enabled for that user and allowed by the current tenant configuration.
2. Configure the frontend
Two parts exist 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 Configure the frontend.
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(MultiFactorAuth.FactorIds.TOTP);
}from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user
from supertokens_python.recipe.multifactorauth.types import FactorIds
async def is_totp_factor_enabled_for_user(user_id: str) -> bool:
factors = await get_required_secondary_factors_for_user(user_id, {})
return FactorIds.TOTP in factorsfrom supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user
from supertokens_python.recipe.multifactorauth.types import FactorIds
def is_totp_factor_enabled_for_user(user_id: str) -> bool:
factors = get_required_secondary_factors_for_user(user_id, {})
return FactorIds.TOTP in factorsIf 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, MultiFactorAuth.FactorIds.TOTP);
}
async function disableMFAForUser(userId: string) {
await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP);
}from supertokens_python.recipe.multifactorauth.asyncio import (
add_to_required_secondary_factors_for_user,
remove_from_required_secondary_factors_for_user,
)
from supertokens_python.recipe.multifactorauth.types import FactorIds
async def enable_mfa_for_user(user_id: str) -> None:
await add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP)
async def disable_mfa_for_user(user_id: str) -> None:
await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP)from supertokens_python.recipe.multifactorauth.syncio import (
add_to_required_secondary_factors_for_user,
remove_from_required_secondary_factors_for_user,
)
from supertokens_python.recipe.multifactorauth.types import FactorIds
def enable_mfa_for_user(user_id: str) -> None:
add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP)
def disable_mfa_for_user(user_id: str) -> None:
remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP)In order to list existing TOTP devices on the frontend, you can call the following API:
Notice that the API call requires the session’s access token as an input (this should be added by our frontend SDK automatically):
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");
}
}async function fetchTOTPDevices() {
if (await supertokensSession.doesSessionExist()) {
try {
let totpDevicesResponse = await supertokensTotp.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");
}
}curl --location --request GET '<YOUR_API_DOMAIN>/auth/totp/device/list' \
--header 'Authorization: Bearer ...'The output from the API call is as follows:
{
"status": "OK",
"devices": {
"name": "TOTP Device 1",
"period": 30,
"skew": 1,
"verified": true
}[];
} | {
"status": "GENERAL_ERROR"
}- A
status: OKwill 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 areverifiedto the user. - A
status: GENERAL_ERROR: This is possible if you have overridden 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:
Notice that the API call requires the session’s access token as an input (this should be added by our frontend SDK automatically):
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");
}
}async function removeTOTPDevices(deviceName: string) {
if (await supertokensSession.doesSessionExist()) {
try {
await supertokensTotp.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");
}
}curl --location --request POST '<YOUR_API_DOMAIN>/auth/totp/device/remove' \
--header 'Authorization: Bearer ...'
--header 'Content-Type: application/json' \
--data-raw '{
"deviceName": "..."
}'The output from the API call is as follows:
{
"status": "OK",
"didDeviceExist": true;
} | {
"status": "GENERAL_ERROR"
}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:
In order to add a new device, you can redirect the user to /{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath} from your settings page. This will show the TOTP factor setup screen to the user:
- We add the query param of
setup=truebecause we want to create a new device. - The
redirectToPathquery param will also tell our SDK to redirect the user back to the current page after they have finished creating the device.
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
async function redirectToTotpSetupScreen() {
MultiFactorAuth.redirectToFactor({
factorId: "totp",
forceSetup: true,
redirectBack: true,
});
}- In the snippet above, we redirect to the TOTP factor setup screen. We set the
forceSetuptotruesince we want the user to setup a new TOTP device. TheredirectBackboolean is alsotruesince we want to redirect back to the current page after the user has finished setting up the device. - You can also 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.
To create a new device, redirect the user to a page that creates a TOTP device on the backend, asks the user to scan the QR code, and then verifies a TOTP. Use the functions in Case 1: Set up a new TOTP device.