Protecting API routes
caution
- SuperTokens is not yet optimised for 2FA implementation, so you have to add a lot of customisations for it to work. We are working on improving the development experience for 2FA as well as adding more factors like TOTP. Stay tuned.
- A demo app that uses the pre built UI can be found on our GitHub.
In the previous steps, we saw the a session is created after the first factor, with SecondFactorClaim set to false, and then after the second factor is completed, we update that value to true.
Protecting all APIs#
We want to protect all the application APIs such that they are accessible only when SecondFactorClaim is true - indicating that the user has completed 2FA. We can do this by by overriding the getGlobalClaimValidators function in the Session recipe.
- NodeJS
- GoLang
- Python
import Session from "supertokens-node/recipe/session";
Session.init({
    override: {
        functions: (oI) => {
            return {
                ...oI,
                getGlobalClaimValidators: (input) => [
                    ...input.claimValidatorsAddedByOtherRecipes,
                    SecondFactorClaim.validators.hasValue(true),
                ],
            };
        },
    }
})
import (
    "github.com/supertokens/supertokens-golang/recipe/session"
    "github.com/supertokens/supertokens-golang/recipe/session/claims"
    "github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
    "github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
    _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId string, userContext supertokens.UserContext) (interface{}, error) {
        return false, nil
    }, nil)
    session.Init(&sessmodels.TypeInput{
        Override: &sessmodels.OverrideStruct{
            Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
                
                (*originalImplementation.GetGlobalClaimValidators) = func(userId string, claimValidatorsAddedByOtherRecipes []claims.SessionClaimValidator, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
                    claimValidatorsAddedByOtherRecipes = append(claimValidatorsAddedByOtherRecipes,
                        SecondFactorClaimValidator.HasValue(true, nil, nil))
                    return claimValidatorsAddedByOtherRecipes, nil
                }
                return originalImplementation
            },
        },
    })
}
from typing import List, Dict, Any
from supertokens_python.recipe.session.claims import BooleanClaim
from supertokens_python.recipe import session
from supertokens_python.recipe.session.interfaces import RecipeInterface, SessionClaimValidator
SecondFactorClaim = BooleanClaim(
    key="2fa-completed", fetch_value=lambda _, __: False)
def override_session_functions(original_implementation: RecipeInterface):
    async def get_global_claim_validators(
        user_id: str,
        claim_validators_added_by_other_recipes: List[SessionClaimValidator],
        user_context: Dict[str, Any],
    ):
        return claim_validators_added_by_other_recipes + [SecondFactorClaim.validators.has_value(True)]
    original_implementation.get_global_claim_validators = get_global_claim_validators
    return original_implementation
session.init(override=session.InputOverrideConfig(override_session_functions))
Protecting specific API routes#
If instead, you want to enforce 2FA just on certain API routes, you can add the validator only when calling the verifySession function:
- NodeJS
- GoLang
- Python
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js
- NestJS
import express from "express";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";
let app = express();
app.post("/like-comment", verifySession({
    overrideGlobalClaimValidators: (globalValidators) => [
        ...globalValidators, 
        SecondFactorClaim.validators.hasValue(true),
    ]
}), (req: SessionRequest, res) => {
    //....
});
import Hapi from "@hapi/hapi";
import { verifySession } from "supertokens-node/recipe/session/framework/hapi";
import { SessionRequest } from "supertokens-node/framework/hapi";
let server = Hapi.server({ port: 8000 });
server.route({
    path: "/like-comment",
    method: "post",
    options: {
        pre: [
            {
                method: verifySession({
                    overrideGlobalClaimValidators: (globalValidators) => [
                        ...globalValidators, 
                        SecondFactorClaim.validators.hasValue(true),
                    ]
                })
            },
        ],
    },
    handler: async (req: SessionRequest, res) => {
        //...
    }
})
import Fastify from "fastify";
import { verifySession } from "supertokens-node/recipe/session/framework/fastify";
import { SessionRequest } from "supertokens-node/framework/fastify";
let fastify = Fastify();
fastify.post("/like-comment", {
    preHandler: verifySession({
        overrideGlobalClaimValidators: (globalValidators) => [
            ...globalValidators, 
            SecondFactorClaim.validators.hasValue(true),
        ]
    }),
}, (req: SessionRequest, res) => {
    //....
});
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import { SessionEventV2 } from "supertokens-node/framework/awsLambda";
async function likeComment(awsEvent: SessionEventV2) {
    //....
};
exports.handler = verifySession(likeComment, {
    overrideGlobalClaimValidators: (globalValidators) => [
        ...globalValidators, 
        SecondFactorClaim.validators.hasValue(true),
    ]
});
import KoaRouter from "koa-router";
import { verifySession } from "supertokens-node/recipe/session/framework/koa";
import { SessionContext } from "supertokens-node/framework/koa";
let router = new KoaRouter();
router.post("/like-comment", verifySession({
    overrideGlobalClaimValidators: (globalValidators) => [
        ...globalValidators, 
        SecondFactorClaim.validators.hasValue(true),
    ]
}), (ctx: SessionContext, next) => {
    //....
});
import { inject, intercept } from "@loopback/core";
import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";
import { verifySession } from "supertokens-node/recipe/session/framework/loopback";
import { SessionContext } from "supertokens-node/framework/loopback";
class LikeComment {
    constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { }
    @post("/like-comment")
    @intercept(verifySession({
        overrideGlobalClaimValidators: (globalValidators) => [
            ...globalValidators, 
            SecondFactorClaim.validators.hasValue(true),
        ]
    }))
    @response(200)
    handler() {
        //....
    }
}
import { superTokensNextWrapper } from 'supertokens-node/nextjs'
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";
export default async function likeComment(req: SessionRequest, res: any) {
    await superTokensNextWrapper(
        async (next) => {
            await verifySession({
                overrideGlobalClaimValidators: (globalValidators) => [
                    ...globalValidators, 
                    SecondFactorClaim.validators.hasValue(true),
                ]
            })(req, res, next);
        },
        req,
        res
    )
    //....
}
import { Controller, Post, UseGuards, Session } from "@nestjs/common";
import { SessionContainer } from "supertokens-node/recipe/session";
import { AuthGuard } from './auth/auth.guard';
@Controller()
export class ExampleController {
  @Post('example')
  // For more information about this guard please read our NestJS guide.
  @UseGuards(new AuthGuard({
      overrideGlobalClaimValidators: (globalValidators) => [
        ...globalValidators,
        SecondFactorClaim.validators.hasValue(true),
      ]
  })) 
  async postExample(@Session() session: SessionContainer): Promise<boolean> {
    return true;
  }
}
- Chi
- net/http
- Gin
- Mux
import (
    "net/http"
    "github.com/supertokens/supertokens-golang/recipe/session"
    "github.com/supertokens/supertokens-golang/recipe/session/claims"
    "github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
    "github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
    _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId string, userContext supertokens.UserContext) (interface{}, error) {
        return false, nil
    }, nil)
    http.ListenAndServe("SERVER ADDRESS", corsMiddleware(
        supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
            // Handle your APIs..
            if r.URL.Path == "/like-comment" {
                session.VerifySession(&sessmodels.VerifySessionOptions{
                    OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
                        globalClaimValidators = append(globalClaimValidators,
                            SecondFactorClaimValidator.HasValue(true, nil, nil))
                        return globalClaimValidators, nil
                    },
                }, likeCommentAPI).ServeHTTP(rw, r)
                return
            }
        }))))
}
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) {
        //...
    })
}
func likeCommentAPI(w http.ResponseWriter, r *http.Request) {
    // If it comes here, the user has completed 2fa.
}
import (
    "net/http"
    "github.com/gin-gonic/gin"
    "github.com/supertokens/supertokens-golang/recipe/session"
    "github.com/supertokens/supertokens-golang/recipe/session/claims"
    "github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
    "github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
    _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId string, userContext supertokens.UserContext) (interface{}, error) {
        return false, nil
    }, nil)
    router := gin.New()
    router.GET("/like-comment", verifySession(&sessmodels.VerifySessionOptions{
        OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
            globalClaimValidators = append(globalClaimValidators,
                SecondFactorClaimValidator.HasValue(true, nil, nil))
            return globalClaimValidators, nil
        },
    }), likeComment)
}
// Wrap session.VerifySession to work with Gin
func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc {
    return func(c *gin.Context) {
        session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) {
            c.Request = c.Request.WithContext(r.Context())
            c.Next()
        })(c.Writer, c.Request)
        // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly
        c.Abort()
    }
}
func likeComment(c *gin.Context) {
    // If it comes here, the user has completed 2fa.
}
import (
    "net/http"
    "github.com/go-chi/chi"
    "github.com/supertokens/supertokens-golang/recipe/session"
    "github.com/supertokens/supertokens-golang/recipe/session/claims"
    "github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
    "github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
    _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId string, userContext supertokens.UserContext) (interface{}, error) {
        return false, nil
    }, nil)
    r := chi.NewRouter()
    r.Get("/like-comment", session.VerifySession(&sessmodels.VerifySessionOptions{
        OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
            globalClaimValidators = append(globalClaimValidators,
                SecondFactorClaimValidator.HasValue(true, nil, nil))
            return globalClaimValidators, nil
        },
    }, likeComment))
}
func likeComment(w http.ResponseWriter, r *http.Request) {
    // If it comes here, the user has completed 2fa.
}
import (
    "net/http"
    "github.com/gorilla/mux"
    "github.com/supertokens/supertokens-golang/recipe/session"
    "github.com/supertokens/supertokens-golang/recipe/session/claims"
    "github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
    "github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
    _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId string, userContext supertokens.UserContext) (interface{}, error) {
        return false, nil
    }, nil)
    router := mux.NewRouter()
    router.HandleFunc("/like-comment",
        session.VerifySession(&sessmodels.VerifySessionOptions{
            OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
                globalClaimValidators = append(globalClaimValidators,
                    SecondFactorClaimValidator.HasValue(true, nil, nil))
                return globalClaimValidators, nil
            },
        }, likeComment)).Methods(http.MethodGet)
}
func likeComment(w http.ResponseWriter, r *http.Request) {
    // If it comes here, the user has completed 2fa.
}
- FastAPI
- Flask
- Django
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.session import SessionContainer
from fastapi import Depends
from supertokens_python.recipe.session.claims import BooleanClaim
SecondFactorClaim = BooleanClaim(
    key="2fa-completed", fetch_value=lambda _, __: False)
@app.post('/like_comment')  
async def like_comment(session: SessionContainer = Depends(
        verify_session(
            # We add the SecondFactorClaim's has_value(True) validator
            override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \
            [SecondFactorClaim.validators.has_value(True)]
        )
)):
    # All validator checks have passed and the user has completed 2FA
    pass
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.session.claims import BooleanClaim
SecondFactorClaim = BooleanClaim(
    key="2fa-completed", fetch_value=lambda _, __: False)
@app.route('/update-jwt', methods=['POST'])  
@verify_session(
    # We add the SecondFactorClaim's has_value(True) validator
    override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \
    [SecondFactorClaim.validators.has_value(True)]
)
def like_comment():
    # All validator checks have passed and the user has completed 2FA
    pass
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from django.http import HttpRequest
from supertokens_python.recipe.session.claims import BooleanClaim
SecondFactorClaim = BooleanClaim(
    key="2fa-completed", fetch_value=lambda _, __: False)
@verify_session(
    # We add the SecondFactorClaim's has_value(True) validator
    override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \
    [SecondFactorClaim.validators.has_value(True)]
)
async def like_comment(request: HttpRequest):
    # All validator checks have passed and the user has completed 2FA
    pass
important
If the SecondFactorClaim claim validator fails, then the SDK will send a 403 response.