Function Overrides
Override backend authentication functions to cover custom use cases.
Overview
Function overrides let you customize the behavior of the functions used internally, by the SDKs. You can change how actions like signing in, signing up, creating, or revoking sessions or signing out work. This flexibility lets you integrate your own logic into the authentication and session management processes.
For example, if a recipe checks for an active session using the session recipe’s doesSessionExist function, you can override that function to work with custom session management.
Similarly, if you already have a sign-in/sign-up flow and want to integrate with SuperTokens, you can use an override to handle the migration process.
You can even implement a custom userId format by mapping userIds to those generated by SuperTokens.
Example
The code snippet shows the general flow of overriding a function. Custom logic can be injected while also calling the original implementation of the function.
import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import ThirdParty from "supertokens-node/recipe/thirdparty";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
supertokens: {
connectionURI: "...",
},
recipeList: [
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
// here we are only overriding the function that's responsible
// for creating a new session
createNewSession: async function (input) {
// TODO: some custom logic
// or call the default behaviour as show below
return await originalImplementation.createNewSession(input);
},
// ...
// TODO: override more functions
};
},
},
}),
ThirdParty.init({
signInAndUpFeature: {
providers: [
/* ... */
],
},
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
// here we are only overriding the function that's responsible
// for signing in or signing up a user.
signInUp: async function (input) {
// TODO: some custom logic
// or call the default behaviour as show below
return await originalImplementation.signInUp(input);
},
// ...
// TODO: override more functions
};
},
},
}),
],
});import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/recipe/thirdparty"
"github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
session.Init(&sessmodels.TypeInput{
Override: &sessmodels.OverrideStruct{
Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
// First we make a copy of the original implementation
originalCreateNewSession := *originalImplementation.CreateNewSession
// Then we override the default impl
(*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) {
// TODO: some custom logic
// or call the default behaviour as show below
return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext)
}
return originalImplementation
},
},
}),
thirdparty.Init(&tpmodels.TypeInput{
Override: &tpmodels.OverrideStruct{
Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface {
//First we copy the original impl
originalSignInUp := *originalImplementation.SignInUp
// Then we override the functions we want to
(*originalImplementation.SignInUp) = func(thirdPartyID string, thirdPartyUserID string, email string, oAuthTokens map[string]interface{}, rawUserInfoFromProvider tpmodels.TypeRawUserInfoFromProvider, tenantId string, userContext *map[string]interface{}) (tpmodels.SignInUpResponse, error) {
// TODO: some custom logic
// or call the default behaviour as show below
return originalSignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext)
}
// TODO: Override more functions
return originalImplementation
},
},
}),
},
})
}from typing import Any, Dict, Optional, Union
from supertokens_python import InputAppInfo, SupertokensConfig, init
from supertokens_python.recipe import session, thirdparty
from supertokens_python.recipe.session.interfaces import (
RecipeInterface as SessionRecipeInterface,
)
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.thirdparty.interfaces import (
RecipeInterface as ThirdPartyRecipeInterface,
)
from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider
from supertokens_python.types import RecipeUserId
def override_thirdparty_functions(original_implementation: ThirdPartyRecipeInterface):
original_sign_in_up = original_implementation.sign_in_up
async def sign_in_up(
third_party_id: str,
third_party_user_id: str,
email: str,
is_verified: bool,
oauth_tokens: Dict[str, Any],
raw_user_info_from_provider: RawUserInfoFromProvider,
session: Optional[SessionContainer],
should_try_linking_with_session_user: Union[bool, None],
tenant_id: str,
user_context: Dict[str, Any],
):
# TODO: custom logic
# or call the default behaviour as show below
return await original_sign_in_up(
third_party_id,
third_party_user_id,
email,
is_verified,
oauth_tokens,
raw_user_info_from_provider,
session,
should_try_linking_with_session_user,
tenant_id,
user_context,
)
original_implementation.sign_in_up = sign_in_up
return original_implementation
def override_session_functions(original_implementation: SessionRecipeInterface):
original_create_new_session = original_implementation.create_new_session
async def create_new_session(
user_id: str,
recipe_user_id: RecipeUserId,
access_token_payload: Optional[Dict[str, Any]],
session_data_in_database: Optional[Dict[str, Any]],
disable_anti_csrf: Optional[bool],
tenant_id: str,
user_context: Dict[str, Any],
):
# TODO: custom logic
# or call the default behaviour as show below
return await original_create_new_session(
user_id,
recipe_user_id,
access_token_payload,
session_data_in_database,
disable_anti_csrf,
tenant_id,
user_context,
)
original_implementation.create_new_session = create_new_session
return original_implementation
init(
supertokens_config=SupertokensConfig(connection_uri="..."),
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="fastapi",
recipe_list=[
session.init(
override=session.InputOverrideConfig(functions=override_session_functions)
),
thirdparty.init(
override=thirdparty.InputOverrideConfig(
functions=override_thirdparty_functions
),
sign_in_and_up_feature=thirdparty.SignInAndUpFeature(
providers=[
# ...
]
),
)
],
)Error management
If you want to throw a custom error from function overrides you have to handle it manually.
Raise the error
import Session from "supertokens-node/recipe/session";
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
createNewSession: async function (input) {
const existingSessions = await Session.getAllSessionHandlesForUser(input.userId);
if (existingSessions.length > 0) {
// this means that the user already has a session on some other device
throw new Error("Session already exists on another device");
}
// no other session exists, and so we can continue with logging in this user
return originalImplementation.createNewSession(input);
},
};
},
},
});import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
session.Init(&sessmodels.TypeInput{
Override: &sessmodels.OverrideStruct{
Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
// first we copy the original implementation
originalCreateNewSession := *originalImplementation.CreateNewSession
(*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) {
existingSessions, err := session.GetAllSessionHandlesForUser(userID, &tenantId, userContext)
if err != nil {
return nil, err
}
if len(existingSessions) > 0 {
// this means that the user already has a session on some other device
return nil, errors.New("Session already exists on another device")
}
// no other session exists, and so we can continue with logging in this user
return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext)
}
return originalImplementation
},
},
})
}from typing import Any, Dict, Optional
from supertokens_python.recipe import session
from supertokens_python.recipe.session.asyncio import get_all_session_handles_for_user
from supertokens_python.recipe.session.interfaces import RecipeInterface
from supertokens_python.types import RecipeUserId
def override_session_functions(original_implementation: RecipeInterface):
# first we copy the original implementation
original_create_new_session = original_implementation.create_new_session
async def create_new_session(
user_id: str,
recipe_user_id: RecipeUserId,
access_token_payload: Optional[Dict[str, Any]],
session_data_in_database: Optional[Dict[str, Any]],
disable_anti_csrf: Optional[bool],
tenant_id: str,
user_context: Dict[str, Any],
):
existing_sessions = await get_all_session_handles_for_user(user_id)
if len(existing_sessions) > 0:
# this means that the user already has a session on some other device
raise Exception("Session already exists on another device")
# no other session exists, and so we can continue with logging in this user
return await original_create_new_session(
user_id,
recipe_user_id,
access_token_payload,
session_data_in_database,
disable_anti_csrf,
tenant_id,
user_context,
)
original_implementation.create_new_session = create_new_session
return original_implementation
session.init(override=session.InputOverrideConfig(functions=override_session_functions))Handle the error manually
import express from "express";
let app = express();
//...
// in your app's error handler, we catch the custom error
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err.message === "Session already exists on another device") {
// TODO: send a custom response using res
return;
}
res.status(500).send(err.message);
});import Hapi from "@hapi/hapi";
let server = new Hapi.Server({ port: 8000 });
// first we create a plugin to handle all errors from the app
const plugin = {
name: "...",
version: "...",
register: async function (server: Hapi.Server) {
server.ext("onPreResponse", async (request, h) => {
if ("isBoom" in request.response) {
let err = request.response.data;
if (err.message === "Session already exists on another device") {
// TODO: send a custom response here with takeover
}
}
return h.continue;
});
},
};
// then we register this plugin
(async () => {
await server.register(plugin);
await server.start();
})();import Fastify from "fastify";
let fastify = Fastify();
fastify.setErrorHandler(async (err: any, req, res) => {
if (err.message === "Session already exists on another device") {
// TODO: send a custom response here with takeover
}
// TODO: send a 500 error with the err.message
});import middy from "@middy/core";
import cors from "@middy/http-cors";
import SuperTokens from "supertokens-node";
// this is in the auth.js file
import { middleware } from "supertokens-node/framework/awsLambda";
import { getBackendConfig } from "./config";
module.exports.handler = middy(middleware())
.use(
cors({
origin: getBackendConfig().appInfo.websiteDomain,
credentials: true,
headers: ["Content-Type", ...SuperTokens.getAllCORSHeaders()].join(", "),
methods: "OPTIONS,POST,GET,PUT,DELETE",
}),
)
.onError((request) => {
if (request.error !== null && request.error.message === "Session already exists on another device") {
// TODO: send a custom response here with takeover
}
throw request.error;
});import Koa from "koa";
import { middleware } from "supertokens-node/framework/koa";
let app = new Koa();
app.use(async (ctx, next) => {
try {
await next();
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: return a custom response
}
throw err;
}
});
app.use(middleware());import { Next } from "@loopback/core";
import { RestApplication, Middleware, MiddlewareContext } from "@loopback/rest";
import { middleware } from "supertokens-node/framework/loopback";
let app = new RestApplication();
export const customErrorMiddleware: Middleware = async (ctx: MiddlewareContext, next: Next) => {
try {
return await next();
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: return a custom response
}
throw err;
}
};
app.middleware(middleware);
app.middleware(customErrorMiddleware);import { superTokensNextWrapper } from "supertokens-node/nextjs";
import { middleware } from "supertokens-node/framework/express";
// in the /auth/[[...path]].tsx file
export default async function superTokens(req: any, res: any) {
//...
try {
await superTokensNextWrapper(
async (next) => {
// Refer to the Next.js integration guide to know why this is needed
res.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
await middleware()(req, res, next);
},
req,
res,
);
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: send custom reply
}
throw err;
}
//...
}import { ExceptionFilter, Catch, ArgumentsHost } from "@nestjs/common";
import { errorHandler } from "supertokens-node/framework/express";
import { Error as STError } from "supertokens-node";
// we want to add our own error handler which will catch the special exception
@Catch(STError)
export class AppErrorHandler implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
const ctx = host.switchToHttp();
if (exception.message === "Session already exists on another device") {
// TODO: send custom error using ctx.getResponse<Response>()
} else {
throw exception;
}
}
}import (
"net/http"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
OnSuperTokensAPIError: func(err error, req *http.Request, res http.ResponseWriter) {
if err.Error() == "Session already exists on another device" {
// TODO: send custom error
}
// TODO: send generic error
},
})
}from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(Exception)
async def exception_handler(_: Request, exc: Exception) -> JSONResponse:
if str(exc) == "Session already exists on another device":
return JSONResponse({"message": str(exc)}, status_code=409)
return JSONResponse({"message": "Internal server error"}, status_code=500)from flask import Flask
app = Flask(__name__)
@app.errorhandler(Exception)
def all_exception_handler(error: Exception):
if str(error) == "Session already exists on another device":
return {"message": str(error)}, 409
return {"message": "Internal server error"}, 500# Add this middlware in settings.py
from typing import Callable
from django.http import HttpRequest, HttpResponse
class ErrorHandlerMiddleware:
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest):
response = self.get_response(request)
return response
def process_exception(self, request: HttpRequest, exception: Exception) -> HttpResponse:
if exception and str(exception) == "Session already exists on another device":
pass # TODO: send custom response
return HttpResponse("Error processing the request.", status=500)import { getAppDirRequestHandler } from "supertokens-node/nextjs";
import { NextRequest, NextResponse } from "next/server";
import SuperTokens from "supertokens-node";
import { backendConfig } from "@/app/config/backend";
SuperTokens.init(backendConfig());
// in the app/api/auth/[...path]/route.ts file
const handleCall = getAppDirRequestHandler();
const withCustomErrorHandling = async (request: NextRequest) => {
try {
return await handleCall(request);
} catch (err: any) {
if (err.message === "Session already exists on another device") {
// TODO: send custom reply
}
throw err;
}
};
export async function GET(request: NextRequest) {
const res = await withCustomErrorHandling(request);
if (!res.headers.has("Cache-Control")) {
// Refer to the Next.js integration guide to know why this is needed
res.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
}
return res;
}
export const POST = withCustomErrorHandling;
export const DELETE = withCustomErrorHandling;
export const PUT = withCustomErrorHandling;
export const PATCH = withCustomErrorHandling;
export const HEAD = withCustomErrorHandling;Read custom request information
We use the getRequestFromUserContext function provided by the SDK to get the request object from the user context.
We use the GetRequestFromUserContext function provided by the SDK to get the request object from the user context.
We use the get_request_from_user_context function provided by the SDK to get the request object from the user context.
import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
Session.init({
override: {
functions: (oI) => {
return {
...oI,
revokeSession: async (input) => {
let customHeaderValue = "";
const request = SuperTokens.getRequestFromUserContext(input.userContext);
if (request !== undefined) {
customHeaderValue = request.getHeaderValue("customHeader") ?? "";
} else {
/**
* This is possible if the function is triggered from the user management dashboard
*
* In this case set a reasonable default value to use
*/
customHeaderValue = "default";
}
// Perform custom logic based on the value of customHeaderValue
return oI.revokeSession(input);
},
};
},
},
});import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
session.Init(&sessmodels.TypeInput{
Override: &sessmodels.OverrideStruct{
Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
originalRevokeSession := *originalImplementation.RevokeSession
*originalImplementation.RevokeSession = func(sessionHandle string, userContext supertokens.UserContext) (bool, error) {
customHeadervalue := ""
request := supertokens.GetRequestFromUserContext(userContext)
if request != nil {
customHeadervalue = request.Header.Get("customHeader")
} else {
/**
* This is possible if the function is triggered from the user management dashboard
*
* In this case set a reasonable default value to use
*/
customHeadervalue = "default";
}
print(customHeadervalue)
// Perform custom logic based on the value of customHeadervalue
return originalRevokeSession(sessionHandle, userContext)
}
return originalImplementation
},
},
})
}from typing import Any, Dict
from supertokens_python import get_request_from_user_context
from supertokens_python.recipe import session
from supertokens_python.recipe.session.interfaces import RecipeInterface
def override_session_functions(original_implementation: RecipeInterface):
original_revoke_session = original_implementation.revoke_session
async def revoke_session(session_handle: str, user_context: Dict[str, Any]):
request=get_request_from_user_context(user_context)
customHeaderValue=""
if request is not None:
customHeaderValue=request.get_header("customHeader")
else:
#
# This is possible if the function is triggered from the user management dashboard
#
# In this case set a reasonable default value to use
#
customHeaderValue="default"
print(customHeaderValue)
# Perform custom logic based on the value of customHeadervalue
return await original_revoke_session(session_handle, user_context)
original_implementation.revoke_session = revoke_session
return original_implementation
session.init(
override=session.InputOverrideConfig(
functions=override_session_functions,
),
)