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.

Protecting frontend and backend routes

In thie section, we will talk about how to protect your frontend and backend routes to make them accessible only when the user has finished all the MFA challenges configured for them.

In both the backend and the frontend, we will protect routes based on the value of MFA claim in the session's access token payload.

Protecting API routes#

The default behaviour#

When you call MultiFactorAuth.init in the supertokens.init on the backend, SuperTokens automatically adds a session claim validator globally. This validator checks that the value of v in the MFA claim is true before allowing the request to proceed. If the value of v is false, the validator will send a 403 error to the frontend.

important

This validator is added globally, which means that everytime you use verifySession or getSession from our backend SDKs, this check will happen. This means that you don't need to add any extra code on a per API level to enforce MFA.

Excluding routes from the default check#

If you wish to not have the default validator check in a certain backend route, you can exclude that when calling verifySession in the following way:

import { verifySession } from "supertokens-node/recipe/session/framework/express";
import express from "express";
import { SessionRequest } from "supertokens-node/framework/express";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

let app = express();

app.post(
"/update-blog",
verifySession({
overrideGlobalClaimValidators: async (globalValidators) => {
return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
},
}),
async (req: SessionRequest, res) => {
// The user may or may not have completed the MFA required factors since we exclude
// that from the globalValidators
}
);

The same modification can be done for getSession as well.

Manually checking the MFA claim value#

If you want to have a more complex logic for doing authorisation based on the MFA claim (other than checking if v is true), you can do it in this way:

import { verifySession } from "supertokens-node/recipe/session/framework/express";
import express from "express";
import { SessionRequest } from "supertokens-node/framework/express";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session"

let app = express();

app.post(
"/update-blog",
verifySession({
overrideGlobalClaimValidators: async (globalValidators) => {
return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
},
}),
async (req: SessionRequest, res) => {
let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim);
if (mfaClaimValue === undefined) {
// this means that there is no MFA claim information in the session. This can happen if the session was created
// prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session
// in the following way:
await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim);
mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!;
}

let completedFactors = mfaClaimValue.c;
if ("totp" in completedFactors) {
// the user has finished totp
} else {
// the user has not finished totp. You can choose to do anything you like here, for example, we may throw a
// claim validation error in the following way:
throw new STError({
type: "INVALID_CLAIMS",
message: "User has not finished TOTP",
payload: [{
id: MultiFactorAuth.MultiFactorAuthClaim.key,
reason: {
message: "Factor validation failed: totp not completed",
factorId: "totp",
},
}]
})
}
}
);
  • In the code snippet above, we remove the default validator that was added to the global validators (which checks if the v value in the claim is true or not). You don't need to do this, but in the code snippet above, we show it anyway.
  • Then in the API logic, we manually fetch the claim value, and then check if TOTP has been completed or not. If it hasn't, we send back a 403 error to the frontend.

You can use a similar approach as shown above to do any kind of check.

When using a JWT verification lib#

If you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the v value in the MFA claim is true. This would be equavalent to doing a check as our default claim validator mentioned above.

important

Make sure to also do other checks on the JWT's payload. For example, if you require all users to have finished email verification, then we need to check for htat claim as well in the JWT.

Protecting frontend routes #

The default behaviour #

When you call MultiFactorAuth.init in the supertokens.init on the frontend, SuperTokens will add a default validator check that runs whenever you use the SessionAuth component. This validator checks if the v value in the MFA claim is true or not. If it is not, then the user will be redirected to the MFA auth screen.

Other forms of authorization #

If you do not want to run our default validator on a specific route, you can modify the use of SessionAuth in the following way:

import React from "react";
import { SessionAuth, useSessionContext, useClaimValue } from 'supertokens-auth-react/recipe/session';
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";

const VerifiedRoute = (props: React.PropsWithChildren<any>) => {
return (
<SessionAuth
overrideGlobalClaimValidators={(globalValidators) => {
return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id);
}}>
<InvalidClaimHandler>
{props.children}
</InvalidClaimHandler>
</SessionAuth>
);
}

function InvalidClaimHandler(props: React.PropsWithChildren<any>) {

const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim);

if (claimValue.loading) {
return null;
}

if (claimValue.value === undefined || !("totp" in claimValue.value.c)) {
return <div>You do not have access to this page because you have not completed TOTP. Please <a href="/auth/mfa/totp">click here</a> to finish to proceed.</div>
}

// the user has finished TOTP, so we can render the children
return <div>{props.children}</div>;
}
  • In the snippet above, we remove the default claim validator that is added to SessionAuth, and add out own logic that reads from the session's payload.
  • Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks.