Paid Feature
This is a paid feature.
For self hosted users, Sign up to get a license key and follow the instructions sent to you by email. Using the dev license key is free. We only start charging you once you enable the feature in production using the provided production license key.
For managed service users, you can click on the "enable paid features" button on our dashboard, and follow the steps from there on. Once enabled, this feature is free on the provided development environment.
Protecting frontend and backend routes
caution
This guide only applies to scenarios which involve SuperTokens Session Tokens.
If you are implementing either, Unified Login or Microservice Authentication, features that make use of OAuth2 Access Tokens, please check the separate page that shows you how to verify those types of 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.
In thie section, we will talk about how to protect your frontend and backend routes to make them accessible only when the user has finished all the MFA challenges configured for them.
In both the backend and the frontend, we will protect routes based on the value of MFA claim in the session's access token payload.
#
Protecting API routes#
The default behaviourWhen you call MultiFactorAuth.init
in the supertokens.init
on the backend, SuperTokens automatically adds a session claim validator globally. This validator checks that the value of v
in the MFA claim is true
before allowing the request to proceed. If the value of v
is false
, the validator will send a 403 error to the frontend.
important
This validator is added globally, which means that everytime you use verifySession
or getSession
from our backend SDKs, this check will happen. This means that you don't need to add any extra code on a per API level to enforce MFA.
#
Excluding routes from the default checkIf you wish to not have the default validator check in a certain backend route, you can exclude that when calling verifySession
in the following way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js (Pages Dir)
- Next.js (App Dir)
- NestJS
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 { 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);
},
});
}
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;
}
}
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
- FastAPI
- Flask
- Django
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim
from supertokens_python.recipe.session import SessionContainer
from fastapi import Depends
@app.post('/like_comment')
async def like_comment(session: SessionContainer = Depends(
verify_session(
# We keep all validators except for the EmailVerification ones
override_global_claim_validators=lambda global_validators, session, user_context: [
validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key]
)
)):
# All validator checks have passed and the user has a verified email address
pass
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim
@app.route('/update-jwt', methods=['POST'])
@verify_session(
# We keep all validators except for the EmailVerification ones
override_global_claim_validators=lambda global_validators, session, user_context: [
validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key]
)
def like_comment():
# All validator checks have passed and the user has a verified email address
pass
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from django.http import HttpRequest
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim
@verify_session(
# We keep all validators except for the EmailVerification ones
override_global_claim_validators=lambda global_validators, session, user_context: [
validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key]
)
async def like_comment(request: HttpRequest):
# All validator checks have passed and the user has a verified email address
pass
The same modification can be done for getSession
as well.
#
Manually checking the MFA claim valueIf you want to have a more complex logic for doing authorisation based on the MFA claim (other than checking if v
is true
), you can do it in this way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js (Pages Dir)
- Next.js (App Dir)
- NestJS
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 { 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);
},
});
}
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;
}
}
note
Coming soon. In the meantime, checkout the legacy method for adding MFA to your app.
- FastAPI
- Flask
- Django
from fastapi import Depends
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.session.exceptions import (
raise_invalid_claims_exception,
ClaimValidationError,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import (
MultiFactorAuthClaim,
)
@app.post("/update-blog")
async def update_blog_api(session: SessionContainer = Depends(verify_session())):
mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
if mfa_claim_value is None:
# This means that there is no MFA claim information in the session.
# This can happen if the session was created prior to enabling the MFA recipe on the backend.
# So here, we add the value of the MFA claim to the session:
await session.fetch_and_set_claim(MultiFactorAuthClaim)
mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
assert mfa_claim_value is not None
completed_factors = mfa_claim_value.c
if "totp" not in completed_factors:
# The user has not finished TOTP. We throw a claim validation error:
raise_invalid_claims_exception(
"User has not finished TOTP",
[
ClaimValidationError(
MultiFactorAuthClaim.key,
{
"message": "Factor validation failed: totp not completed",
"factorId": "totp",
},
)
],
)
# If we reach here, it means the user has completed TOTP
from flask import Flask, g
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.exceptions import raise_invalid_claims_exception, ClaimValidationError
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim
app = Flask(__name__)
@app.route('/update-blog', methods=['POST'])
@verify_session()
def check_mfa_api():
session: SessionContainer = g.supertokens
mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim)
if mfa_claim_value is None:
# This means that there is no MFA claim information in the session.
# This can happen if the session was created prior to enabling the MFA recipe on the backend.
# So here, we add the value of the MFA claim to the session:
session.sync_fetch_and_set_claim(MultiFactorAuthClaim)
mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim)
assert mfa_claim_value is not None
completed_factors = mfa_claim_value.c
if "totp" not in completed_factors:
# The user has not finished TOTP. We throw a claim validation error:
raise_invalid_claims_exception("User has not finished TOTP", [
ClaimValidationError(MultiFactorAuthClaim.key, {
"message": "Factor validation failed: totp not completed",
"factorId": "totp",
})
])
# If we reach here, it means the user has completed TOTP
from django.http import HttpRequest
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.exceptions import (
raise_invalid_claims_exception,
ClaimValidationError,
)
from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import (
MultiFactorAuthClaim,
)
@verify_session()
async def get_user_info_api(request: HttpRequest):
session: SessionContainer = request.supertokens
mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
if mfa_claim_value is None:
# This means that there is no MFA claim information in the session.
# This can happen if the session was created prior to enabling the MFA recipe on the backend.
# So here, we add the value of the MFA claim to the session:
await session.fetch_and_set_claim(MultiFactorAuthClaim)
mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim)
assert mfa_claim_value is not None
completed_factors = mfa_claim_value.c
if "totp" not in completed_factors:
# The user has not finished TOTP. We throw a claim validation error:
raise_invalid_claims_exception(
"User has not finished TOTP",
[
ClaimValidationError(
MultiFactorAuthClaim.key,
{
"message": "Factor validation failed: totp not completed",
"factorId": "totp",
},
)
],
)
# If we reach here, it means the user has completed TOTP
- In the code snippet above, we remove the default validator that was added to the global validators (which checks if the
v
value in the claim is true or not). You don't need to do this, but in the code snippet above, we show it anyway. - Then in the API logic, we manually fetch the claim value, and then check if TOTP has been completed or not. If it hasn't, we send back a 403 error to the frontend.
You can use a similar approach as shown above to do any kind of check.
#
When using a JWT verification libIf you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the v
value in the MFA claim is true
. This would be equavalent to doing a check as our default claim validator mentioned above.
important
Make sure to also do other checks on the JWT's payload. For example, if you require all users to have finished email verification, then we need to check for htat claim as well in the JWT.
#
Protecting frontend routes- ReactJS
- Angular
- Vue
#
The default behaviourWhen 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 authorizationIf you do not want to run our default validator on a specific route, you can modify the use of SessionAuth
in the following way:
import React from "react";
import { SessionAuth, useSessionContext, useClaimValue } from 'supertokens-auth-react/recipe/session';
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
const VerifiedRoute = (props: React.PropsWithChildren<any>) => {
return (
<SessionAuth
overrideGlobalClaimValidators={(globalValidators) => {
return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id);
}}>
<InvalidClaimHandler>
{props.children}
</InvalidClaimHandler>
</SessionAuth>
);
}
function InvalidClaimHandler(props: React.PropsWithChildren<any>) {
const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim);
if (claimValue.loading) {
return null;
}
if (claimValue.value === undefined || !("totp" in claimValue.value.c)) {
return <div>You do not have access to this page because you have not completed TOTP. Please <a href="/auth/mfa/totp">click here</a> to finish to proceed.</div>
}
// the user has finished TOTP, so we can render the children
return <div>{props.children}</div>;
}
- In the snippet above, we remove the default claim validator that is added to
SessionAuth
, and add out own logic that reads from the session's payload. - Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks.
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 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 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.
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 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.