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 setupExample 1: Only enable TOTP for users that have an `admin` role
To start with, we configure the backend in the following way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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: [
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 []
}
}
}
}
}
})
]
})
note
Coming soon.
note
Coming soon.
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:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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: [
MultiFactorAuth.FactorIds.EMAILPASSWORD,
MultiFactorAuth.FactorIds.THIRDPARTY
],
})
]
})
note
Coming soon.
note
Coming soon.
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 theappName
config, 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 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 setupThere 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:
- 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.
If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function:
- 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)
}
async function disableMFAForUser(userId: string) {
await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(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.
In order to list existing TOTP devices on the frontend, you can call the following API:
- Web
- Mobile
- Via NPM
- Via Script Tag
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")
}
}
Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically):
curl --location --request GET '<YOUR_API_DOMAIN>/auth/totp/device/list' \
--header 'rid: totp' \
--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: 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 areverified
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:
- Web
- Mobile
- Via NPM
- Via Script Tag
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")
}
}
Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically):
curl --location --request POST '<YOUR_API_DOMAIN>/auth/totp/device/remove' \
--header 'rid: totp' \
--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"
}
- ReactJS
- Angular
- Vue
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=true
because we want to create a new device. - The
redirectToPath
query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device.
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 alsotrue
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.
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=true
because we want to create a new device. - The
redirectToPath
query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device.
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 setupA 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
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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: [
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 []
}
}
}
}
}
})
]
})
note
Coming soon.
note
Coming soon.
- The implementation of
shouldRequireTotpForTenant
is entirely up to you.
Example 2: Ask for TOTP only for users that have enabled TOTP on their account
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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: [
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 []
}
}
}
}
})
]
})
note
Coming soon.
note
Coming soon.
- 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 ofshouldRequireTotpForTenant
is entirely up to you.
#
Frontend setupThe frontend setup is identical to the frontend setup section above.
#
Protecting frontend and backend routesSee the section on protecting frontend and backend routes.
#
Frontend events, pre and post API hooksTODO..