---
title: Protect frontend and backend routes
description: Protect frontend and backend routes using SuperTokens Session Tokens and Multi-Factor Authentication (MFA).
sidebar:
  order: 100
---

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

:::caution[The backend is the authorization boundary]
Frontend MFA checks only control rendering and navigation. They can be bypassed. Every protected API must verify the
session and enforce the MFA claim on the backend; never rely on a mobile or web payload check to protect backend data.
:::

## Before you start

:::info
This guide only applies to scenarios involving **SuperTokens Session Access Tokens**.
:::

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](./important-concepts#factors) 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.

:::note[This validator is added globally, which means that every time you use `Verify Session` or `Get Session` from the 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.
:::


### Exclude routes from the default check

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

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
<DependentContent passive group="node-frameworks">
<ContentOption title="Next.js" value="nextjs">
<NextjsRouterTypeSelect />
</ContentOption>
</DependentContent>
</ContentOption>
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Express" value="express">
```tsx
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
  },
);
```
</ContentOption>
<ContentOption title="Hapi" value="hapi">
```tsx
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
  },
});
```
</ContentOption>
<ContentOption title="Fastify" value="fastify">
```tsx
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
  },
);
```
</ContentOption>
<ContentOption title="Aws Lambda" value="aws-lambda">
```tsx
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);
  },
});
```
</ContentOption>
<ContentOption title="Koa" value="koa">
```tsx
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
  },
);
```
</ContentOption>
<ContentOption title="LoopBack" value="loopback">
```tsx
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
  }
}
```
</ContentOption>
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="pages-router">

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

export default async function example(req: SessionRequest, res: any) {
  await superTokensNextWrapper(
    async (next) => {
      await verifySession({
        overrideGlobalClaimValidators: async (globalValidators) => {
          return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
        },
      })(req, res, next);
    },
    req,
    res,
  );
  // The user may or may not have completed the MFA required factors since we exclude
  // that from the globalValidators
}
```

</ConditionalContent>
</ContentOption>
<ContentOption title="Nestjs" value="nestjs">
```tsx check=false reason="application example imports local modules defined elsewhere"
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;
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-frameworks" label="Python framework">
<ContentOption title="FastAPI" value="fastapi">
```python check=false reason="route fragment assumes an existing framework application"
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
```
</ContentOption>
<ContentOption title="Flask" value="flask">
```python check=false reason="route fragment assumes an existing framework application"
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
```
</ContentOption>
<ContentOption title="Django" value="django">
```python
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
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<CodeGroup passive group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="app-router">

```tsx check=false reason="application example imports local modules defined elsewhere"
import { NextResponse, NextRequest } from "next/server";
import SuperTokens from "supertokens-node";
import { withSession } from "supertokens-node/nextjs";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { backendConfig } from "@/app/config/backend";

SuperTokens.init(backendConfig());

export function POST(request: NextRequest) {
  return withSession(
    request,
    async (err, session) => {
      if (err) {
        return NextResponse.json(err, { status: 500 });
      }
      // The user may or may not have completed the MFA required factors since we exclude
      // that from the globalValidators
      return NextResponse.json({});
    },
    {
      overrideGlobalClaimValidators: async (globalValidators) => {
        return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
      },
    },
  );
}
```

</ConditionalContent>
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

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.

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
<DependentContent passive group="node-frameworks">
<ContentOption title="Next.js" value="nextjs">
<NextjsRouterTypeSelect />
</ContentOption>
</DependentContent>
</ContentOption>
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Express" value="express">
```tsx
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",
            },
          },
        ],
      });
    }
  },
);
```
</ContentOption>
<ContentOption title="Hapi" value="hapi">
```tsx
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",
            },
          },
        ],
      });
    }
  },
});
```
</ContentOption>
<ContentOption title="Fastify" value="fastify">
```tsx
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",
            },
          },
        ],
      });
    }
  },
);
```
</ContentOption>
<ContentOption title="Aws Lambda" value="aws-lambda">
```tsx
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);
  },
});
```
</ContentOption>
<ContentOption title="Koa" value="koa">
```tsx
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",
            },
          },
        ],
      });
    }
  },
);
```
</ContentOption>
<ContentOption title="LoopBack" value="loopback">
```tsx
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",
            },
          },
        ],
      });
    }
  }
}
```
</ContentOption>
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="pages-router">

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

export default async function example(req: SessionRequest, res: any) {
  await superTokensNextWrapper(
    async (next) => {
      await verifySession({
        overrideGlobalClaimValidators: async (globalValidators) => {
          return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
        },
      })(req, res, next);
    },
    req,
    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:
    await superTokensNextWrapper(
      async (next) => {
        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",
              },
            },
          ],
        });
      },
      req,
      res,
    );
  }
}
```

</ConditionalContent>
</ContentOption>
<ContentOption title="Nestjs" value="nestjs">
```tsx check=false reason="application example imports local modules defined elsewhere"
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;
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-frameworks" label="Python framework">
<ContentOption title="FastAPI" value="fastapi">
```python check=false reason="route fragment assumes an existing framework application"
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
```
</ContentOption>
<ContentOption title="Flask" value="flask">
```python check=false reason="route fragment assumes an existing framework application"
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
```
</ContentOption>
<ContentOption title="Django" value="django">
```python check=false reason="session attribute is injected by framework middleware"
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
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<CodeGroup passive group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="app-router">

```tsx check=false reason="application example imports local modules defined elsewhere"
import { NextResponse, NextRequest } from "next/server";
import SuperTokens from "supertokens-node";
import { withSession } from "supertokens-node/nextjs";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import { backendConfig } from "@/app/config/backend";
import { Error as STError } from "supertokens-node/recipe/session";

SuperTokens.init(backendConfig());

export function POST(request: NextRequest) {
  return withSession(
    request,
    async (err, session) => {
      if (err) {
        return NextResponse.json(err, { status: 500 });
      }
      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:
        const error = 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 NextResponse.json(error, { status: 403 });
      }
      return NextResponse.json({});
    },
    {
      overrideGlobalClaimValidators: async (globalValidators) => {
        return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key);
      },
    },
  );
}
```

</ConditionalContent>
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

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

:::info[important]
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](./important-concepts#factor-completion-status) is `true`.
This would be equivalent to doing a check as our default claim validator mentioned above.

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

---

## Protect frontend routes

<UITypeSwitch />

<VariantContent storageKey="ui-type" value="prebuilt">

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
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](./important-concepts#factor-completion-status) 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:
</ContentOption>
<ContentOption title="Angular" value="angular">
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](./important-concepts#factor-completion-status) is `true` or not.
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-prebuilt-ui">
<Tab title="Reactjs" value="reactjs">
```tsx
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>;
}
```
</Tab>
<Tab title="Angular" value="angular">
```tsx
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;
}
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
- 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.
</ContentOption>
<ContentOption title="Angular" value="angular">
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.
</ContentOption>
</DependentContent>

</VariantContent>

<VariantContent storageKey="ui-type" value="custom">




<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
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](./important-concepts#factor-completion-status) is `true` or not.
</ContentOption>
<ContentOption title="Mobile" value="mobile">
The examples below read the raw access token payload for user-interface decisions only. They do not use or imply a
mobile-specific MFA authorization API. Backend MFA claim validation remains required for protected APIs.
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
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;
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles"
async function shouldLoadRoute(): Promise<boolean> {
  if (await supertokensSession.doesSessionExist()) {
    let validationErrors = await supertokensSession.validateClaims();

    if (validationErrors.length === 0) {
      // user has finished all MFA factors.
      return true;
    } else {
      for (const err of validationErrors) {
        if (err.id === supertokensMultiFactorAuth.MultiFactorAuthClaim.id) {
          // user has not finished MFA factors.
          let mfaClaimValue = await supertokensSession.getClaimValue({
            claim: supertokensMultiFactorAuth.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;
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
<DependentContent group="mobile-frameworks" label="Mobile framework">
<ContentOption title="ReactNative" value="reactnative">
```tsx
import SuperTokens from "supertokens-react-native";

async function checkIfMFAIsCompleted() {
  if (await SuperTokens.doesSessionExist()) {
    let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v;

    if (isMFACompleted) {
      // All required factors for MFA have been completed
    } else {
      // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user
    }
  }
}
```
</ContentOption>
<ContentOption title="Android" value="android">
```kotlin
import android.app.Application
import com.supertokens.session.SuperTokens
import org.json.JSONObject

class MainApplication: Application() {
    fun checkIfMFAIsCompleted() {
        val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this);
        val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean
        if (isMFACompleted) {
            // All required factors for MFA have been completed
        } else {
            // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user
        }
    }
}
```
</ContentOption>
<ContentOption title="iOS" value="ios">
```swift
import UIKit
import SuperTokensIOS

fileprivate class ViewController: UIViewController {
    func checkIfMFAIsCompleted() {
        // Attempt to retrieve the access token payload securely
        if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely() {
            // Extract the mfaObject from the accessTokenPayload
            if let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any] {
                // Determine if MFA has been completed
                if let isMFACompleted: Bool = mfaObject["v"] as? Bool {
                    if isMFACompleted {
                        // All required factors for MFA have been completed
                    } else {
                        // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user
                    }
                }
            }
        }
    }
}
```
</ContentOption>
<ContentOption title="Flutter" value="flutter">
```dart
import 'package:supertokens_flutter/supertokens.dart';

Future<void> checkIfMFAIsCompleted() async {
    var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely();

    if (accessTokenPayload.containsKey("st-mfa")) {
      Map<String, dynamic> mfaObject = accessTokenPayload["st-mfa"];

      if (mfaObject.containsKey("v")) {
        bool isMFACompleted = mfaObject["v"];

        if (isMFACompleted) {
            // All required factors for MFA have been completed
        } else {
            // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user
        }
      }
    }
}
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
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.
</ContentOption>
<ContentOption title="Mobile" value="mobile">
If the MFA claim value is missing in the access token payload, then it means that the session was created before you enabled MFA on the backend. In this case, you can call the [MFA Info](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) endpoint which will add the MFA claim to the session and check again.
</ContentOption>
</DependentContent>


</VariantContent>


---

## See also

<CardGroup cols={3}>
  <Card title="Email &amp; SMS OTP" href="/additional-verification/mfa/email-sms-otp/otp-for-all-users" />
  <Card title="TOTP" href="/additional-verification/mfa/totp/totp-for-all-users" />
  <Card title="Step-up Authentication" href="/additional-verification/mfa/step-up-auth" />
  <Card title="Backup Codes" href="/additional-verification/mfa/backup-codes" />
</CardGroup>
