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 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
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
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, tenantId 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, tenantId string, 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(
tenant_id: str,
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
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js (Pages Dir)
- Next.js (App Dir)
- 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 { NextResponse, NextRequest } from "next/server";
import SuperTokens from "supertokens-node";
import { withSession } from "supertokens-node/nextjs";
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 });
}
//....
return NextResponse.json({});
},
{
overrideGlobalClaimValidators: async function (globalClaimValidators) {
return [
...globalClaimValidators,
SecondFactorClaim.validators.hasValue(true),
]
}
});
}
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, tenantId 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, tenantId 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, tenantId 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, tenantId 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.