Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

User Context

Pass information across API functions using user context for custom execution scenarios.

Overview

The UserContext mechanism is a way to pass information across recipe or API functions to customize the authentication flow inside a specific execution context.

By default, the user context passed to APIs and functions contains the request object that can read custom headers, body, or query parameters.

Prerequisites

Example

For example, you may want to disable creation of a session during sign up, ensuring that the user has to login again post sign up. To do that, the create new session recipe function must know that it’s called from the sign up API and return an empty session. This is as opposed to it invoking from the sign in API, when it should continue with normal functionality.

To achieve this, all the API interface and recipe interface functions take a parameter called userContext, which is by default an empty object. When overriding the functions, anything can be added to this object, and that information carries onto the next set of functions called in the API.

import SuperTokens from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  supertokens: {
    connectionURI: "...",
  },
  recipeList: [
    EmailPassword.init({
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            signUp: async function (input) {
              let resp = await originalImplementation.signUp(input);
              if (resp.status === "OK" && resp.user.loginMethods.length === 1 && input.session === undefined) {
                /*
                 * This is called during the sign up API for email password login,
                 * but before calling the createNewSession function.
                 * We override the recipe function as shown here,
                 * and then set the relevant context only if it's a new user.
                 */
                input.userContext.isSignUp = true;
              }
              return resp;
            },
          };
        },
      },
    }),
    ThirdParty.init({
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            signInUp: async function (input) {
              let resp = await originalImplementation.signInUp(input);
              if (
                resp.status === "OK" &&
                resp.createdNewRecipeUser &&
                resp.user.loginMethods.length === 1 &&
                input.session === undefined
              ) {
                /*
                 * This is called during the signInUp API for third party login,
                 * but before calling the createNewSession function.
                 * At the start of the API, we do not know if it will result in a
                 * sign in or a sign up, so we cannot override the API function.
                 * Instead, we override the recipe function as shown here,
                 * and then set the relevant context only if it's a new user.
                 */
                input.userContext.isSignUp = true;
              }
              return resp;
            },
          };
        },
      },
    }),
  ],
});
import (
	"github.com/supertokens/supertokens-golang/recipe/emailpassword"
	"github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels"
	"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{
			thirdparty.Init(&tpmodels.TypeInput{
				Override: &tpmodels.OverrideStruct{
					Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface {
						ogSignInUp := *originalImplementation.SignInUp

						(*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) {
							resp, err := ogSignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext)

							if err != nil {
								return tpmodels.SignInUpResponse{}, err
							}

							if resp.OK != nil && resp.OK.CreatedNewUser {
								/*
								 * This is called during the signInUp API for third party login,
								 * but before calling the createNewSession function.
								 * At the start of the API, we do not know if it will result in a
								 * sign in or a sign up, so we cannot override the API function.
								 * Instead, we override the recipe function as shown here,
								 * and then set the relevant context only if it's a new user.
								 */
								(*userContext)["isSignUp"] = true
							}
							return resp, nil
						}

						return originalImplementation
					},
				},
			}),
			emailpassword.Init(&epmodels.TypeInput{
				Override: &epmodels.OverrideStruct{
					APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface {
						ogSignUpPOST := *originalImplementation.SignUpPOST

						(*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) {
							// by default, the userContext object is {},
							// it's changed to {isSignUp: true}, since this is one of the
							// sign up API, and this will tell the CreateNewSession function
							// (being called inside ogEmailPasswordSignUpPOST)
							// to not create a new session in case userContext["isSignUp"] == true
							(*userContext)["isSignUp"] = true
							return ogSignUpPOST(formFields, tenantId, options, userContext)
						}

						return originalImplementation
					},
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import thirdparty, emailpassword
from supertokens_python.recipe.thirdparty.interfaces import (
    RecipeInterface,
    SignInUpOkResult,
)
from supertokens_python.recipe.emailpassword.interfaces import APIOptions, APIInterface
from supertokens_python.recipe.emailpassword.types import FormField
from typing import List, Dict, Any, Union, Optional
from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider
from supertokens_python.recipe.session.interfaces import SessionContainer


def override_emailpassword_apis(original_implementation: APIInterface):
    original_sign_up_post = original_implementation.sign_up_post

    async def sign_up_post(
        form_fields: List[FormField],
        tenant_id: str,
        session: Union[SessionContainer, None],
        should_try_linking_with_session_user: Union[bool, None],
        api_options: APIOptions,
        user_context: Dict[str, Any],
    ):

        # by default, the userContext Dict is {},
        # it's changed to {isSignUp: true}, since this is one of the
        # sign up API, and this will tell the create_new_session function
        # (being called inside original_emailpassword_sign_up_post)
        # to not create a new session in case userContext["isSignUp"] is True
        user_context["isSignUp"] = True
        return await original_sign_up_post(
            form_fields,
            tenant_id,
            session,
            should_try_linking_with_session_user,
            api_options,
            user_context,
        )

    original_implementation.sign_up_post = sign_up_post
    return original_implementation


def override_thirdparty_functions(original_implementation: RecipeInterface):
    original_thirdparty_sign_in_up = original_implementation.sign_in_up

    async def thirdparty_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],
    ):
        response = await original_thirdparty_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,
        )

        # This is called during the sign_in_up API for third party login,
        # but before calling the create_new_session function.
        # At the start of the API, we do not know if it will result in a
        # sign in or a sign up, so we cannot override the API function.
        # Instead, we override the recipe function as shown here,
        # and then set the relevant context only if it's a new user.
        if (
            isinstance(response, SignInUpOkResult)
            and response.created_new_recipe_user
            and len(response.user.login_methods) == 1
            and session is None
        ):
            user_context["isSignUp"] = True

        return response

    original_implementation.sign_in_up = thirdparty_sign_in_up
    return original_implementation


init(
    supertokens_config=SupertokensConfig(connection_uri="..."),
    app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
    framework="fastapi",
    recipe_list=[
        thirdparty.init(
            override=thirdparty.InputOverrideConfig(
                functions=override_thirdparty_functions
            )
        ),
        emailpassword.init(
            override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)
        ),
    ],
)

Then consume that context in the createNewSession function to return an empty function in case the userContext.isSignUp is true.

import SuperTokens from "supertokens-node";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Session from "supertokens-node/recipe/session";

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  supertokens: {
    connectionURI: "...",
  },
  recipeList: [
    ThirdParty.init({
      /* See previous step... */
    }),
    EmailPassword.init({
      /* See previous step... */
    }),
    Session.init({
      override: {
        functions: (originalImplementation) => {
          return {
            ...originalImplementation,
            createNewSession: async function (input) {
              if (input.userContext.isSignUp) {
                /**
                 * The execution will come here only in case
                 * a sign up API is calling this function. This is because
                 * only then will the input.userContext.isSignUp === true
                 * (see above code).
                 */
                return {
                  // this is an empty session. It won't result in a session being created for the user.
                  getAccessToken: () => "",
                  getAccessTokenPayload: () => null,
                  getExpiry: async () => -1,
                  getHandle: () => "",
                  getSessionDataFromDatabase: async () => null,
                  getTimeCreated: async () => -1,
                  getUserId: () => "",
                  revokeSession: async () => {},
                  updateSessionDataInDatabase: async () => {},
                  mergeIntoAccessTokenPayload: async () => {},
                  assertClaims: async () => {},
                  fetchAndSetClaim: async () => {},
                  getClaimValue: async () => undefined,
                  setClaimValue: async () => {},
                  removeClaim: async () => {},
                  attachToRequestResponse: () => {},
                  getAllSessionTokensDangerously: () => ({
                    accessAndFrontTokenUpdated: false,
                    accessToken: "",
                    frontToken: "",
                    antiCsrfToken: undefined,
                    refreshToken: undefined,
                  }),
                  getTenantId: () => "public",
                  getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""),
                };
              }
              return originalImplementation.createNewSession(input);
            },
          };
        },
      },
    }),
  ],
});
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() {
	supertokens.Init(supertokens.TypeInput{
		RecipeList: []supertokens.Recipe{
			session.Init(&sessmodels.TypeInput{
				Override: &sessmodels.OverrideStruct{
					Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
						ogCreateNewSession := *originalImplementation.CreateNewSession

						(*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) {
							_, isSignUp := (*userContext)["isSignUp"]
							if isSignUp {
								/**
								 * The execution will come here only in case
								 * a sign up API is calling this function. This is because
								 * only then will the (*userContext)["isSignUp"] === true
								 * (see above code).
								 */
								return &sessmodels.TypeSessionContainer{
									RevokeSession:                          func() error { return nil },
									GetSessionDataInDatabase:               func() (map[string]interface{}, error) { return nil, nil },
									UpdateSessionDataInDatabase:            func(newSessionData map[string]interface{}) error { return nil },
									GetUserID:                              func() string { return "" },
									GetTenantId:                            func() string { return "public" },
									GetAccessTokenPayload:                  func() map[string]interface{} { return nil },
									GetHandle:                              func() string { return "" },
									GetAllSessionTokensDangerously:         func() sessmodels.SessionTokens { return sessmodels.SessionTokens{} },
									GetAccessToken:                         func() string { return "" },
									GetTimeCreated:                         func() (uint64, error) { return 0, nil },
									GetExpiry:                              func() (uint64, error) { return 0, nil },
									RevokeSessionWithContext:               func(userContext supertokens.UserContext) error { return nil },
									GetSessionDataInDatabaseWithContext:    func(userContext supertokens.UserContext) (map[string]interface{}, error) { return nil, nil },
									UpdateSessionDataInDatabaseWithContext: func(newSessionData map[string]interface{}, userContext supertokens.UserContext) error { return nil },
									GetUserIDWithContext:                   func(userContext supertokens.UserContext) string { return "" },
									GetTenantIdWithContext:                 func(userContext supertokens.UserContext) string { return "public" },
									GetAccessTokenPayloadWithContext:       func(userContext supertokens.UserContext) map[string]interface{} { return nil },
									GetHandleWithContext:                   func(userContext supertokens.UserContext) string { return "" },
									GetAccessTokenWithContext:              func(userContext supertokens.UserContext) string { return "" },
									GetTimeCreatedWithContext:              func(userContext supertokens.UserContext) (uint64, error) { return 0, nil },
									GetExpiryWithContext:                   func(userContext supertokens.UserContext) (uint64, error) { return 0, nil },
									MergeIntoAccessTokenPayloadWithContext: func(accessTokenPayloadUpdate map[string]interface{}, userContext supertokens.UserContext) error {
										return nil
									},
									AssertClaimsWithContext: func(claimValidators []claims.SessionClaimValidator, userContext supertokens.UserContext) error {
										return nil
									},
									FetchAndSetClaimWithContext: func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error { return nil },
									SetClaimValueWithContext: func(claim *claims.TypeSessionClaim, value interface{}, userContext supertokens.UserContext) error {
										return nil
									},
									GetClaimValueWithContext:    func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) interface{} { return nil },
									RemoveClaimWithContext:      func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error { return nil },
									MergeIntoAccessTokenPayload: func(accessTokenPayloadUpdate map[string]interface{}) error { return nil },
									AssertClaims:                func(claimValidators []claims.SessionClaimValidator) error { return nil },
									FetchAndSetClaim:            func(claim *claims.TypeSessionClaim) error { return nil },
									SetClaimValue:               func(claim *claims.TypeSessionClaim, value interface{}) error { return nil },
									GetClaimValue:               func(claim *claims.TypeSessionClaim) interface{} { return nil },
									RemoveClaim:                 func(claim *claims.TypeSessionClaim) error { return nil },
									AttachToRequestResponse:     func(info sessmodels.RequestResponseInfo) error { return nil },
								}, nil // this is an empty session. It won't result in a session being created for the user.
							}

							return ogCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext)
						}

						return originalImplementation
					},
				},
			}),
		},
	})
}
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe.session.interfaces import (
    RecipeInterface,
    SessionClaimValidator,
    SessionClaim,
    GetSessionTokensDangerouslyDict,
)
from supertokens_python.recipe.session.recipe_implementation import RecipeImplementation
from typing import Dict, Any, Union, List, TypeVar, Optional
from supertokens_python.recipe import session
from supertokens_python.framework import BaseRequest
from supertokens_python.recipe.session.utils import TokenTransferMethod
from supertokens_python.types import RecipeUserId

_T = TypeVar("_T")


def override_session_functions(original_implementation: RecipeInterface):
    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],
    ):
        if user_context["isSignUp"] is True:
            # The execution will come here only in case
            # a sign up API is calling this function. This is because
            # only then will the user_context["isSignUp"] === true
            # (see above code).
            return EmptySession(original_implementation)

        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)
        )
    ],
)


class EmptySession(session.SessionContainer):
    def __init__(self, recipe_implementation: RecipeInterface):
        assert isinstance(recipe_implementation, RecipeImplementation)
        super().__init__(
            recipe_implementation,
            recipe_implementation.config,
            "",
            "",
            None,
            "",
            "",
            "",
            RecipeUserId(""),
            {},
            None,
            False,
            "",
        )

    async def revoke_session(self, user_context: Union[Any, None] = None) -> None:
        pass

    async def get_session_data_from_database(
        self, user_context: Union[Dict[str, Any], None] = None
    ) -> Dict[str, Any]:
        return {}

    async def update_session_data_in_database(
        self,
        new_session_data: Dict[str, Any],
        user_context: Union[Dict[str, Any], None] = None,
    ) -> None:
        pass

    def get_user_id(self, user_context: Union[Dict[str, Any], None] = None) -> str:
        return ""

    def get_recipe_user_id(
        self, user_context: Union[Dict[str, Any], None] = None
    ) -> RecipeUserId:
        return RecipeUserId("")

    def get_access_token_payload(
        self, user_context: Union[Dict[str, Any], None] = None
    ) -> Dict[str, Any]:
        return {}

    def get_handle(self, user_context: Union[Dict[str, Any], None] = None) -> str:
        return ""

    def get_access_token(self, user_context: Union[Dict[str, Any], None] = None) -> str:
        return ""

    async def get_time_created(
        self, user_context: Union[Dict[str, Any], None] = None
    ) -> int:
        return -1

    async def get_expiry(self, user_context: Union[Dict[str, Any], None] = None) -> int:
        return -1

    async def attach_to_request_response(
        self,
        request: BaseRequest,
        transfer_method: TokenTransferMethod,
        user_context: Union[Dict[str, Any], None] = None,
    ):
        pass

    async def assert_claims(
        self,
        claim_validators: List[SessionClaimValidator],
        user_context: Union[Dict[str, Any], None] = None,
    ) -> None:
        pass

    async def fetch_and_set_claim(
        self, claim: SessionClaim[Any], user_context: Union[Dict[str, Any], None] = None
    ) -> None:
        pass

    async def set_claim_value(
        self,
        claim: SessionClaim[_T],
        value: _T,
        user_context: Union[Dict[str, Any], None] = None,
    ) -> None:
        pass

    async def get_claim_value(
        self, claim: SessionClaim[_T], user_context: Union[Dict[str, Any], None] = None
    ) -> Union[_T, None]:
        pass

    async def remove_claim(
        self,
        claim: SessionClaim[Any],
        user_context: Union[Dict[str, Any], None] = None,
    ) -> None:
        pass

    async def merge_into_access_token_payload(
        self,
        access_token_payload_update: Dict[str, Any],
        user_context: Union[Dict[str, Any], None] = None,
    ) -> None:
        pass

    def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict:
        return {
            "accessAndFrontTokenUpdated": False,
            "accessToken": "",
            "antiCsrfToken": None,
            "frontToken": "",
            "refreshToken": None,
        }

    def get_tenant_id(self, user_context: Union[Dict[str, Any], None] = None) -> str:
        return ""

As a summary, when the sign up API invokes, the initial value of userContext is an empty object. Change that user context to add the isSignUp field, ensuring that information communicates to the createNewSession function.

When that function invokes, it checks if isSignUp === true, and if it is, it doesn’t call the original implementation, and instead, returns an empty session. This way, the system does not create a session if the user is signing up, but it creates one if the user is signing in.

Note that there are other ways of achieving this, but the above showcases how user context can use to communicate across recipes and across API & Recipe functions.

API reference

API schema and response details