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 TOPT. 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 APIsWe 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, Anyfrom supertokens_python.recipe.session.claims import BooleanClaimfrom supertokens_python.recipe import sessionfrom 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 routesIf 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 Ginfunc 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_sessionfrom supertokens_python.recipe.session import SessionContainerfrom fastapi import Dependsfrom 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_sessionfrom 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_sessionfrom django.http import HttpRequestfrom 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.