User Context
important
This feature is only available for SDKs versions:
- NodeJS >=
v9.0
- Python >=
v0.5
- GoLang >=
v0.5
#
How does it work?This is a powerful concept that allows you to pass information across recipe and / or API functions so that customisations can be made based on a specific "execution context".
For example, you may want to disable creation of a session during sign up so that the user has to login again post sign up. In order to do that, the createNewSession
recipe function (from the Session recipe) will have to know that it's being called from the sign up API and return an empty session (which is equal to no session). This is as opposed to it being called from the sign in API, in which it should continue with normal functionalty.
In order 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, you can add anything in this object and that information is carried onto the next set of functions being called in the API
#
Example useLet's take the example mentioned above and implement it in the context of this recipe. First, we override the sign up recipe function to add information into the context indicating that it's a sign up call:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
supertokens: {
connectionURI: "...",
},
recipeList: [
Passwordless.init({
contactMethod: "...",
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
consumeCode: async function (input) {
let resp = await originalImplementation.consumeCode(input);
if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1 && input.session === undefined) {
/*
* This is called during the consume code API,
* 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.
*/
/*
* by default, the userContext Dict is {},
* we change it to {isSignUp: true}, since this is called in the
* sign up API, and this will tell the create_new_session function
* (which will be called next)
* to not create a new session in case input.userContext.isSignUp == true
*/
input.userContext.isSignUp = true;
}
return resp;
},
}
},
}
})
]
});
import (
"github.com/supertokens/supertokens-golang/recipe/passwordless"
"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
passwordless.Init(plessmodels.TypeInput{
Override: &plessmodels.OverrideStruct{
Functions: func(originalImplementation plessmodels.RecipeInterface) plessmodels.RecipeInterface {
ogConsumeCode := *originalImplementation.ConsumeCode
(*originalImplementation.ConsumeCode) = func(userInput *plessmodels.UserInputCodeWithDeviceID, linkCode *string, preAuthSessionID string, tenantId string, userContext supertokens.UserContext) (plessmodels.ConsumeCodeResponse, error) {
resp, err := ogConsumeCode(userInput, linkCode, preAuthSessionID, tenantId, userContext)
if err != nil {
return plessmodels.ConsumeCodeResponse{}, err
}
if resp.OK != nil && resp.OK.CreatedNewUser {
/*
* This is called during the consume code API,
* 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.
*/
/*
* by default, the userContext Dict is {},
* we change it to {isSignUp: true}, since this is called in the
* sign up API, and this will tell the create_new_session function
* (which will be called next)
* to not create a new session in case (*userContext)["isSignUp"] == True
*/
(*userContext)["isSignUp"] = true
}
return resp, nil
}
return originalImplementation
},
},
}),
},
})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import passwordless
from supertokens_python.recipe.passwordless.interfaces import RecipeInterface, ConsumeCodeOkResult
from typing import Dict, Any, Union
def override_passwordless_functions(original_implementation: RecipeInterface):
original_consume_code = original_implementation.consume_code
async def consume_code(pre_auth_session_id: str,
user_input_code: Union[str, None],
device_id: Union[str, None],
link_code: Union[str, None],
tenant_id: str,
user_context: Dict[str, Any]):
response = await original_consume_code(pre_auth_session_id, user_input_code, device_id, link_code, tenant_id, user_context)
if isinstance(response, ConsumeCodeOkResult):
# This is called during the consume_code API ,
# 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.
# by default, the userContext Dict is {},
# we change it to {isSignUp: true}, since this is called in the
# sign up API, and this will tell the create_new_session function
# (which will be called next)
# to not create a new session in case userContext["isSignUp"] is True
if response.created_new_user:
user_context["isSignUp"] = True
return response
original_implementation.consume_code = consume_code
return original_implementation
init(
app_info=InputAppInfo(
api_domain="...", app_name="...", website_domain="..."),
framework='...',
recipe_list=[
passwordless.init(
contact_config=...,
flow_type="...",
override=passwordless.InputOverrideConfig(
functions=override_passwordless_functions
)
)
]
)
Then we consume that context in the createNewSession
function to return an empty function in case the userContext.isSignUp
is true
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import SuperTokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
supertokens: {
connectionURI: "...",
},
recipeList: [
Passwordless.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/passwordless"
"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
"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{
passwordless.Init(plessmodels.TypeInput{ /* See previous step... */ }),
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
from supertokens_python.recipe import passwordless
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
_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,
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, 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(
app_info=InputAppInfo(
api_domain="...", app_name="...", website_domain="..."),
framework='...',
recipe_list=[
passwordless.init(
# see previous step...
),
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, "", "", "", {}, 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_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 is called, the initial value of userContext
is an empty object. We change that user context to add the isSignUp
field so that that information can be communicated to the createNewSession
function.
When that is called, that function checks if isSignUp === true
, and if it is, it doesn't call the original implementation, and instead, just returns an empty session. This way, we don't create a session if the user is signing up, but we do create one if the user is signing in.
#
Default information in the user context objectBy default the user context passed to APIs and functions contains the request object that can be used to read custom headers, body/query params etc.
To learn more on how you can use the default user context to consume custom request information visit this page.