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.
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 { 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
}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
passfrom 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
passfrom 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
passimport { 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);
},
},
);
}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.
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 { 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,
);
}
}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 TOTPfrom 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 TOTPfrom 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 TOTPimport { 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);
},
},
);
}- In the code snippet above, we remove the default validator that was added to the global validators (which checks if the
vvalue 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
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.
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.
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.
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;
}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;
}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
}
}
}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
}
}
}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
}
}
}
}
}
}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
}
}
}
}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.
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 endpoint which will add the MFA claim to the session and check again.