Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Protect frontend and backend routes

Protect frontend and backend routes using SuperTokens Session Tokens and Multi-Factor Authentication (MFA).

Overview

This page shows you 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, routes are protected based on value of the MFA claim, in the session’s access token payload.

Before you start

One thing to note here is that, with OAuth2 Access Tokens, you don’t need to check the MFA claims. You will get the token once the MFA flow is done.


Protect API routes

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.

Exclude routes from the default check

To exclude the default validator check in a certain backend route, you have to update Verify Session call.

Next.js router
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
  },
);
import Hapi from "@hapi/hapi";
import { verifySession } from "supertokens-node/recipe/session/framework/hapi";
import { SessionRequest } from "supertokens-node/framework/hapi";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

let server = Hapi.server({ port: 8000 });

server.route({
  path: "/update-blog",
  method: "post",
  options: {
    pre: [
      {
        method: verifySession({
          overrideGlobalClaimValidators: async (globalValidators) => {
            return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
          },
        }),
      },
    ],
  },
  handler: async (req: SessionRequest, res) => {
    // The user may or may not have completed the MFA required factors since we exclude
    // that from the globalValidators
  },
});
import Fastify from "fastify";
import { verifySession } from "supertokens-node/recipe/session/framework/fastify";
import { SessionRequest } from "supertokens-node/framework/fastify";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

let fastify = Fastify();

fastify.post(
  "/update-blog",
  {
    preHandler: 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
  },
);
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import { SessionEvent } from "supertokens-node/framework/awsLambda";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

async function updateBlog(awsEvent: SessionEvent) {
  // The user may or may not have completed the MFA required factors since we exclude
  // that from the globalValidators
}

exports.handler = verifySession(updateBlog, {
  overrideGlobalClaimValidators: async (globalValidators) => {
    return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
  },
});
import KoaRouter from "koa-router";
import { verifySession } from "supertokens-node/recipe/session/framework/koa";
import { SessionContext } from "supertokens-node/framework/koa";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

let router = new KoaRouter();

router.post(
  "/update-blog",
  verifySession({
    overrideGlobalClaimValidators: async (globalValidators) => {
      return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
    },
  }),
  async (ctx: SessionContext, next) => {
    // The user may or may not have completed the MFA required factors since we exclude
    // that from the globalValidators
  },
);
import { inject, intercept } from "@loopback/core";
import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";
import { verifySession } from "supertokens-node/recipe/session/framework/loopback";
import Session from "supertokens-node/recipe/session";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

class Example {
  constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {}
  @post("/update-blog")
  @intercept(
    verifySession({
      overrideGlobalClaimValidators: async (globalValidators) => {
        return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
      },
    }),
  )
  @response(200)
  async handler() {
    // The user may or may not have completed the MFA required factors since we exclude
    // that from the globalValidators
  }
}
import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common";
import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session";
import { AuthGuard } from "./auth/auth.guard";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";

@Controller()
export class ExampleController {
  @Post("example")
  @UseGuards(
    new AuthGuard({
      overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => {
        return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
      },
    }),
  )
  async postExample(@Session() session: SessionContainer): Promise<boolean> {
    // The user may or may not have completed the MFA required factors since we exclude
    // that from the globalValidators
    return true;
  }
}
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim
from supertokens_python.recipe.session import SessionContainer
from fastapi import Depends

@app.post('/like_comment')  
async def like_comment(session: SessionContainer = Depends(
        verify_session(
            # We keep all validators except for the EmailVerification ones
            override_global_claim_validators=lambda global_validators, session, user_context: [
                validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key]
        )
)):
    # All validator checks have passed and the user has a verified email address
    pass
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim

@app.route('/update-jwt', methods=['POST'])  
@verify_session(
    # We keep all validators except for the EmailVerification ones
    override_global_claim_validators=lambda global_validators, session, user_context: [
        validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key]
)
def like_comment():
    # All validator checks have passed and the user has a verified email address
    pass
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from django.http import HttpRequest
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim

@verify_session(
    # We keep all validators except for the EmailVerification ones
    override_global_claim_validators=lambda global_validators, session, user_context: [
        validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key]
)
async def like_comment(request: HttpRequest):
    # All validator checks have passed and the user has a verified email address
    pass

The same modification can be done for getSession as well.

Check MFA claim manually

To account for a more complex logic when you check the MFA claim (other than checking if v is true), look over the next code snippet.

Next.js router
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",
            },
          },
        ],
      });
    }
  },
);
import Hapi from "@hapi/hapi";
import { verifySession } from "supertokens-node/recipe/session/framework/hapi";
import { SessionRequest } from "supertokens-node/framework/hapi";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session";

let server = Hapi.server({ port: 8000 });

server.route({
  path: "/update-blog",
  method: "post",
  options: {
    pre: [
      {
        method: verifySession({
          overrideGlobalClaimValidators: async (globalValidators) => {
            return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
          },
        }),
      },
    ],
  },
  handler: 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",
            },
          },
        ],
      });
    }
  },
});
import Fastify from "fastify";
import { verifySession } from "supertokens-node/recipe/session/framework/fastify";
import { SessionRequest } from "supertokens-node/framework/fastify";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session";

let fastify = Fastify();

fastify.post(
  "/update-blog",
  {
    preHandler: 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",
            },
          },
        ],
      });
    }
  },
);
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import { SessionEvent } from "supertokens-node/framework/awsLambda";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session";

async function updateBlog(awsEvent: SessionEvent) {
  let mfaClaimValue = await awsEvent.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 awsEvent.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim);
    mfaClaimValue = (await awsEvent.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",
          },
        },
      ],
    });
  }
}

exports.handler = verifySession(updateBlog, {
  overrideGlobalClaimValidators: async (globalValidators) => {
    return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
  },
});
import KoaRouter from "koa-router";
import { verifySession } from "supertokens-node/recipe/session/framework/koa";
import { SessionContext } from "supertokens-node/framework/koa";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session";

let router = new KoaRouter();

router.post(
  "/update-blog",
  verifySession({
    overrideGlobalClaimValidators: async (globalValidators) => {
      return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
    },
  }),
  async (ctx: SessionContext, next) => {
    let mfaClaimValue = await ctx.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 ctx.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim);
      mfaClaimValue = (await ctx.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",
            },
          },
        ],
      });
    }
  },
);
import { inject, intercept } from "@loopback/core";
import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";
import { verifySession } from "supertokens-node/recipe/session/framework/loopback";
import Session from "supertokens-node/recipe/session";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session";

class Example {
  constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {}
  @post("/update-blog")
  @intercept(
    verifySession({
      overrideGlobalClaimValidators: async (globalValidators) => {
        return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
      },
    }),
  )
  @response(200)
  async handler() {
    let mfaClaimValue = await (this.ctx as any).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 (this.ctx as any).session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim);
      mfaClaimValue = (await (this.ctx as any).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",
            },
          },
        ],
      });
    }
  }
}
import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common";
import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session";
import { AuthGuard } from "./auth/auth.guard";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { Error as STError } from "supertokens-node/recipe/session";

@Controller()
export class ExampleController {
  @Post("example")
  @UseGuards(
    new AuthGuard({
      overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => {
        return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
      },
    }),
  )
  async postExample(@Session() session: SessionContainer): Promise<boolean> {
    let mfaClaimValue = await 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 session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim);
      mfaClaimValue = (await 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",
            },
          },
        ],
      });
    }
    return true;
  }
}
from fastapi import Depends

from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import (
    MultiFactorAuthClaim,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.exceptions import (
    ClaimValidationError,
    raise_invalid_claims_exception,
)
from supertokens_python.recipe.session.framework.fastapi import verify_session


@app.post("/update-blog")  
async def update_blog_api(session: SessionContainer = Depends(verify_session())):
    mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
    if mfa_claim_value is None:
        # This means that there is no MFA claim information in the session.
        # This can happen if the session was created prior to enabling the MFA recipe on the backend.
        # So here, we add the value of the MFA claim to the session:
        await session.fetch_and_set_claim(MultiFactorAuthClaim)
        mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
    assert mfa_claim_value is not None
    completed_factors = mfa_claim_value.c
    if "totp" not in completed_factors:
        # The user has not finished TOTP. We throw a claim validation error:
        raise_invalid_claims_exception(
            "User has not finished TOTP",
            [
                ClaimValidationError(
                    MultiFactorAuthClaim.key,
                    {
                        "message": "Factor validation failed: totp not completed",
                        "factorId": "totp",
                    },
                )
            ],
        )
    # If we reach here, it means the user has completed TOTP
from flask import Flask, g

from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import (
    MultiFactorAuthClaim,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.exceptions import (
    ClaimValidationError,
    raise_invalid_claims_exception,
)
from supertokens_python.recipe.session.framework.flask import verify_session

app = Flask(__name__)

@app.route('/update-blog', methods=['POST'])  
@verify_session()
def check_mfa_api():
    session: SessionContainer = g.supertokens  
    mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim)
    if mfa_claim_value is None:
        # This means that there is no MFA claim information in the session.
        # This can happen if the session was created prior to enabling the MFA recipe on the backend.
        # So here, we add the value of the MFA claim to the session:
        session.sync_fetch_and_set_claim(MultiFactorAuthClaim)
        mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim)
    assert mfa_claim_value is not None
    completed_factors = mfa_claim_value.c
    if "totp" not in completed_factors:
        # The user has not finished TOTP. We throw a claim validation error:
        raise_invalid_claims_exception("User has not finished TOTP", [
            ClaimValidationError(MultiFactorAuthClaim.key, {
                "message": "Factor validation failed: totp not completed",
                "factorId": "totp",
            })
        ])
    # If we reach here, it means the user has completed TOTP
from typing import cast

from django.http import HttpRequest

from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import (
    MultiFactorAuthClaim,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.exceptions import (
    ClaimValidationError,
    raise_invalid_claims_exception,
)
from supertokens_python.recipe.session.framework.django.asyncio import verify_session


@verify_session()
async def get_user_info_api(request: HttpRequest):
    session: SessionContainer = cast(SessionContainer, request.supertokens) 
    mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
    if mfa_claim_value is None:
        # This means that there is no MFA claim information in the session.
        # This can happen if the session was created prior to enabling the MFA recipe on the backend.
        # So here, we add the value of the MFA claim to the session:
        await session.fetch_and_set_claim(MultiFactorAuthClaim)
        mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
    assert mfa_claim_value is not None
    completed_factors = mfa_claim_value.c
    if "totp" not in completed_factors:
        # The user has not finished TOTP. We throw a claim validation error:
        raise_invalid_claims_exception(
            "User has not finished TOTP",
            [
                ClaimValidationError(
                    MultiFactorAuthClaim.key,
                    {
                        "message": "Factor validation failed: totp not completed",
                        "factorId": "totp",
                    },
                )
            ],
        )
    # If we reach here, it means the user has completed 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.


Protect frontend routes

UI type

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:

By default, when you do MultiFactorAuth.init in supertokens.init on the frontend, SuperTokens will add a default validator check that runs whenever you call the Session.validateClaims function. This validator checks if the v value in the MFA claim is true or not.

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>;
}
import Session from "supertokens-web-js/recipe/session";
import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth";

async function shouldLoadRoute(): Promise<boolean> {
  if (await Session.doesSessionExist()) {
    let validationErrors = await Session.validateClaims();

    if (validationErrors.length === 0) {
      // user has finished all MFA factors.
      return true;
    } else {
      for (const err of validationErrors) {
        if (err.id === MultiFactorAuthClaim.id) {
          // user has not finished MFA factors.
          let mfaClaimValue = await Session.getClaimValue({
            claim: MultiFactorAuthClaim,
          });
          if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) {
            // the user has not finished totp
            return false;
          }
        }
      }
    }
  }
  // a session does not exist, or email is not verified
  return false;
}
  • 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.

In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session’s contents and runs claim validators on them. If a claim validator fails, it will be reflected in the validationErrors variable. The MultiFactorAuthClaim validator will be automatically checked by this function since you have initialized the MFA recipe.

In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it’s the TOTP factor that is missing when the claim fails and return false from this function. However, it’s really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen.


See also

API reference

API schema and response details