# SuperTokens Docs > Open Source User Authentication # Introduction Source: https://supertokens.com/docs SuperTokens is an open-source authentication provider that gives you control over your user data and login experience. Use the managed service or self-host the SuperTokens Core, then integrate authentication through frontend and backend SDKs. Integrate SuperTokens into an existing application. Replace an existing authentication provider with SuperTokens. ## How SuperTokens works The **SuperTokens Core** provides the authentication functionality. Your backend SDK communicates with the Core to perform authentication tasks, while the frontend SDK sends requests to authentication routes exposed by your backend. Unlike many authentication providers, the frontend SDK never communicates with the authentication service directly. This lets your application control the API layer, customize authentication behavior, and keep the Core inside your infrastructure when self-hosting. **Managed service** Flowchart of the SuperTokens managed service architecture **Self-hosted** Flowchart of the self-hosted SuperTokens architecture :::caution The **SuperTokens Core** is a trusted backend component. It should only be reachable by your backend, never exposed directly to the public internet or your frontend. When self-hosting, see [Secure the core](/deployment/self-host-supertokens#secure-the-core). ::: ### Recipes SuperTokens groups functionalities, like authentication methods or session and user management actions, into **recipes**. Each can be used as extendable building blocks which allow you to customize the authentication experience based on your needs. ## Explore capabilities Let users sign up and sign in with an email and password, with customizable forms and password reset flows. Authenticate users with one-time passwords or magic links sent by email or SMS. Let users sign in with Google, Apple, GitHub, and other identity providers. Offer phishing-resistant sign-in using biometrics, device PINs, or security keys. Authenticate users across web, mobile, and desktop applications through a common OAuth2 provider. Secure service-to-service requests with access tokens using the OAuth2 client credentials flow. Add email or SMS OTP, TOTP, or passkeys as factors, with step-up authentication for sensitive actions. Detect risky authentication activity and respond with extra verification or blocked attempts. Configure tenant-specific login methods, isolated user pools, and enterprise SSO. ## Next steps Configure the sign-in methods that fit your application. Protect routes, manage sessions, and work with authenticated users. Give an AI coding agent the context it needs to integrate SuperTokens. Use the managed service or deploy the SuperTokens Core yourself. --- # Initial setup Source: https://supertokens.com/docs/additional-verification/attack-protection-suite/initial-setup Add SuperTokens Attack Protection Suite to this application. First confirm that the feature is enabled for the environment and that the application uses email/password or passwordless authentication. Configure the public API key, secret API key, environment ID, frontend request IDs, and backend overrides using environment variables. Do not expose the secret key or commit credentials. Preserve existing authentication behavior and validate request ID propagation, brute-force protection, bot detection, and failure handling. ## Overview The following page shows you how to include the **Attack Protection Suite** feature in your **SuperTokens** integration. ## Before you start This feature is **in beta**. To get access to it, please [reach out](mailto:support@supertokens.com) to get it set up for you. Once you have access to it, you receive: - **Public API key** - use this on your frontend for generating request IDs - **Secret API key** - use this on your backend for making requests to the anomaly detection API - **Environment ID** - use this for identifying the environment you are using both on the backend and the frontend You can use the feature with either the `Email Password` or the `Passwordless` authentication methods. For social or enterprise login, it is not needed for multiple reasons: - **Existing anomaly detection**: Most reputable third-party authentication providers (like Google, Facebook, Apple, etc.) have robust security measures in place, including their own anomaly detection systems. These systems are typically more comprehensive and tailored to their specific platforms. - **Limited visibility**: When using third-party authentication, you have limited visibility into the authentication process. This makes it difficult to accurately detect anomalies or suspicious activities that occur on the third-party's side. - **Potential false positives**: Applying anomaly detection to third-party logins might lead to an increase in false positives, as you don't have full context of the user's interactions with the third-party provider. - **User experience**: Additional security checks on top of third-party authentication could negatively impact the user experience, defeating the purpose of offering third-party login as a convenient option. ## Steps ### 1. Attach request IDs to backend API calls The **Attack Protection Suite** feature relies on identifying each request through a unique ID. This way the fingerprinting process can determine if it's a potential threat or not. :::info[Important] This step applies only to bot detection and anomaly IP-based detection such as impossible travel detection. Also, check for bot detection only on the email password login flows. ::: #### 1.1 Generate a request ID To generate a request ID, import, and initialize the SDK using your public API key. This SDK generates a unique request ID for each authentication event attempt. ```tsx check=false reason="browser example imports the remote request-ID SDK from its deployment URL" const ENVIRONMENT_ID = ""; // Your environment ID that you received from the SuperTokens team // Initialize the agent on page load using your public API key that you received from the SuperTokens team. const supertokensRequestIdPromise = import( "https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s?apiKey=" ).then((RequestId: any) => RequestId.load({ endpoint: ["https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2", RequestId.defaultEndpoint], }), ); async function getRequestId() { const sdk = await supertokensRequestIdPromise; const result = await sdk.get({ tag: { environmentId: ENVIRONMENT_ID, }, }); return result.requestId; } ``` #### 1.2 Pass the request ID to the backend Include the `requestId` property along with the value as part of the `preAPIHook` body from the initialisation of the recipes. :::info[Important] If the request ID is not passed to the backend, the anomaly detection can only detect password breaches and brute force attacks. ::: Below is a full example of how to configure the SDK and pass the request ID to the backend. The request ID generates only for the email password sign in, sign up, and reset password actions because these are the only actions that require bot detection. For all the other recipes, this is not needed. ```tsx check=false reason="browser example imports the remote request-ID SDK from its deployment URL" import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; const ENVIRONMENT_ID = ""; // Your environment ID that you received from the SuperTokens team // Initialize the agent on page load using your public API key that you received from the SuperTokens team. const supertokensRequestIdPromise = import( "https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s?apiKey=" ).then((RequestId: any) => RequestId.load({ endpoint: ["https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2", RequestId.defaultEndpoint], }), ); async function getRequestId() { const sdk = await supertokensRequestIdPromise; const result = await sdk.get({ tag: { environmentId: ENVIRONMENT_ID, }, }); return result.requestId; } export const SuperTokensConfig = { // ... other config options appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // recipeList contains all the modules that you want to // use from SuperTokens. See the full list here: https://supertokens.com/docs/authentication/overview recipeList: [ EmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if ( action === "EMAIL_PASSWORD_SIGN_IN" || action === "EMAIL_PASSWORD_SIGN_UP" || action === "SEND_RESET_PASSWORD_EMAIL" ) { let requestId = await getRequestId(); let body = context.requestInit.body; if (body !== undefined) { let bodyJson = JSON.parse(body as string); bodyJson.requestId = requestId; requestInit.body = JSON.stringify(bodyJson); } } return { requestInit, url, }; }, }), ], }; ``` ### 2. Retrieve the request ID To retrieve the request ID in the backend you have to override the recipe implementations. #### Email and password ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import axios from "axios"; import { createHash } from "crypto"; function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = (userIdentifier: string, ip: string, prefix?: string) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()).requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0].value as string; let password = input.formFields.filter((f) => f.id === "password")[0].value as string; const bruteForceConfig = getBruteForceConfig(email, ip, actionType); return originalImplementation.signUpPOST!(input); }, signInPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()).requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0].value as string; let password = input.formFields.filter((f) => f.id === "password")[0].value as string; const bruteForceConfig = getBruteForceConfig(email, ip, actionType); return originalImplementation.signInPOST!(input); }, generatePasswordResetTokenPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()).requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0].value as string; let password = input.formFields.filter((f) => f.id === "password")[0].value as string; const bruteForceConfig = getBruteForceConfig(email, ip, actionType); return originalImplementation.generatePasswordResetTokenPOST!(input); }, }; }, }, }), ], }); ``` ```go import ( "fmt" "encoding/json" "net/http" "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) type BruteForceConfig struct { Key string `json:"key"` MaxRequests []MaxRequests `json:"maxRequests"` } type MaxRequests struct { Limit int `json:"limit"` PerTimeIntervalMS int `json:"perTimeIntervalMS"` } type ReqBody struct { RequestID *string `json:"requestId"` } func getIpFromRequest(req *http.Request) string { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // rewrite the original implementation of SignUpPOST originalSignUpPOST := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID fmt.Println(requestId) actionType := "emailpassword-sign-up" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } fmt.Println(password) bruteForceConfig := getBruteForceConfig(email, ip, actionType) fmt.Println(bruteForceConfig) // pre API logic... resp, err := originalSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of SignInPOST originalSignInPOST := *originalImplementation.SignInPOST (*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignInPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignInPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID fmt.Println(requestId) actionType := "emailpassword-sign-in" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } fmt.Println(password) bruteForceConfig := getBruteForceConfig(email, ip, actionType) fmt.Println(bruteForceConfig) // pre API logic... resp, err := originalSignInPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignInPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of GeneratePasswordResetTokenPOST originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID fmt.Println(requestId) actionType := "send-password-reset-email" ip := getIpFromRequest(options.Req) email := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = valueAsString } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) fmt.Println(bruteForceConfig) // pre API logic... resp, err := originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } return resp, nil } return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import APIInterface, APIOptions from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.framework import BaseRequest from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] def override_email_password_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], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) print(brute_force_config) response = await original_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_up_post = sign_up_post original_sign_in_post = original_implementation.sign_in_post async def sign_in_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], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) print(brute_force_config) response = await original_sign_in_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_in_post = sign_in_post original_generate_password_reset_token_post = ( original_implementation.generate_password_reset_token_post ) async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "send-password-reset-email" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) print(brute_force_config) response = await original_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) return response original_implementation.generate_password_reset_token_post = ( generate_password_reset_token_post ) return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) ) ], ) ``` #### Passwordless ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import axios from "axios"; function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = (userIdentifier: string, ip: string, prefix?: string) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; SuperTokens.init({ framework: "express", appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ Passwordless.init({ // ... other customisations ... contactMethod: "EMAIL_OR_PHONE", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); const emailOrPhoneNumber = "email" in input ? input.email : input.phoneNumber; const bruteForceConfig = getBruteForceConfig(emailOrPhoneNumber, ip, actionType); return originalImplementation.createCodePOST!(input); }, resendCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); let codesInfo = await Passwordless.listCodesByPreAuthSessionId({ tenantId: input.tenantId, preAuthSessionId: input.preAuthSessionId, }); const phoneNumber = codesInfo && "phoneNumber" in codesInfo ? codesInfo.phoneNumber : undefined; const email = codesInfo && "email" in codesInfo ? codesInfo.email : undefined; const userIdentifier = email || phoneNumber || input.deviceId; const bruteForceConfig = getBruteForceConfig(userIdentifier, ip, actionType); return originalImplementation.resendCodePOST!(input); }, }; }, }, }), ], }); ``` ```go import ( "net/http" "fmt" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) type BruteForceConfig struct { Key string `json:"key"` MaxRequests []MaxRequests `json:"maxRequests"` } type MaxRequests struct { Limit int `json:"limit"` PerTimeIntervalMS int `json:"perTimeIntervalMS"` } func getIpFromRequest(req *http.Request) string { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE", ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{ Enabled: true, }, Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email *string, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) var key string if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) fmt.Println(bruteForceConfig) return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } originalResendCodePOST := *originalImplementation.ResendCodePOST (*originalImplementation.ResendCodePOST) = func(deviceID string, preAuthSessionID string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.ResendCodePOSTResponse, error) { // retreive user details codesInfo, err := passwordless.ListCodesByDeviceID(tenantId, deviceID, userContext) if err != nil { return plessmodels.ResendCodePOSTResponse{}, err } var email *string var phoneNumber *string if codesInfo.Email != nil { email = codesInfo.Email } if codesInfo.PhoneNumber != nil { phoneNumber = codesInfo.PhoneNumber } actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) key := "" if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) fmt.Println(bruteForceConfig) return originalResendCodePOST(deviceID, preAuthSessionID, tenantId, options, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from supertokens_python.recipe.passwordless.asyncio import list_codes_by_device_id from supertokens_python.framework import BaseRequest from supertokens_python.recipe.session import SessionContainer def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) print(brute_force_config) # We need to call the original implementation of create_code_post. response = await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.create_code_post = create_code_post original_resend_code_post = original_implementation.resend_code_post async def resend_code_post( device_id: str, pre_auth_session_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) email = None phone_number = None codes = await list_codes_by_device_id( tenant_id=tenant_id, device_id=device_id, user_context=user_context ) if codes is not None: email = codes.email phone_number = codes.phone_number identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) print(brute_force_config) # We need to call the original implementation of resend_code_post. response = await original_resend_code_post( device_id, pre_auth_session_id, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.resend_code_post = resend_code_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ passwordless.init( flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", contact_config=passwordless.ContactEmailOrPhoneConfig(), override=passwordless.InputOverrideConfig(apis=override_passwordless_apis), ) ], ) ``` ### 3. Call the protection service To use the service, send requests to the appropriate regional endpoint based on your location: - **US Region (N. Virginia)**: `https://security-us-east-1.aws.supertokens.io/v1/security` - **EU Region (Ireland)**: `https://security-eu-west-1.aws.supertokens.io/v1/security` - **APAC Region (Singapore)**: `https://security-ap-southeast-1.aws.SuperTokens.io/v1/security` **Examples** ```bash curl --location --request POST 'https://security-us-east-1.aws.supertokens.io/v1/security' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "user@email.com", "phoneNumber": "+1234567890", "passwordHash": "9cf95dacd226dcf43da376cdb6cbba7035218920", "requestId": "some-request-id", "actionType": "emailpassword-sign-in", "bruteForce": [ { "key": "some-key", "maxRequests": [ { "limit": 1, "perTimeIntervalMS": 1000 } ] } ] }' ``` ```tsx const REGION = "us-east-1"; // or "eu-west-1" or "ap-southeast-1" const SECRET_API_KEY = ""; const url = `https://security-${REGION}.aws.supertokens.io/v1/security`; const payload = { email: "user@email.com", phoneNumber: "+1234567890", passwordHashPrefix: "9cf95dacd226dcf43da376cdb6cbba7035218920", requestId: "some-request-id", actionType: "emailpassword-sign-in", bruteForce: [ { key: "some-key", maxRequests: [ { limit: 1, perTimeIntervalMS: 1000, }, ], }, ], }; fetch(url, { method: "POST", headers: { Authorization: "Bearer " + SECRET_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify(payload), }) .then((response) => response.json()) .then((json) => console.log(json)) .catch((err) => console.error(err)); ``` ```python import requests REGION = "us-east-1" # or "eu-west-1" or "ap-southeast-1" SECRET_API_KEY = "" url = f"https://security-{REGION}.aws.supertokens.io/v1/security" payload = { "email": "user@email.com", "phoneNumber": "+1234567890", "passwordHash": "9cf95dacd226dcf43da376cdb6cbba7035218920", "requestId": "some-request-id", "actionType": "emailpassword-sign-in", "bruteForce": [ { "key": "some-key", "maxRequests": [ { "limit": 1, "perTimeIntervalMS": 1000 } ] } ] } headers = { "Authorization": f"Bearer {SECRET_API_KEY}", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` ```go import ( "bytes" "encoding/json" "fmt" "net/http" "io/ioutil" ) func main() { region := "us-east-1" // or "eu-west-1" or "ap-southeast-1" secretApiKey := "" url := fmt.Sprintf("https://security-%s.aws.supertokens.io/v1/security", region) payload := map[string]interface{}{ "email": "user@email.com", "phoneNumber": "+1234567890", "passwordHash": "9cf95dacd226dcf43da376cdb6cbba7035218920", "requestId": "some-request-id", "actionType": "emailpassword-sign-in", "bruteForce": []map[string]interface{}{ { "key": "some-key", "maxRequests": []map[string]interface{}{ { "limit": 1, "perTimeIntervalMS": 1000, }, }, }, }, } jsonPayload, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", secretApiKey)) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) } ``` **Details** Checks for suspicious activity and potential security threats during authentication events. **Authorization**: Set the `Authorization` header to `Bearer ` where `` is your SuperTokens Attack Protection Suite API key. ## Request ### Body Schema | Name | Type | Description | Required | | ------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | | `email` | `string` | User's email address. If omitted (along with `phoneNumber`), the system skips impossible travel detection, new device detection, and device count detection. | No | | `phoneNumber` | `string` | User's phone number. If omitted (along with email), the system skips impossible travel detection, new device detection, and device count detection. | No | | `passwordHash` | `string` | First 5 characters of the `SHA-1` hash of the password. If omitted, the system skips the password breach check. | No | | `requestId` | `string` | Frontend-generated request ID. If omitted, the system skips bot detection, impossible travel detection, new device detection, device count detection, and request ID info. | No | | `actionType` | `ActionType` | Type of authentication action performed | No | | `bruteForce` | `array` of `BruteForceCheck` | Configuration for brute force detection | No | #### Action type Action type is a string that can have one of the following values: - `emailpassword-sign-in`: Email password sign in attempt - `emailpassword-sign-up`: Email password sign up attempt - `send-password-reset-email`: Password reset email request - `passwordless-send-email`: Passwordless email code/link - `passwordless-send-sms`: Passwordless SMS code - `totp-verify-device`: TOTP device verification - `totp-verify-totp`: TOTP code verification - `thirdparty-login`: Third-party provider login - `emailverification-send-email`: Email verification request #### Brute force check | Name | Type | Description | Required | | ----------- | ----------------------- | ------------------------------------------------------------------ | -------- | | `key` | `string` | Unique identifier for rate limiting (for example, email, phone number, IP) | Yes | | `maxRequests` | `array` of `MaxRequest` | Rate limit rules | Yes | #### Max request | Name | Type | Description | Required | | ----------------- | -------- | ---------------------------------- | -------- | | `limit` | `number` | Maximum number of requests allowed | Yes | | `perTimeIntervalMS` | `number` | Time window in milliseconds | Yes | ### Example ```bash curl --location --request POST 'https://security-us-east-1.aws.supertokens.io/v1/security' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "user@email.com", "phoneNumber": "+1234567890", "passwordHash": "9cf95dacd226dcf43da376cdb6cbba7035218920", "requestId": "some-request-id", "actionType": "emailpassword-sign-in", "bruteForce": [ { "key": "some-key", "maxRequests": [ { "limit": 1, "perTimeIntervalMS": 1000 } ] } ] }' ``` Additional examples that show specific use cases: ```tsx const userIp = "127.0.0.1"; // this should be the user's IP address const userEmail = "user@email.com"; // this should be the user's email // Useful for limiting a user's attempt fom the same network // This is the most common use case // --- // This does two check: // 1. 1 request per second - fast rate of requests // 2. 100 requests per 60 minutes - slow brute force - some attackers might try sidestepping the regular brute force detection by using a slower rate of requests const checkUserInSameNetwork = [ { key: `${userIp}-${userEmail}`, maxRequests: [ { limit: 1, perTimeIntervalMS: 1000, }, { limit: 100, perTimeIntervalMS: 60 * 1000 * 60, }, ], }, ]; ``` ```tsx const userIp = "127.0.0.1"; // this should be the user's IP address // Useful for limiting requests from the same network // This should usually have a higher number of requests/time interval allowed const checkNetwork = [ { key: `${userIp}`, maxRequests: [ { limit: 100, perTimeIntervalMS: 1000, }, ], }, ]; ``` ```tsx const userEmail = "user@email.com"; // this should be the user's email // Useful for limiting requests for the user only const checkUserOnly = [ { key: `${userEmail}`, maxRequests: [ { limit: 1, perTimeIntervalMS: 1000, }, ], }, ]; ``` ```tsx const userIp = "127.0.0.1"; // this should be the user's IP address const userEmail = "user@email.com"; // this should be the user's email // Checking by multiple keys at once const checkUserOnly = [ { key: `${userEmail}-${userIp}`, maxRequests: [ { limit: 1, perTimeIntervalMS: 1000, }, { limit: 100, perTimeIntervalMS: 60 * 1000 * 60, }, ], }, { key: `${userIp}`, maxRequests: [ { limit: 100, perTimeIntervalMS: 1000, }, ], }, ]; ``` ## Response ### 200 Returns the anomaly detection results. #### Response schema | Name | Type | Description | | ---------------------------- | --------- | -------------------------------------------------------------------------- | | `id` | `string` | Unique identifier for the anomaly detection check | | `bruteForce` | `object` | Brute force detection results | | `emailRisk` | `object` | Email risk assessment results | | `phoneNumberRisk` | `object` | Phone number risk assessment results | | `passwordBreaches` | `object` | Password breach check results | | `isNewDevice` | `boolean` | Whether this is a new device for the user | | `isImpossibleTravel` | `boolean` | Whether the system detects impossible travel | | `numberOfUniqueDevicesForUser` | `number` | Number of unique devices used by the user | | `requestIdInfo` | `object` | Information about the request including Virtual Private Network (VPN) detection, bot detection, etc. | #### Example ```ts const response = { id: "0191bc35-d527-7bbd-88df-1e7669e82cc0", // the id of the anomaly detection check bruteForce: { detected: true, key: "some-key", // this will be present only if brute force has been detected and the value will be the key for which the brute force detection has been detected }, emailRisk: null, phoneNumberRisk: null, passwordBreaches: { c1d808e04732adf679965ccc34ca7ae3441: "120", // the suffix of the password hash and the number of times it has been breached "7acba4f54f55aafc33bb06bbbf6ca803e9a": "399", // the suffix of the password hash and the number of times it has been breached }, // can be null if the password hash is not provided isNewDevice: false, // can be null if the email or phone number is not provided isImpossibleTravel: false, // can be null if the email or phone number is not provided numberOfUniqueDevicesForUser: 1, // can be null if the email or phone number is not provided /* All the values below can be null based on the request ID provided and what has been detected */ requestIdInfo: { // can be null if the request ID is not provided vpn: { result: true, // this is true if the user is using a VPN methods: { publicVPN: true, // this is true if the user is using a public VPN osMismatch: false, auxiliaryMobile: false, timezoneMismatch: true, }, originCountry: "unknown", originTimezone: "Europe/Bucharest", }, frida: false, proxy: false, // this is true if the user is using a proxy valid: true, ipInfo: { v4: { asn: { asn: "16509", name: "AMAZON-02", network: "127.0.0.1/13" }, address: "127.0.0.1", datacenter: { name: "Amazon AWS", result: true }, geolocation: { city: { name: "Frankfurt am Main" }, country: { code: "DE", name: "Germany" }, latitude: 51.1187, timezone: "Europe/Berlin", continent: { code: "EU", name: "Europe" }, longitude: 9.6842, postalCode: "12345", subdivisions: [{ name: "Hesse", isoCode: "HE" }], accuracyRadius: 200, }, }, v6: null, // contains same information as v4 if the user is using IPv6 }, velocity: { events: { intervals: { "1h": 3, "5m": 3, "24h": 5 } }, distinctIp: { intervals: { "1h": 1, "5m": 1, "24h": 1 } }, distinctCountry: { intervals: { "1h": 1, "5m": 1, "24h": 1 } }, distinctLinkedId: { intervals: null }, }, clonedApp: false, incognito: false, tampering: { result: false, anomalyScore: 0 }, isEmulator: false, isUsingTor: false, // this is true if the user is using Tor jailbroken: false, botDetected: false, // this is true if the user is a bot ipBlocklist: { result: false, details: { emailSpam: false, attackSource: false }, }, factoryReset: { time: "1970-01-01T00:00:00Z", timestamp: 0 }, highActivity: false, remoteControl: false, identification: { tag: { environmentId: "cddd8855-ff50-4bbe-bb82-62b5057fa4f4" }, // this is the environment ID that you will receive from the SuperTokens team url: "http://example.com/index.html?eid=cddd8855-ff50-4bbe-bb82-62b5057fa4f4", // this is the URL that has been used to generate the request ID linkedId: null, timeInMS: 1723130887458, incognito: false, requestId: "1723130887451.92r32x", // this is the request ID that has been generated on the frontend visitorId: "mEYaqlY67Z55cHgzt37y", confidence: { score: 1 }, browserDetails: { os: "Mac OS X", device: "Other", osVersion: "10.15.7", userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", browserName: "Chrome", browserFullVersion: "127.0.0", browserMajorVersion: "127", }, }, virtualMachine: false, privacySettings: false, locationSpoofing: false, rawDeviceAttributes: { // These are the raw device attributes that are being sent from the frontend // They might vary based on the device and browser that is being used audio: { value: 124.04346607114712 }, fonts: { value: ["Arial Unicode MS", "Gill Sans", "Helvetica Neue", "Menlo"], }, canvas: { value: { Text: "32a115bd05e0f411c5ecd7e285fd36e2", Winding: true, Geometry: "d45e7d71dc99e368affd8a40840c833d", }, }, contrast: { value: 0 }, cpuClass: {}, colorDepth: { value: 124.04346607114712 }, colorGamut: { value: "p3" }, architecture: { value: 127 }, cookiesEnabled: { value: true }, }, }, }; ``` ### 400 The request was invalid. #### Example ```json { "error": "Invalid request body" } ``` ### 401 Invalid or missing API key. #### Example ```json { "error": "Unauthorized" } ``` ### 500 An internal server error occurred. ## Examples Use the following examples for complete code references on how to integrate the feature. ### Email and password ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import axios from "axios"; import { createHash } from "crypto"; const SECRET_API_KEY = ""; // Your secret API key that you received from the SuperTokens team // The full URL with the correct region will be provided by the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security"; async function handleSecurityChecks(input: { actionType?: string; email?: string; phoneNumber?: string; password?: string; requestId?: string; bruteForceConfig?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; }): Promise< | { status: "GENERAL_ERROR"; message: string; } | undefined > { let requestBody: { email?: string; phoneNumber?: string; actionType?: string; requestId?: string; passwordHashPrefix?: string; bruteForce?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; } = {}; if (input.requestId !== undefined) { requestBody.requestId = input.requestId; } let passwordHash: string | undefined; if (input.password !== undefined) { let shasum = createHash("sha1"); shasum.update(input.password); passwordHash = shasum.digest("hex"); requestBody.passwordHashPrefix = passwordHash.slice(0, 5); } requestBody.bruteForce = input.bruteForceConfig; requestBody.email = input.email; requestBody.phoneNumber = input.phoneNumber; requestBody.actionType = input.actionType; let response; try { response = await axios.post(ANOMALY_DETECTION_API_URL, requestBody, { headers: { Authorization: `Bearer ${SECRET_API_KEY}`, "Content-Type": "application/json", }, }); } catch (err) { // silently fail in order to not break the auth flow console.error(err); return; } let responseData = response.data; if (responseData.bruteForce.detected) { return { status: "GENERAL_ERROR", message: "Too many requests. Please try again later.", }; } if (responseData.requestIdInfo?.isUsingTor) { return { status: "GENERAL_ERROR", message: "Tor activity detected. Please use a regular browser.", }; } if (responseData.requestIdInfo?.vpn?.result) { return { status: "GENERAL_ERROR", message: "VPN activity detected. Please use a regular network.", }; } if (responseData.requestIdInfo?.botDetected) { return { status: "GENERAL_ERROR", message: "Bot activity detected.", }; } if (responseData?.passwordBreaches && passwordHash) { const suffix = passwordHash.slice(5).toUpperCase(); const foundPasswordHash = responseData?.passwordBreaches[suffix]; if (foundPasswordHash) { return { status: "GENERAL_ERROR", message: "This password has been detected in a breach. Please set a different password.", }; } } return undefined; } function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = (userIdentifier: string, ip: string, prefix?: string) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; // backend SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()).requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-up"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0].value as string; let password = input.formFields.filter((f) => f.id === "password")[0].value as string; const bruteForceConfig = getBruteForceConfig(email, ip, actionType); // we check the anomaly detection service before calling the original implementation of signUp let securityCheckResponse = await handleSecurityChecks({ requestId, email, password, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.signUpPOST!(input); }, signInPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()).requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "emailpassword-sign-in"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0].value as string; const bruteForceConfig = getBruteForceConfig(email, ip, actionType); // we check the anomaly detection service before calling the original implementation of signIn let securityCheckResponse = await handleSecurityChecks({ requestId, email, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.signInPOST!(input); }, generatePasswordResetTokenPOST: async function (input) { // We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc. const requestId = (await input.options.req.getJSONBody()).requestId; if (!requestId) { return { status: "GENERAL_ERROR", message: "The request ID is required", }; } const actionType = "send-password-reset-email"; const ip = getIpFromRequest(input.options.req.original); let email = input.formFields.filter((f) => f.id === "email")[0].value as string; const bruteForceConfig = getBruteForceConfig(email, ip, actionType); // we check the anomaly detection service before calling the original implementation of generatePasswordResetToken let securityCheckResponse = await handleSecurityChecks({ requestId, email, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.generatePasswordResetTokenPOST!(input); }, passwordResetPOST: async function (input) { let password = input.formFields.filter((f) => f.id === "password")[0].value as string; let securityCheckResponse = await handleSecurityChecks({ password, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.passwordResetPOST!(input); }, }; }, }, }), ], }); ``` ```go import ( "bytes" "crypto/sha1" "encoding/hex" "encoding/json" "net/http" "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) const SECRET_API_KEY = "" // Your secret API key that you received from the SuperTokens team // The full URL with the correct region will be provided by the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" type SecurityCheckInput struct { ActionType string `json:"actionType,omitempty"` Email string `json:"email,omitempty"` PhoneNumber string `json:"phoneNumber,omitempty"` Password string `json:"password,omitempty"` RequestID string `json:"requestId,omitempty"` BruteForceConfig []BruteForceConfig `json:"bruteForceConfig,omitempty"` } type BruteForceConfig struct { Key string `json:"key"` MaxRequests []MaxRequests `json:"maxRequests"` } type MaxRequests struct { Limit int `json:"limit"` PerTimeIntervalMS int `json:"perTimeIntervalMS"` } type ReqBody struct { RequestID *string `json:"requestId"` } func getIpFromRequest(req *http.Request) string { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func handleSecurityChecks(input SecurityCheckInput) (*supertokens.GeneralErrorResponse, error) { requestBody := make(map[string]interface{}) if input.RequestID != "" { requestBody["requestId"] = input.RequestID } var passwordHash string if input.Password != "" { hash := sha1.New() hash.Write([]byte(input.Password)) passwordHash = hex.EncodeToString(hash.Sum(nil)) requestBody["passwordHashPrefix"] = passwordHash[:5] } requestBody["bruteForce"] = input.BruteForceConfig requestBody["email"] = input.Email requestBody["phoneNumber"] = input.PhoneNumber requestBody["actionType"] = input.ActionType jsonBody, err := json.Marshal(requestBody) if err != nil { return nil, err } req, err := http.NewRequest("POST", ANOMALY_DETECTION_API_URL, bytes.NewBuffer(jsonBody)) if err != nil { // silently fail in order to not break the auth flow return nil, nil } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+SECRET_API_KEY) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var responseData map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&responseData) if err != nil { return nil, err } if bruteForce, ok := responseData["bruteForce"].(map[string]interface{}); ok { if detected, ok := bruteForce["detected"].(bool); ok && detected { return &supertokens.GeneralErrorResponse{ Message: "Too many requests. Please try again later.", }, nil } } if requestIdInfo, ok := responseData["requestIdInfo"].(map[string]interface{}); ok { if isUsingTor, ok := requestIdInfo["isUsingTor"].(bool); ok && isUsingTor { return &supertokens.GeneralErrorResponse{ Message: "Tor activity detected. Please use a regular browser.", }, nil } if vpn, ok := requestIdInfo["vpn"].(map[string]interface{}); ok { if result, ok := vpn["result"].(bool); ok && result { return &supertokens.GeneralErrorResponse{ Message: "VPN activity detected. Please use a regular network.", }, nil } } if botDetected, ok := requestIdInfo["botDetected"].(bool); ok && botDetected { return &supertokens.GeneralErrorResponse{ Message: "Bot activity detected.", }, nil } } if passwordBreaches, ok := responseData["passwordBreaches"].(map[string]interface{}); ok { passwordHashSuffix := passwordHash[5:] if _, ok := passwordBreaches[passwordHashSuffix]; ok { return &supertokens.GeneralErrorResponse{ Message: "This password has been detected in a breach. Please set a different password.", }, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // rewrite the original implementation of SignUpPOST originalSignUpPOST := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID actionType := "emailpassword-sign-up" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: email, RequestID: requestId, BruteForceConfig: bruteForceConfig, Password: password, }, ) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if checkErr != nil { return epmodels.SignUpPOSTResponse{ GeneralError: checkErr, }, nil } // pre API logic... resp, err := originalSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of SignInPOST originalSignInPOST := *originalImplementation.SignInPOST (*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.SignInPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.SignInPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID actionType := "emailpassword-sign-in" ip := getIpFromRequest(options.Req) email := "" password := "" for _, field := range formFields { if field.ID == "email" || field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if field.ID == "email" { email = valueAsString } else { password = valueAsString } } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: email, RequestID: requestId, BruteForceConfig: bruteForceConfig, Password: password, }, ) if err != nil { return epmodels.SignInPOSTResponse{}, err } if checkErr != nil { return epmodels.SignInPOSTResponse{ GeneralError: checkErr, }, nil } // pre API logic... resp, err := originalSignInPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignInPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of GeneratePasswordResetTokenPOST originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { // Generate request ID for bot and suspicious IP detection var reqBody ReqBody err := json.NewDecoder(options.Req.Body).Decode(&reqBody) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if reqBody.RequestID == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "The request ID is required", }, }, nil } requestId := *reqBody.RequestID actionType := "send-password-reset-email" ip := getIpFromRequest(options.Req) email := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = valueAsString } } bruteForceConfig := getBruteForceConfig(email, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: email, RequestID: requestId, BruteForceConfig: bruteForceConfig, }, ) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if checkErr != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: checkErr, }, nil } // pre API logic... resp, err := originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } return resp, nil } // rewrite the original implementation of PasswordResetPOST originalPasswordResetPOST := *originalImplementation.PasswordResetPOST (*originalImplementation.PasswordResetPOST) = func(formFields []epmodels.TypeFormField, token string, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.ResetPasswordPOSTResponse, error) { password := "" for _, field := range formFields { if field.ID == "password" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.ResetPasswordPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } password = valueAsString } } // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ Password: password, }, ) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } if checkErr != nil { return epmodels.ResetPasswordPOSTResponse{ GeneralError: checkErr, }, nil } // First we call the original implementation resp, err := originalPasswordResetPOST(formFields, token, tenantId, options, userContext) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } return resp, nil } return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from httpx import AsyncClient from hashlib import sha1 from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import APIInterface, APIOptions from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.framework import BaseRequest from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer SECRET_API_KEY = "" # Your secret API key that you received from the SuperTokens team # The full URL with the correct region will be provided by the SuperTokens team ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" async def handle_security_checks( request_id: Union[str, None], password: Union[str, None], brute_force_config: Union[List[Dict[str, Any]], None], email: Union[str, None], phone_number: Union[str, None], action_type: Union[str, None], ) -> Union[GeneralErrorResponse, None]: request_body: Dict[str, Any] = {} if request_id is not None: request_body["requestId"] = request_id password_hash = None if password is not None: password_hash = sha1(password.encode()).hexdigest() request_body["passwordHashPrefix"] = password_hash[:5] request_body["bruteForce"] = brute_force_config request_body["email"] = email request_body["phoneNumber"] = phone_number request_body["actionType"] = action_type try: async with AsyncClient(timeout=10.0) as client: response = await client.post( ANOMALY_DETECTION_API_URL, json=request_body, headers={ "Authorization": f"Bearer {SECRET_API_KEY}", "Content-Type": "application/json", }, ) response_data = response.json() except: # silently fail in order to not break the auth flow return None if response_data.get("bruteForce", {}).get("detected"): return GeneralErrorResponse( message="Too many requests. Please try again later." ) if response_data.get("requestIdInfo", {}).get("isUsingTor"): return GeneralErrorResponse( message="Tor activity detected. Please use a regular browser." ) if response_data.get("requestIdInfo", {}).get("vpn", {}).get("result"): return GeneralErrorResponse( message="VPN activity detected. Please use a regular network." ) if response_data.get("requestIdInfo", {}).get("botDetected"): return GeneralErrorResponse(message="Bot activity detected.") if response_data.get("passwordBreaches") and password_hash is not None: password_hash_suffix = password_hash[5:] if password_hash_suffix in response_data["passwordBreaches"]: return GeneralErrorResponse( message="This password has been detected in a breach. Please set a different password." ) return None def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] def override_email_password_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], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None password = None for field in form_fields: if field.id == "email": email = field.value if field.id == "password": password = field.value brute_force_config = get_brute_force_config(email, ip, action_type) # we check the anomaly detection service before calling the original implementation of signUp security_check_response = await handle_security_checks( request_id=request_id, password=password, brute_force_config=brute_force_config, email=email, phone_number=None, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of sign_up_post. response = await original_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_up_post = sign_up_post original_sign_in_post = original_implementation.sign_in_post async def sign_in_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], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "emailpassword-sign-in" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) # we check the anomaly detection service before calling the original implementation of sign_in_post security_check_response = await handle_security_checks( request_id=request_id, password=None, brute_force_config=brute_force_config, email=email, phone_number=None, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of sign_in_post. response = await original_sign_in_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) return response original_implementation.sign_in_post = sign_in_post original_generate_password_reset_token_post = ( original_implementation.generate_password_reset_token_post ) async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): request_body = await api_options.request.json() if not request_body: return GeneralErrorResponse(message="The request body is required") request_id = request_body.get("requestId") if not request_id: return GeneralErrorResponse(message="The request ID is required") action_type = "send-password-reset-email" ip = get_ip_from_request(api_options.request) email = None for field in form_fields: if field.id == "email": email = field.value brute_force_config = get_brute_force_config(email, ip, action_type) # we check the anomaly detection service before calling the original implementation of generate_password_reset_token_post security_check_response = await handle_security_checks( request_id=request_id, password=None, brute_force_config=brute_force_config, email=email, phone_number=None, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of generate_password_reset_token_post. response = await original_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) return response original_implementation.generate_password_reset_token_post = ( generate_password_reset_token_post ) original_password_reset_post = original_implementation.password_reset_post async def password_reset_post( form_fields: List[FormField], token: str, tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): password = None for field in form_fields: if field.id == "password": password = field.value # we check the anomaly detection service before calling the original implementation of password_reset_post security_check_response = await handle_security_checks( request_id=None, password=password, brute_force_config=None, email=None, phone_number=None, action_type=None, ) if security_check_response is not None: return security_check_response response = await original_password_reset_post( form_fields, token, tenant_id, api_options, user_context ) return response original_implementation.password_reset_post = password_reset_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) ) ], ) ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - We get the request ID from the request body. This is a unique ID for the request. - Define the action type based on the API you call. - We get the email and password from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - We call the anomaly detection service to check if the request is permissible. - If the request is not allowed, it returns a descriptive error response. - If the request is permissible, it calls the original implementation of the API. - We return the response from the original implementation of the API. ### Passwordless ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import axios from "axios"; import { createHash } from "crypto"; const SECRET_API_KEY = ""; // Your secret API key that you received from the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-us-east-1.aws.supertokens.io/v1/security"; async function handleSecurityChecks(input: { actionType?: string; email?: string; phoneNumber?: string; bruteForceConfig?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; }): Promise< | { status: "GENERAL_ERROR"; message: string; } | undefined > { let requestBody: { email?: string; phoneNumber?: string; actionType?: string; bruteForce?: { key: string; maxRequests: { limit: number; perTimeIntervalMS: number; }[]; }[]; } = {}; requestBody.bruteForce = input.bruteForceConfig; requestBody.email = input.email; requestBody.phoneNumber = input.phoneNumber; requestBody.actionType = input.actionType; let response; try { response = await axios.post(ANOMALY_DETECTION_API_URL, requestBody, { headers: { Authorization: `Bearer ${SECRET_API_KEY}`, "Content-Type": "application/json", }, }); } catch (err) { // silently fail in order to not break the auth flow console.error(err); return; } let responseData = response.data; if (responseData.bruteForce.detected) { return { status: "GENERAL_ERROR", message: "Too many requests. Please try again later.", }; } return undefined; } function getIpFromRequest(req: Request): string { let headers: { [key: string]: string } = {}; for (let key of Object.keys(req.headers)) { headers[key] = (req as any).headers[key]!; } return (req as any).headers["x-forwarded-for"] || "127.0.0.1"; } const getBruteForceConfig = (userIdentifier: string, ip: string, prefix?: string) => [ { key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, { key: `${prefix ? `${prefix}-` : ""}${ip}`, maxRequests: [ { limit: 5, perTimeIntervalMS: 60 * 1000 }, { limit: 15, perTimeIntervalMS: 60 * 60 * 1000 }, ], }, ]; SuperTokens.init({ framework: "express", appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ Passwordless.init({ // ... other customisations ... contactMethod: "EMAIL_OR_PHONE", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); const emailOrPhoneNumber = "email" in input ? input.email : input.phoneNumber; const bruteForceConfig = getBruteForceConfig(emailOrPhoneNumber, ip, actionType); // we check the anomaly detection service before calling the original implementation of createCodePOST let securityCheckResponse = await handleSecurityChecks({ bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.createCodePOST!(input); }, resendCodePOST: async function (input) { const actionType = "passwordless-send-sms"; const ip = getIpFromRequest(input.options.req.original); let codesInfo = await Passwordless.listCodesByPreAuthSessionId({ tenantId: input.tenantId, preAuthSessionId: input.preAuthSessionId, }); const phoneNumber = codesInfo && "phoneNumber" in codesInfo ? codesInfo.phoneNumber : undefined; const email = codesInfo && "email" in codesInfo ? codesInfo.email : undefined; const userIdentifier = email || phoneNumber || input.deviceId; const bruteForceConfig = getBruteForceConfig(userIdentifier, ip, actionType); // we check the anomaly detection service before calling the original implementation of resendCodePOST let securityCheckResponse = await handleSecurityChecks({ phoneNumber, email, bruteForceConfig, actionType, }); if (securityCheckResponse !== undefined) { return securityCheckResponse; } return originalImplementation.resendCodePOST!(input); }, }; }, }, }), ], }); ``` ```go import ( "bytes" "encoding/json" "net/http" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) const SECRET_API_KEY = "" // Your secret API key that you received from the SuperTokens team // The full URL with the correct region will be provided by the SuperTokens team const ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" type SecurityCheckInput struct { ActionType string `json:"actionType,omitempty"` Email string `json:"email,omitempty"` PhoneNumber string `json:"phoneNumber,omitempty"` BruteForceConfig []BruteForceConfig `json:"bruteForceConfig,omitempty"` } type BruteForceConfig struct { Key string `json:"key"` MaxRequests []MaxRequests `json:"maxRequests"` } type MaxRequests struct { Limit int `json:"limit"` PerTimeIntervalMS int `json:"perTimeIntervalMS"` } func getIpFromRequest(req *http.Request) string { if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { return forwardedFor } return "127.0.0.1" } func getBruteForceConfig(userIdentifier string, ip string, prefix string) []BruteForceConfig { var key string if prefix != "" { key = prefix + "-" } return []BruteForceConfig{ { Key: key + userIdentifier, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, { Key: key + ip, MaxRequests: []MaxRequests{ {Limit: 5, PerTimeIntervalMS: 60 * 1000}, {Limit: 15, PerTimeIntervalMS: 60 * 60 * 1000}, }, }, } } func handleSecurityChecks(input SecurityCheckInput) (*supertokens.GeneralErrorResponse, error) { requestBody := make(map[string]interface{}) requestBody["bruteForce"] = input.BruteForceConfig requestBody["email"] = input.Email requestBody["phoneNumber"] = input.PhoneNumber requestBody["actionType"] = input.ActionType jsonBody, err := json.Marshal(requestBody) if err != nil { return nil, err } req, err := http.NewRequest("POST", ANOMALY_DETECTION_API_URL, bytes.NewBuffer(jsonBody)) if err != nil { // silently fail in order to not break the auth flow return nil, nil } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+SECRET_API_KEY) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var responseData map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&responseData) if err != nil { return nil, err } if bruteForce, ok := responseData["bruteForce"].(map[string]interface{}); ok { if detected, ok := bruteForce["detected"].(bool); ok && detected { return &supertokens.GeneralErrorResponse{ Message: "Too many requests. Please try again later.", }, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE", ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{ Enabled: true, }, Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email *string, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) var key string if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: *email, PhoneNumber: *phoneNumber, BruteForceConfig: bruteForceConfig, }, ) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if checkErr != nil { return plessmodels.CreateCodePOSTResponse{ GeneralError: checkErr, }, nil } return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } originalResendCodePOST := *originalImplementation.ResendCodePOST (*originalImplementation.ResendCodePOST) = func(deviceID string, preAuthSessionID string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.ResendCodePOSTResponse, error) { // retreive user details codesInfo, err := passwordless.ListCodesByDeviceID(tenantId, deviceID, userContext) if err != nil { return plessmodels.ResendCodePOSTResponse{}, err } var email *string var phoneNumber *string if codesInfo.Email != nil { email = codesInfo.Email } if codesInfo.PhoneNumber != nil { phoneNumber = codesInfo.PhoneNumber } actionType := "passwordless-send-sms" ip := getIpFromRequest(options.Req) key := "" if email != nil { key = *email } else { key = *phoneNumber } bruteForceConfig := getBruteForceConfig(key, ip, actionType) // Check anomaly detection service before proceeding checkErr, err := handleSecurityChecks( SecurityCheckInput{ ActionType: actionType, Email: *email, PhoneNumber: *phoneNumber, BruteForceConfig: bruteForceConfig, }, ) if err != nil { return plessmodels.ResendCodePOSTResponse{}, err } if checkErr != nil { return plessmodels.ResendCodePOSTResponse{ GeneralError: checkErr, }, nil } return originalResendCodePOST(deviceID, preAuthSessionID, tenantId, options, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from httpx import AsyncClient from typing import Dict, Any, Union, List from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from supertokens_python.recipe.passwordless.asyncio import list_codes_by_device_id from supertokens_python.framework import BaseRequest from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer SECRET_API_KEY = "" # Your secret API key that you received from the SuperTokens team # The full URL with the correct region will be provided by the SuperTokens team ANOMALY_DETECTION_API_URL = "https://security-.aws.supertokens.io/v1/security" async def handle_security_checks( request_id: Union[str, None], password: Union[str, None], brute_force_config: Union[List[Dict[str, Any]], None], email: Union[str, None], phone_number: Union[str, None], action_type: Union[str, None], ) -> Union[GeneralErrorResponse, None]: request_body: Dict[str, Any] = {} request_body["bruteForce"] = brute_force_config request_body["email"] = email request_body["phoneNumber"] = phone_number request_body["actionType"] = action_type try: async with AsyncClient(timeout=10.0) as client: response = await client.post( ANOMALY_DETECTION_API_URL, json=request_body, headers={ "Authorization": f"Bearer {SECRET_API_KEY}", "Content-Type": "application/json", }, ) response_data = response.json() except: # silently fail in order to not break the auth flow return None if response_data.get("bruteForce", {}).get("detected"): return GeneralErrorResponse( message="Too many requests. Please try again later." ) return None def get_ip_from_request(req: BaseRequest) -> str: forwarded_for = req.get_header("x-forwarded-for") if forwarded_for: return forwarded_for return "127.0.0.1" def get_brute_force_config( user_identifier: Union[str, None], ip: str, prefix: Union[str, None] = None ) -> List[Dict[str, Any]]: return [ { "key": f"{prefix}-{user_identifier}" if prefix else user_identifier, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, { "key": f"{prefix}-{ip}" if prefix else ip, "maxRequests": [ {"limit": 5, "perTimeIntervalMS": 60 * 1000}, {"limit": 15, "perTimeIntervalMS": 60 * 60 * 1000}, ], }, ] def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) # we check the anomaly detection service before calling the original implementation of create_code_post security_check_response = await handle_security_checks( request_id=None, password=None, brute_force_config=brute_force_config, email=email, phone_number=phone_number, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of create_code_post. response = await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.create_code_post = create_code_post original_resend_code_post = original_implementation.resend_code_post async def resend_code_post( device_id: str, pre_auth_session_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): action_type = "passwordless-send-sms" ip = get_ip_from_request(api_options.request) email = None phone_number = None codes = await list_codes_by_device_id( tenant_id=tenant_id, device_id=device_id, user_context=user_context ) if codes is not None: email = codes.email phone_number = codes.phone_number identifier = None if email is not None: identifier = email elif phone_number is not None: identifier = phone_number brute_force_config = get_brute_force_config(identifier, ip, action_type) # we check the anomaly detection service before calling the original implementation of resend_code_post security_check_response = await handle_security_checks( request_id=None, password=None, brute_force_config=brute_force_config, email=email, phone_number=phone_number, action_type=action_type, ) if security_check_response is not None: return security_check_response # We need to call the original implementation of resend_code_post. response = await original_resend_code_post( device_id, pre_auth_session_id, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return response original_implementation.resend_code_post = resend_code_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ passwordless.init( flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", contact_config=passwordless.ContactEmailOrPhoneConfig(), override=passwordless.InputOverrideConfig(apis=override_passwordless_apis), ) ], ) ``` The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows: - Define the action type based on the API you call. - We get the email or the phone number from the form fields. - We get the IP address from the request. - We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per: 1. Action and email/phone number. 2. Action and IP address. - The anomaly detection service checks if the request passes the allowed criteria (only brute force detection occurs here). - If the request is not allowed, the system returns a descriptive error response. - If the request passes the allowed criteria, the original implementation of the API executes. - We return the response from the original implementation of the API. --- # Introduction Source: https://supertokens.com/docs/additional-verification/attack-protection-suite/introduction ## Overview The **Attack Protection Suite** feature identifies and prevents suspicious activities in authentication sessions. It leverages detection methods to enhance the security of your application. :::info[Important] This feature is **in beta** and not enabled by default. Check the [quickstart guide](/additional-verification/attack-protection-suite/initial-setup) for instructions on how to set it up. ::: The feature processes requests and provides risk assessments. You can use these assessments to enforce additional security measures, such as requiring two-factor authentication for suspicious logins or blocking high-risk attempts altogether. Check the next diagram for a high-level overview of how the feature's flow works. Attack Protection Suite flow ### Features - **Brute Force Attack Detection**: Watches how many times someone tries to do a specific action (such as logging in, resetting password, etc.) within a certain time. If there are too many attempts, it stops them to prevent bad actors from compromising accounts. - **Password Breach Detection**: Checks passwords against a database of leaked passwords to see if they have leaked before. This helps keep accounts safe by avoiding weak passwords. - **Impossible Travel Detection**: Identifies fraudulent login attempts by detecting geographically impossible travel between user sessions in a short time. - **Bot Detection**: Identifies and prevents automated scripts or bots from performing malicious activities such as credential stuffing, account takeover attempts, or scraping sensitive data. It uses advanced algorithms to analyze user behavior, request patterns, and other indicators to distinguish between human users and automated bots. - **Suspicious IP Detection**: Detects suspicious IP addresses known for malicious activities. This includes detecting the use of VPNs, Tor, proxy servers, or other network configurations that may hide the user's true location or identity. - **New Device Detection**: Recognizes when a user logs in from a new, previously unseen device. This helps find possible unauthorized logins. - **Device Count Tracking**: Monitors the number of unique devices associated with a user account. This helps spot unusual account use. - **Requester Detection**: Recognize and remember devices and requester details, even when they try to disguise themselves. This helps track and identify the same device or requester across multiple login attempts, improving security and user recognition. ## Getting started To learn how to use the feature in your application open the [quickstart guide](/additional-verification/attack-protection-suite/initial-setup). :::info[Use the feature only with either the `Email Password` or `Passwordless` authentication recipes.] For social or enterprise login, it is not needed. ::: --- # CAPTCHA Source: https://supertokens.com/docs/additional-verification/captcha ## Overview This following tutorial shows you how to add CAPTCHA validation to your authentication flows. The guide makes use of the plugins functionality. A new abstraction layer aimed to simplify how you can add new features in your **SuperTokens** integration. ## Before you start The plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. You can use the plugin with the following CAPTCHA providers: - [Google reCAPTCHA v2](https://developers.google.com/recaptcha/docs/display) - [Google reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) - [Cloudflare Turnstile](https://www.cloudflare.com/en-gb/application-services/products/turnstile) Make sure to have the appropriate provider keys before starting the tutorial. The implementation is in early stages and APIs might change. For more information on how plugins work refer to the [references page](/references/plugins/introduction). ## Steps ### 1. Initialize the frontend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/captcha-react ``` #### 1.2 Update your frontend SDK configuration ```typescript import SuperTokens from "supertokens-auth-react"; import CaptchaPlugin from "@supertokens-plugins/captcha-react"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // your recipes ], experimental: { plugins: [ CaptchaPlugin.init({ type: "reCAPTCHAv3", // or "reCAPTCHAv2" or "turnstile" captcha: { sitekey: "your-site-key", // Additional configuration based on the captcha provider }, }), ], }, }); ``` ### 2. Initialize the backend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/captcha-nodejs ``` #### 2.2 Update your backend SDK configuration ```typescript import SuperTokens from "supertokens-node"; import CaptchaPlugin from "@supertokens-plugins/captcha-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ // your recipes ], experimental: { plugins: [ CaptchaPlugin.init({ type: "reCAPTCHAv3", // or "reCAPTCHAv2" or "turnstile" captcha: { secretKey: "your-secret-key", }, }), ], }, }); ``` :::info If you are using a captcha input that renders an input on the frontend, you will have to [disable the use of shadow DOM](/references/frontend-sdks/prebuilt-ui/shadow-dom) when you initialize the SDK. ::: ### 3. Customize the plugin By default, the plugin performs CAPTCHA validation on the following authentication flows: | Recipe | Authentication Flow | Forms | Pre-API Hook Action | API Function | | --------------- | -------------------------- | -------------------------------------------------------------------------------------- | --------------------------- | -------------------------------- | | `EmailPassword` | User sign in | `EmailPasswordSignInForm` | `EMAIL_PASSWORD_SIGN_IN` | `signInPOST` | | `EmailPassword` | User registration | `EmailPasswordSignUpForm` | `EMAIL_PASSWORD_SIGN_UP` | `signUpPOST` | | `EmailPassword` | Password reset request | `EmailPasswordResetPasswordEmail` | `SEND_RESET_PASSWORD_EMAIL` | `generatePasswordResetTokenPOST` | | `EmailPassword` | Password reset submission | `EmailPasswordSubmitNewPassword` | `SUBMIT_NEW_PASSWORD` | `passwordResetPOST` | | `Passwordless` | Generate verification code | `PasswordlessEmailForm` and `PasswordlessPhoneForm` and `PasswordlessEmailOrPhoneForm` | `PASSWORDLESS_CREATE_CODE` | `createCodePOST` | | `Passwordless` | Verify code and sign in | `PasswordlessUserInputForm` | `PASSWORDLESS_CONSUME_CODE` | `consumeCodePOST` | To limit which actions require additional validation, pass additional configuration parameters to the frontend and backend setup steps. #### Frontend conditional validation On the frontend create a custom component that conditionally loads the CAPTCHA provider based on the name of the form. ```tsx import { forwardRef, useEffect } from "react"; import type { ComponentPropsWithoutRef } from "react"; import SuperTokens from "supertokens-auth-react"; import CaptchaPlugin, { useCaptcha, useCaptchaInputContainer } from "@supertokens-plugins/captcha-react"; type CaptchaInputContainerProps = ComponentPropsWithoutRef>; const CaptchaInputContainer = forwardRef((props, ref) => { const { form, ...rest } = props; const { load, render, containerId } = useCaptcha(); useEffect(() => { // CAPTCHA applies/renders only for the EmailPasswordSignUpForm // and the EmailPasswordResetPasswordEmail if (form === "EmailPasswordSignUpForm" || form === "EmailPasswordResetPasswordEmail") { void load().then(() => render()); } }, [form, load, render]); return (
); }); SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // your recipes ], experimental: { plugins: [ CaptchaPlugin.init({ type: "reCAPTCHAv3", // or "reCAPTCHAv2" or "turnstile" captcha: { sitekey: "your-site-key", }, InputContainer: CaptchaInputContainer, }), ], }, }); ``` #### Backend conditional validation On the backend pass a custom validation function that tells the plugin which actions should require extra validation. ```typescript import SuperTokens from "supertokens-node"; import CaptchaPlugin, { SuperTokensPluginCaptchaConfig } from "@supertokens-plugins/captcha-nodejs"; const shouldValidate: NonNullable = (api, input) => { // Only require CAPTCHA for sign up if (api === "signUpPOST") { return true; } // Check request headers for suspicious activity if (api === "signInPOST") { const userAgent = input.options.req.getHeaderValue("user-agent"); return !userAgent || userAgent.includes("bot"); } return false; }; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ // your recipes ], experimental: { plugins: [ CaptchaPlugin.init({ type: "reCAPTCHAv3", // or "reCAPTCHAv2" or "turnstile" captcha: { secretKey: "your-secret-key", }, shouldValidate, }), ], }, }); ``` ## Next steps Besides CAPTCHA validation you can also look into the **Attack Protection Suite** feature which provides prevention against suspicious authentication attempts. Prevent suspicious authentication attempts. Add multi-factor authentication to your authentication flows. General information on how plugins work. --- # Customize the pre-built UI Source: https://supertokens.com/docs/additional-verification/email-verification/changing-style ## Overview Updating the CSS allows you to change the UI of the components to meet your needs. This section guides you through an example of updating the look of buttons. Note that you can apply the process to update any HTML tag from within SuperTokens components. ## Before you start This guide is only relevant if you are using the **pre-built UI** components. If you are using your own UI, you can skip this section. --- ## Global style changes Each stylable component contains the `data-supertokens` attribute (in this example `data-supertokens="link"`). For more information on how to find a specific selector look over the [changing style page](/references/frontend-sdks/prebuilt-ui/changing-style). Let's add a `border` to the `link` elements. The syntax for styling is plain CSS. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ style: ` [data-supertokens~=link] { border: 2px solid #0076ff; border-radius: 5; width: 30%; margin: 0 auto; } `, }), Session.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ style: ` [data-supertokens~=link] { border: 2px solid #0076ff; border-radius: 5px; width: 30%; margin: 0 auto; } `, }), ], }); ``` ### Change fonts By default, SuperTokens uses the `Arial` font. The best way to override this is to add a `font-family` styling to the `container` component in the recipe configuration. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ style: ` [data-supertokens~=container] { font-family: cursive } `, }), Session.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ style: ` [data-supertokens~=container] { font-family: cursive } `, }), ], }); ``` ### Use media queries You may want to have different CSS for different `viewports`. You can achieve this via media queries like this: ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // ... EmailVerification.init({ // ... style: ` [data-supertokens~=link] { border: 2px solid #0076ff; borderRadius: 5; width: 30%; margin: 0 auto; } @media (max-width: 440px) { [data-supertokens~=link] { width: 90%; } } `, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // ... supertokensUIEmailVerification.init({ // ... style: ` [data-supertokens~=link] { border: 2px solid #0076ff; borderRadius: 5; width: 30%; margin: 0 auto; } @media (max-width: 440px) { [data-supertokens~=link] { width: 90%; } } `, }), ], }); ``` ## Customize individual screens ### Send email screen This screen is where the system redirects the user if you set `mode` to `REQUIRED` and they visit a path that requires a verified email. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ sendVerifyEmailScreen: { style: ` ... `, }, }), Session.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ sendVerifyEmailScreen: { style: ` ... `, }, }), ], }); ``` ### Verify link clicked screen This is the screen shown to users that click the email verification link in the email. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ verifyEmailLinkClickedScreen: { style: ` ... `, }, }), Session.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ verifyEmailLinkClickedScreen: { style: ` ... `, }, }), ], }); ``` --- ## See also --- # Embed the UI in a page Source: https://supertokens.com/docs/additional-verification/email-verification/embed-in-page ## Overview If you are looking to render the email verification UI in a different page follow this guide. ## Before you start Most of the updates require your attention if you are using the **pre-built UI** components. If you are working with a **custom UI** you need to update the backend configuration, like in step **3.1**. ## Steps ### 1. Disable the default implementation :::note[If you are using a **custom UI** implementation, then you can skip this step.] ::: ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", disableDefaultUI: true, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ mode: "REQUIRED", disableDefaultUI: true, }), ], }); ``` If you navigate to `/auth/verify-email`, you should not see the widget anymore. ### 2. Render the component yourself :::note[If you are using a **custom UI** implementation, then you can skip this step.] ::: Add the `EmailVerification` component in your app: :::warning[You have to build your own UI instead.] ::: ```tsx import React from "react"; import { EmailVerification } from "supertokens-auth-react/recipe/emailverification/prebuiltui"; class EmailVerificationPage extends React.Component { render() { return (
); } } ```
### 3. Change the website path for the email verification UI (optional) The default path for this is component is `/{websiteBasePath}/verify-email`. If you are displaying this at some custom path, then you need add additional configuration on the backend and frontend: #### 3.1 Update the backend configuration ```tsx import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail(input) { return originalImplementation.sendEmail({ ...input, emailVerifyLink: input.emailVerifyLink.replace( // This is: `/auth/verify-email` "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path", ), }); }, }; }, }, }), ], }); ``` ```go import ( "strings" "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeOptional, EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // This is: `/auth/verify-email` input.EmailVerification.EmailVerifyLink = strings.Replace( input.EmailVerification.EmailVerifyLink, "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path", 1, ) return ogSendEmail(input, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailverification from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig from supertokens_python.recipe.emailverification.types import EmailDeliveryOverrideInput, EmailTemplateVars from typing import Dict, Any def custom_email_delivery(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: # This is: `/auth/verify-email` template_vars.email_verify_link = template_vars.email_verify_link.replace( "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path") return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailverification.init( mode="OPTIONAL", email_delivery=EmailDeliveryConfig(override=custom_email_delivery)) ] ) ``` #### 3.2 Update the frontend configuration :::note[If you are using a **custom UI** implementation, then you can skip this step.] ::: ```tsx import SuperTokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", // The user will be taken to the custom path when they need to get their email verified. getRedirectionURL: async (context) => { if (context.action === "VERIFY_EMAIL") { return "/custom-email-verification-path"; } }, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ mode: "REQUIRED", // The user will be taken to the custom path when they need to get their email verified. getRedirectionURL: async (context) => { if (context.action === "VERIFY_EMAIL") { return "/custom-email-verification-path"; } }, }), ], }); ``` --- # Hooks and overrides Source: https://supertokens.com/docs/additional-verification/email-verification/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `emailverification` recipe to your needs. Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Backend override To perform any task post email verification like analytics, sending a user a welcome email or notifying an internal dashboard, you need to override the `verifyEmailPOST` API. ```tsx import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", override: { apis: (originalImplementation) => { return { ...originalImplementation, verifyEmailPOST: async function (input) { if (originalImplementation.verifyEmailPOST === undefined) { throw Error("Should never come here"); } // First we call the original implementation let response = await originalImplementation.verifyEmailPOST(input); // Then we check if it was successfully completed if (response.status === "OK") { let { recipeUserId, email } = response.user; // TODO: post email verification logic } return response; }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, Override: &evmodels.OverrideStruct{ APIs: func(originalImplementation evmodels.APIInterface) evmodels.APIInterface { ogVerifyEmailPOST := *originalImplementation.VerifyEmailPOST (*originalImplementation.VerifyEmailPOST) = func(token string, sessionContainer sessmodels.SessionContainer, tenantId string, options evmodels.APIOptions, userContext supertokens.UserContext) (evmodels.VerifyEmailPOSTResponse, error) { resp, err := ogVerifyEmailPOST(token, sessionContainer, tenantId, options, userContext) if err != nil { return evmodels.VerifyEmailPOSTResponse{}, err } if resp.OK != nil { id := resp.OK.User.ID email := resp.OK.User.Email fmt.Println(id) fmt.Println(email) // TODO: post email verification logic } return resp, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailverification from supertokens_python.recipe.emailverification.interfaces import APIInterface, APIOptions, EmailVerifyPostOkResult from typing import Dict, Any, Optional from supertokens_python.recipe.session.interfaces import SessionContainer def override_email_verification_apis(original_implementation_email_verification: APIInterface): original_email_verify_post = original_implementation_email_verification.email_verify_post async def email_verify_post(token: str, session: Optional[SessionContainer], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any],): response = await original_email_verify_post(token, session, tenant_id, api_options, user_context) # Then we check if it was successfully completed if isinstance(response, EmailVerifyPostOkResult): _ = response.user # TODO: post email verification logic return response original_implementation_email_verification.email_verify_post = email_verify_post return original_implementation_email_verification init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailverification.init( mode="REQUIRED", override=emailverification.InputOverrideConfig( apis=override_email_verification_apis ) ) ] ) ``` --- # Initial Setup Source: https://supertokens.com/docs/additional-verification/email-verification/initial-setup Add SuperTokens email verification to this application. Inspect the existing recipes and determine whether verification should be REQUIRED or OPTIONAL; ask if the business rule is unclear. Configure the backend and frontend EmailVerification and Session recipes consistently, add the required UI routes or custom flow, and verify protected-route behavior. Account for passwordless email behavior and keep delivery credentials in environment variables. Run the relevant tests, typechecks, and build. ## Overview Email verification needs to be explicitly configured to work in your **SuperTokens** integration. The functionality offers two ways to set it up: - `REQUIRED`: The user needs to verify before they can access any protected routes. - `OPTIONAL`: The sessions include information about the email verification status, but it is up to you to enforce the requirement based on your business logic. ## Before you start :::info[Access token guidance] If you are implementing [**Unified Login**](/authentication/unified-login/introduction) you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**. Please read the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify the token. ::: For passwordless login, with email, a user's email is automatically marked as verified when they login. Therefore, this flow only triggers if a user changes their email during a session. ## Steps ### 1. Initialize the backend recipe ```tsx import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, // or evmodels.ModeOptional }), session.Init(&sessmodels.TypeInput{}), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.recipe import emailverification init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailverification.init(mode='REQUIRED'), # or 'OPTIONAL' session.init() ] ) ``` ### 2. Initialize the frontend recipe You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import { EmailVerificationPreBuiltUI } from "supertokens-auth-react/recipe/emailverification/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), Session.init(), ], }); function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ /* Other pre-built UI */ EmailVerificationPreBuiltUI, ])} // ... other routes
); } ```
```tsx import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import { EmailVerificationPreBuiltUI } from "supertokens-auth-react/recipe/emailverification/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), Session.init(), ], }); function App() { if (canHandleRoute([/* Other pre-built UI */ EmailVerificationPreBuiltUI])) { return getRoutingComponent([/* Other pre-built UI */ EmailVerificationPreBuiltUI]); } return {/*Your app*/}; } ```
```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit("supertokensui", { appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailVerification.init({ mode: "REQUIRED", // or "OPTIONAL" }), ], }); ```
This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import EmailVerification from "supertokens-web-js/recipe/emailverification"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [EmailVerification.init(), Session.init()], }); ``` :::note[SuperTokens triggers verification emails by redirecting the user to the email verification path when the mode is `REQUIRED`.] If you have set the mode to `OPTIONAL` or are **NOT** using the `SessionAuth` wrapper, you need to manually trigger the verification email. The guide on [protecting API and website routes](./protecting-routes) covers the changes that you need to make. Additionally, note that SuperTokens does not send verification emails post user sign up. Redirect the user to the email verification path to trigger the sending of the verification email. This happens automatically when using the prebuilt UI and in `REQUIRED` mode. :::
### 2. Initialize the frontend recipe :::success[No specific action required here.] ::: ```tsx import SuperTokens from "supertokens-web-js"; import EmailVerification from "supertokens-web-js/recipe/emailverification"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [EmailVerification.init(), Session.init()], }); ``` ### 3. Send the email verification email After a user signs up, or when the email verification validators fail, you need to tell the user about the email verification process. Redirect them to a screen that informs them about the current status and call the verification API. Create a new screen on your app that asks the user to enter their email to receive an email. This screen should ideally link to the sign in form. Once the user has entered their email, you can call the following API to send an email verification email to that user: ```tsx import { sendVerificationEmail } from "supertokens-web-js/recipe/emailverification"; async function sendEmail() { try { let response = await sendVerificationEmail(); if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { // This can happen if the info about email verification in the session was outdated. // Redirect the user to the home page window.location.assign("/home"); } else { // email was sent successfully. window.alert("Please check your email and click the link in it"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/auth/user/email/verify/token' \ --header 'Authorization: Bearer ...' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: An email was successfully sent to the user. - `status: "EMAIL_ALREADY_VERIFIED_ERROR"`: This status can return if the info about email verification in the session was outdated. Redirect the user to the home page. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. :::info[Multi Tenancy] You do not need to add the tenant ID to the path here because the backend fetches the `tenantId` of the user from the session token. ::: :::note[The API for sending an email verification email requires an active session. If you are using the frontend SDKs, then the session tokens should automatically get attached to the request.] ::: #### Change the email verification link By default, the email verification link points to the `websiteDomain` configured on the backend. That would be the `/auth/verify-email` route if `/auth` is the value of `websiteBasePath`. If you want to change this to something different, follow the next example: ```tsx import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail(input) { return originalImplementation.sendEmail({ ...input, emailVerifyLink: input.emailVerifyLink.replace( // This is: `/auth/verify-email` "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path", ), }); }, }; }, }, }), ], }); ``` ```go import ( "strings" "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeOptional, EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // This is: `/auth/verify-email` input.EmailVerification.EmailVerifyLink = strings.Replace( input.EmailVerification.EmailVerifyLink, "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path", 1, ) return ogSendEmail(input, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailverification from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig from supertokens_python.recipe.emailverification.types import EmailDeliveryOverrideInput, EmailTemplateVars from typing import Dict, Any def custom_email_delivery(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: # This is: `/auth/verify-email` template_vars.email_verify_link = template_vars.email_verify_link.replace( "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path") return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailverification.init( mode="OPTIONAL", email_delivery=EmailDeliveryConfig(override=custom_email_delivery)) ] ) ``` :::info[Multi Tenancy] For a multi tenant setup, the input to the `sendEmail` function also contains the `tenantId`. You can use this to determine the correct value to set for the `websiteDomain` in the generated link. ::: ### 4. Verify the email after the user clicks the link Once the user clicks the email verification link, and it opens your app, call the following function. It extracts the token and `tenantId` (if you use a multi tenant setup) from the link and calls the token verification API. When the user clicks the email verification link, and it opens as a deep link into your mobile app, you can remove the token and call the verification API. ```tsx import { verifyEmail } from "supertokens-web-js/recipe/emailverification"; async function consumeVerificationCode() { try { let response = await verifyEmail(); if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { // This can happen if the verification code is expired or invalid. // You should ask the user to retry window.alert("Oops! Seems like the verification link expired. Please try again"); window.location.assign("/auth/verify-email"); // back to the email sending screen. } else { // email was verified successfully. window.location.assign("/home"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` :::info[Multi Tenancy] For a multitenant setup, read the tenant ID from the email verification link's `tenantId` query parameter and replace `public` in the request path. The public tenant also supports omitting the `/public` path segment. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: Email verification was successful. - `status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`: This can happen if the verification code expires or is invalid. You should ask the user to retry. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. :::warning[- This API doesn't require an active session to succeed.] - If you are calling the above API on page load, there is an edge case in which email clients might open the verification link in the email (for scanning purposes) and consume the token in the URL. This would lead to issues in which an attacker could sign up using someone else’s email and end up with a verified status! To prevent this, on page load, you should check if a session exists, and if it does, only then call the above API. If a session does not exist, you should first show a button, which when clicked would call the above API (email clients do not automatically click on this button). The button text could be something like "Click here to verify your email". ::: ## References ### Verification email This is how the email that the user receives looks like: UI of the verification email sent to the registered user You can find the [source code of this template on GitHub](https://github.com/supertokens/email-sms-templates/blob/master/email-html/email-verification.html) To understand more about how you can customize it, check the [email delivery](/platform-configuration/email-delivery) section. ### Verification link lifetime By default, the email verification link's lifetime is **1 day**. This can change via the Core configuration (time in milliseconds): - Go to the [SuperTokens SaaS dashboard](https://supertokens.com/dashboard) and select the relevant **Managed** deployment. - Open **Configuration** and find the **Email Verification** configuration card. - Change the `email_verification_token_lifetime` value. Configuration changes are saved automatically. ```bash # Here we set the lifetime to 2 hours. docker run \ -p 3567:3567 \ -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command email_verification_token_lifetime: 7200000 ``` ## Next steps --- # Introduction Source: https://supertokens.com/docs/additional-verification/email-verification/introduction ## Overview With the **Email Verification** recipe, you can confirm the email address of a user before they can use your application. ## Getting started You can go through the *Initial Setup* page for a quick tutorial on how to configure the feature. Go through a quick tutorial that shows you how to add the **Email Verification** recipe to your application. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Limit the access to your frontend and backend routes to users who have confirmed their email address. Generate verification links and change the email verification status manually. Learn how to change the look of the pre-built UI. --- # Manual actions Source: https://supertokens.com/docs/additional-verification/email-verification/manual-actions ## Overview Although the **SuperTokens** covers the entire email verification process you can also intervene manually in the process. The following page shows you what SDK methods you can use to adjust the verification flow. --- ## Generate a link You can use the backend SDK to generate the email verification link as shown below: ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; import supertokens from "supertokens-node"; async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { try { // Create an email verification link for the user const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); } else { // user's email is already verified } } catch (err) { console.error(err); } } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/emailverification" ) func main() { userID := "..." email := "..." // Create an email verification link for the user linkRes, err := emailverification.CreateEmailVerificationLink("public", userID, &email) if err != nil { // handle error } if linkRes.OK != nil { link := linkRes.OK.Link fmt.Println(link) } else { // user's email is already verified. } } ``` ```python from supertokens_python.recipe.emailverification.asyncio import create_email_verification_link from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationLinkOkResult from supertokens_python.types import RecipeUserId async def create_link(recipe_user_id: RecipeUserId, email: str): # Create an email verification link for the user link_res = await create_email_verification_link("public", recipe_user_id, email) if isinstance(link_res, CreateEmailVerificationLinkOkResult): link = link_res.link print(link) else: print("user's email is already verified") ``` ```python from supertokens_python.recipe.emailverification.syncio import create_email_verification_link from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationLinkOkResult from supertokens_python.types import RecipeUserId def create_link(recipe_user_id: RecipeUserId, email: str): # Create an email verification link for the user link_res = create_email_verification_link("public", recipe_user_id, email) if isinstance(link_res, CreateEmailVerificationLinkOkResult): link = link_res.link print(link) else: print("user's email is already verified") ``` :::info[Multi Tenancy] Notice that the first argument to the function call above is `"public"`. This refers to the default tenant ID that SuperTokens uses. It means that users belonging to the `"public"` tenant can only consume the generated email verification link. If you are using the multi tenancy feature, you can pass in the `tenantId` that contains this user, which you can fetch by getting the user object for this `userId`. Finally, the generated link uses the configured `websiteDomain` from the `appInfo` object (in `supertokens.init`), however, you can change the domain of the generated link to match that of the tenant ID. ::: --- ## Mark the email as verified To manually mark an email as verified, you need to first create an email verification token for the user and then use the token to verify the user's email. ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; import supertokens from "supertokens-node"; async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Create an email verification token for the user const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); // If the token creation is successful, use the token to verify the user's email if (tokenRes.status === "OK") { await EmailVerification.verifyEmailUsingToken("public", tokenRes.token); } } catch (err) { console.error(err); } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" ) func main() { userID := "..." // Create an email verification token for the user tokenRes, err := emailverification.CreateEmailVerificationToken("public", userID, nil) if err != nil { // handle error } // If the token creation is successful, use the token to verify the user's email if tokenRes.OK != nil { _, err := emailverification.VerifyEmailUsingToken("public", tokenRes.OK.Token) if err != nil { // handle error } } } ``` ```python from supertokens_python.recipe.emailverification.asyncio import create_email_verification_token, verify_email_using_token from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationTokenOkResult from supertokens_python.types import RecipeUserId async def manually_verify_email(recipe_user_id: RecipeUserId): try: # Create an email verification token for the user token_res = await create_email_verification_token("public", recipe_user_id) # If the token creation is successful, use the token to verify the user's email if isinstance(token_res, CreateEmailVerificationTokenOkResult): await verify_email_using_token("public", token_res.token) except Exception as e: print(e) ``` ```python from supertokens_python.recipe.emailverification.syncio import create_email_verification_token, verify_email_using_token from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationTokenOkResult from supertokens_python.types import RecipeUserId def manually_verify_email(recipe_user_id: RecipeUserId): try: # Create an email verification token for the user token_res = create_email_verification_token("public", recipe_user_id) # If the token creation is successful, use the token to verify the user's email if isinstance(token_res, CreateEmailVerificationTokenOkResult): verify_email_using_token("public", token_res.token) except Exception as e: print(e) ``` :::info[Multi Tenancy] Notice that the first argument of the function call above is `"public"`. This refers to the `"public"` `tenantId` (which is the default `tenantId`). In case you are using the multi tenancy feature, you can still pass in the `"public"` tenant ID here. Even if the user ID is not part of that tenant, you can pass it because the system creates and consumes the token in one shot. ::: --- ## Mark the email as unverified To manually mark an email as unverified, you need to first retrieve the user's email address and then update their email verification status in the database. ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; import supertokens from "supertokens-node"; async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { try { // Set email verification status to false await EmailVerification.unverifyEmail(recipeUserId); } catch (err) { console.error(err); } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" ) func main() { userID := "..." // Set email verification status to false _, err := emailverification.UnverifyEmail(userID, nil) if err != nil { // handle error } } ``` ```python from supertokens_python.recipe.emailverification.asyncio import unverify_email from supertokens_python.types import RecipeUserId async def manually_unverify_email(recipe_user_id: RecipeUserId): try: # Set email verification status to false await unverify_email(recipe_user_id) except Exception as e: print(e) ``` ```python from supertokens_python.recipe.emailverification.syncio import unverify_email from supertokens_python.types import RecipeUserId def manually_unverify_email(recipe_user_id: RecipeUserId): try: # Set email verification status to false unverify_email(recipe_user_id) except Exception as e: print(e) ``` :::info[Multi Tenancy] For a multi tenant setup, the function above does not take a tenant ID. A user ID and the associated email verification status is unique on an app level (and not a tenant level). ::: --- ## See also --- # Protect frontend and backend routes Source: https://supertokens.com/docs/additional-verification/email-verification/protecting-routes ## Overview The `EmailVerification` claim shows the status of the email verification process. Follow this page to understand how to limit access based on whether the user has confirmed their email address. ## Before you start :::info[Access token guidance] If you are implementing [**Unified Login**](/authentication/unified-login/introduction), you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**. Please read the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify the token. ::: --- ## Protect backend routes ### Add email verification checks on all routes If you want to protect all your backend API routes with email verification checks, set the `mode` to `REQUIRED` in the `EmailVerification` configuration. Routes protected with the `verifySession` middleware additionally check for email verification status. ```tsx import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailVerification.init({ // This means that verifySession will now only allow calls if the user has verified their email mode: "REQUIRED", }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailverification.Init(evmodels.TypeInput{ // This means that VerifySession will now only allow calls if the user has verified their email Mode: evmodels.ModeRequired, }), session.Init(&sessmodels.TypeInput{}), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.recipe import emailverification init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ # This means that VerifySession will now only allow calls if the user has verified their email emailverification.init(mode='REQUIRED'), session.init() ] ) ``` In case you have set the email verification mode to `REQUIRED` but want to disable the check for a specific route, you can make the following changes to the `verifySession` middleware: ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), async (req: SessionRequest, res) => { // The session and remaining claim validators have passed; email verification was skipped }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), }, ], }, handler: async (req: SessionRequest, res) => { // The session and remaining claim validators have passed; email verification was skipped }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), }, async (req: SessionRequest, res) => { // The session and remaining claim validators have passed; email verification was skipped }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; async function updateBlog(awsEvent: SessionEvent) { // The session and remaining claim validators have passed; email verification was skipped } exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let router = new KoaRouter(); router.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), async (ctx: SessionContext, next) => { // The session and remaining claim validators have passed; email verification was skipped }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), ) @response(200) async handler() { // The session and remaining claim validators have passed; email verification was skipped } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), })(req, res, next); }, req, res, ); // The session and remaining claim validators have passed; email verification was skipped } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; @Controller() export class ExampleController { @Post("example") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), ) async postExample(@Session() session: SessionContainer): Promise { // The session and remaining claim validators have passed; email verification was skipped return true; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { // we keep all claim validators except for // the email verification claim validator. if v.ID != evclaims.EmailVerificationClaim.Key { filtered = append(filtered, v) } } return filtered, nil }, }, exampleAPI).ServeHTTP(rw, r) }) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` ```go import ( "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { // we keep all claim validators except for // the email verification claim validator. if v.ID != evclaims.EmailVerificationClaim.Key { filtered = append(filtered, v) } } return filtered, nil }, }), exampleAPI) } // This is a function that wraps the supertokens verification function // to work the 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 exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { // we keep all claim validators except for // the email verification claim validator. if v.ID != evclaims.EmailVerificationClaim.Key { filtered = append(filtered, v) } } return filtered, nil }, }, exampleAPI)) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { // we keep all claim validators except for // the email verification claim validator. if v.ID != evclaims.EmailVerificationClaim.Key { filtered = append(filtered, v) } } return filtered, nil }, }, exampleAPI)).Methods(http.MethodPost) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != EmailVerificationClaim.key] ) )): # The session and remaining claim validators have passed; email verification was skipped pass ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim @app.route('/update-jwt', methods=['POST']) @verify_session( # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != EmailVerificationClaim.key] ) def like_comment(): # The session and remaining claim validators have passed; email verification was skipped pass ``` ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.emailverification import EmailVerificationClaim @verify_session( # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != EmailVerificationClaim.key] ) async def like_comment(request: HttpRequest): # The session and remaining claim validators have passed; email verification was skipped pass ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import SuperTokens from "supertokens-node"; import { NextResponse, NextRequest } from "next/server"; import { withSession } from "supertokens-node/nextjs"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; import { backendConfig } from "@/app/config/backend"; SuperTokens.init(backendConfig()); export async function POST(request: NextRequest) { return withSession( request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // We skipped checking the email verification claim return NextResponse.json({}); }, { overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }, ); } ``` ### Add email verification checks to specific routes If you want to protect specific backend API routes with email verification checks, set the `mode` to `OPTIONAL` in the `EmailVerification` configuration. You then override the `verifySession` middleware protecting the route to check for email verification status. ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }), async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user has a verified email address } exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let router = new KoaRouter(); router.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user has a verified email address }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }), ) @response(200) async handler() { // All validator checks have passed and the user has a verified email address } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], })(req, res, next); }, req, res, ); // All validator checks have passed and the user has a verified email address } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; @Controller() export class ExampleController { @Post("example") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }), ) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user has a verified email address return true; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, }, exampleAPI).ServeHTTP(rw, r) }) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` ```go import ( "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, }), exampleAPI) } // This is a function that wraps the supertokens verification function // to work the 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 exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, }, exampleAPI)) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims" "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() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, }, exampleAPI)).Methods(http.MethodPost) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( # We add the EmailVerificationClaim's is_verified validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [EmailVerificationClaim.validators.is_verified()] ) )): # All validator checks have passed and the user has a verified email address pass ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim @app.route('/update-jwt', methods=['POST']) @verify_session( # We add the EmailVerificationClaim's is_verified validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [EmailVerificationClaim.validators.is_verified()] ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.emailverification import EmailVerificationClaim @verify_session( # We add the EmailVerificationClaim's is_verified validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [EmailVerificationClaim.validators.is_verified()] ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import SuperTokens from "supertokens-node"; import { NextResponse, NextRequest } from "next/server"; import { withSession } from "supertokens-node/nextjs"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; import { backendConfig } from "@/app/config/backend"; SuperTokens.init(backendConfig()); export async function POST(request: NextRequest) { return withSession( request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } // All validator checks have passed and the user has a verified email address return NextResponse.json({}); }, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, EmailVerificationClaim.validators.isVerified(), ], }, ); } ``` --- ## Protect frontend routes ### Protect all frontend routes Set the email verification mode to `REQUIRED` and wrap your website routes using ``. If the user's email is not verified, SuperTokens automatically redirects the user to the email verification screen. ### Protect specific frontend routes Set the email verification mode to `OPTIONAL`. Create a generic component called `VerifiedRoute` which enforces that its child components can only render if the user has a verified email address. ```tsx import React from "react"; import { SessionAuth, useSessionContext } from "supertokens-auth-react/recipe/session"; import { EmailVerificationClaim } from "supertokens-auth-react/recipe/emailverification"; const VerifiedRoute = (props: React.PropsWithChildren) => { return ( {props.children} ); }; function InvalidClaimHandler(props: React.PropsWithChildren) { let sessionContext = useSessionContext(); if (sessionContext.loading) { return null; } if (sessionContext.invalidClaims.some((i) => i.id === EmailVerificationClaim.id)) { // Alternatively you could redirect the user to the email verification screen to trigger the verification email // Note: /auth/verify-email is the default email verification path // window.location.assign("/auth/verify-email") return
You cannot access this page because your email address is not verified.
; } // We show the protected route since all claims validators have // passed implying that the user has verified their email. return
{props.children}
; } ```
```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has verified their email address return true; } else { for (const err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email is not verified } } } } // a session does not exist, or email is not verified return false; } ```
In the `VerifiedRoute` component, use the `SessionAuth` wrapper to ensure that the session exists. The `` component automatically adds the `EmailVerificationClaim` validator if you initialize the `EmailVerification` recipe. Finally, check the validation result in `InvalidClaimHandler`. It displays `"You cannot access this page because your email address is not verified."` if the `EmailVerificationClaim` validator failed. Alternatively you could also redirect the user to the default email verification path to trigger the sending of the verification email. :::note[You can extend the `VerifiedRoute` component to check for other types of validators as well.] You can reuse this component to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). ::: ### Check the verification status manually If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it reflects in the `validationErrors` variable. The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe. ### Validation errors In case the `validationErrors` array is not empty, you can loop through the errors to know which claim has failed: ```tsx import Session from "supertokens-auth-react/recipe/session"; import { EmailVerificationClaim } from "supertokens-auth-react/recipe/emailverification"; function ProtectedComponent() { let claimValue = Session.useClaimValue(EmailVerificationClaim); if (claimValue.loading || !claimValue.doesSessionExist) { return null; } let isEmailVerified = claimValue.value; if (isEmailVerified !== undefined && isEmailVerified) { //... } else { // Redirect the user the email verification path to send the verification email // Note: /auth/verify-email is the default email verification path window.location.assign("/auth/verify-email"); } } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute() { let validationErrors = await Session.validateClaims(/*{...}*/); for (const err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email verification claim check failed } else { // some other claim check failed (from the global validators list) } } } ``` ### Check the verification status manually If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let isVerified = await Session.getClaimValue({ claim: EmailVerificationClaim }); if (isVerified) { // user has verified their email address return true; } } // either a session does not exist, or the user has not verified their email address return false; } ```
## Protect frontend routes ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim, sendVerificationEmail } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has verified their email address return true; } else { for (const err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email is not verified // Send the verification email to the user await sendEmail(); } } } } // a session does not exist, or email is not verified return false; } async function sendEmail() { try { let response = await sendVerificationEmail(); if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { // This can happen if the info about email verification in the session was outdated. // Redirect the user to the home page window.location.assign("/home"); } else { // email was sent successfully. window.alert("Please check your email and click the link in it"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function checkIfEmailIsVerified() { if (await SuperTokens.doesSessionExist()) { let isVerified: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-ev"].v; if (isVerified) { // TODO.. } else { // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun checkIfEmailIsVerified() { val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); val isVerified: Boolean = (accessTokenPayload.get("st-ev") as JSONObject).get("v") as Boolean if (isVerified) { // TODO.. } else { // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func checkIfEmailIsVerified() { if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let emailVerificationObject: [String: Any] = accessTokenPayload["st-ev"] as? [String: Any], let isVerified: Bool = emailVerificationObject["v"] as? Bool { if isVerified { // Email is verified } else { // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future checkIfEmailIsVerified() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-ev")) { Map emailVerificationObject = accessTokenPayload["st-ev"]; if (emailVerificationObject.containsKey("v")) { bool isVerified = emailVerificationObject["v"]; if (isVerified) { // Email is verified } else { // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } } ``` :::note[The API for sending an email verification email requires an active session.] If you are using the frontend SDKs, then the session tokens should automatically get attached to the request. ::: In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it reflects in the `validationErrors` variable. The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe. ### Handle 403 responses on the frontend If your frontend queries a protected API on your backend and it fails with a 403, you can call the `validateClaims` function. Loop through the errors to know which claim has failed: ### Handle 403 responses on the frontend If your frontend queries a protected API on your backend and it fails with a 403, you can check the value of the `st-ev` claim in the access token payload. If it is `false`, you can send the verification email. ```tsx import axios from "axios"; import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function callProtectedRoute() { try { let response = await axios.get("/protectedroute"); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 403) { let validationErrors = await Session.validateClaims(); for (let err of validationErrors) { if (err.id === EmailVerificationClaim.id) { // email verification claim check failed // We call the sendEmail function defined in the previous section to send the verification email. // await sendEmail(); } else { // some other claim check failed (from the global validators list) } } } } } ``` --- ## See also --- # Implement recovery codes Source: https://supertokens.com/docs/additional-verification/mfa/backup-codes ## Overview Backup codes is one of the ways in which end users can recover their account in case they lose their second factor device. At the moment, SuperTokens does not have an in-built implementation for backup codes, however, you can customize the SDKs to add it. :::info[Note] [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes) of how you can implement backup codes in your application if you are using the pre-built UI. ::: This guide shows you how to implement the following flow: - After MFA setup users can generate backup codes. - The backup code associates with the userID in the `UserMetadata` JSON. - When a user wants to use their backup code, the application shows them a UI to enter the code. This calls an API that verifies the backup code and adds a flag in their session, indicating that they have correctly supplied a backup code. - After that, the flag allows users to create a new MFA device, even if they already have one registered on their account. User can then go about adding a new device and completing MFA using that. ## Before you start These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/additional-verification/mfa/important-concepts). The guide below focuses on Time-based One-Time Password (TOTP) as a second factor, but you can implement something similar for Passwordless as well. ## Steps ### 1. Add an API to generate backup codes on the backend Here is an [example API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L52) for how you can create a recovery code. In here, you create a secure random string and save the hashed version in the user metadata JSON. This API returns the plain text recovery code to the frontend to display to the user. ### 2. Allow users to generate a backup code when they finish MFA setup After the user has successfully set up their second factor (during sign up or during recovery process), they navigate to a page which shows them their backup code. The user can automatically redirect to this page (once they have completed their MFA setup) by adding a claim validator on the frontend. This enforces that the user always has a backup code associated with their account. Even if they consume the code in the future, this validator fails and redirects them to the create new backup code screen. The idea here is to modify the access token payload on the backend to add a boolean value to it. This value is `true` if the user already has a backup code with their account, else it's `false`. The frontend claim validator fails if the value is `false` and redirects them to the UI for creating a backup code. You can see this validator implementation on the frontend in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/recoveryCodeExistsClaim.ts). This validator adds to the [Session.init on the frontend](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/config.tsx#L56) that it runs each time you protect a route with ``. It is also necessary to add a claim validator on the backend to add the boolean value to the access token payload. You can see the claim validator's implementation in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/recoveryCodeExistsClaim.ts). This validator appears in a few places: - [When creating a new backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L61). - [When consuming an existing backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L55). - [When creating a new session](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L127). Once on the page, the UI calls the API created in the previous step to create a new recovery code for the user. Note that calling this API replaces the older recovery code, but since it's all custom, you can change the logic. [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. For custom UI, the UI for where and how you show the recovery code page is up to you. It is advisable to show the user this page post sign up, or whenever they create a new MFA device successfully. The user can automatically redirect to this page (once they have completed their MFA setup) by adding a claim validator on the frontend. This enforces that the user always has a backup code associated with their account. Even if they consume the code in the future, this validator fails and redirects them to the create new backup code screen. The idea here is to modify the access token payload on the backend to add a boolean value to it. This value is `true` if the user already has a backup code with their account, else it's `false`. The frontend claim validator fails if the value is `false` and redirects them to the UI for creating a backup code. You can see this validator implementation on the frontend in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/recoveryCodeExistsClaim.ts). This validator adds to the [Session.init on the frontend](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/config.tsx#L56) that it runs each time you protect a route with `await Session.validateClaims` function call. It is also necessary to add a claim validator on the backend to add the boolean value to the access token payload. You can see the claim validator's implementation in the [recoveryCodeExistsClaim.ts file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/recoveryCodeExistsClaim.ts). This validator appears in a few places: - [When creating a new backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/index.ts#L61). - [When consuming an existing backup code](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L55). - [When creating a new session](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L127). Once on the page, the UI calls the API created in the previous step to create a new recovery code for the user. Note that calling this API replaces the older recovery code, but since it's all custom, you can change the logic. [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. ### 3. Show how to use backup codes on the MFA challenge UI You can achieve this by creating a "Lost device?" button in the pre-built UI that asks the user to enter the Time-based One-Time Password (TOTP) challenge. Once they click on this, users redirect to a page where they can enter their backup code. After verification, they further redirect to the create a new Time-based One-Time Password (TOTP) device page. [Here is how you can override the pre-built UI](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/App.tsx#L19) to display the "Lost device?" button. [Here is an example implementation](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx) of a page which asks the user to enter their backup code. It then calls an API (see next step) to check if the code is correct or not. You should make a UI that asks the user to enter their backup code and call the API to verify it and mark it as "in use" (see next step). You want to give the option for users to enter their backup code when asked for the MFA challenge. ### 4. Modify the user's session to mark that they have verified their backup code On the backend, you set up an API that accepts the recovery code entered by the user. It checks that it matches the hashed version stored in the user's metadata JSON. If it does, it marks as "in use" in the user's session payload. You achieve this by saving the `recoverCodeHash` in the session payload, which is then checked in the next step to force enable Time-based One-Time Password (TOTP) device creation. On the frontend, once this API returns a success, the user should navigate to the create a new Time-based One-Time Password (TOTP) device screen. You can see how this process completes in the [index.tsx file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx#L20). On the frontend, once this API returns a success, the user should navigate to the create a new Time-based One-Time Password (TOTP) device screen. ### 5. Force users to setup a new device Here are the steps: - Force enabling Time-based One-Time Password (TOTP) device creation if the `recoveryCodeHash` is in the user's session's access token payload. - Deleting the recovery code from the user's metadata JSON once they have consumed it to create a new device. - Redirecting the user to the page that shows their new recovery code after they have set up their new device. You can achieve the first step by overriding [this function](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L68) on the backend. By default, the `assertAllowedToSetupFactorElseThrowInvalidClaimError` function throws an error if a Time-based One-Time Password (TOTP) device already exists. If the user tries to set up a new TOTP device during the sign-in process, it is for security reasons. However, this modifies to check if the access token payload contains the `recoveryCodeHash` and that it matches the one in the user metadata JSON. If it does, you allow new device setup, since it is known that the user had previously entered their recovery code successfully. During the Time-based One-Time Password (TOTP) device setup, users call [this API](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/backend/config.ts#L45) from the frontend. If this API succeeds, then it means that a new TOTP device has been created for the user and they have completed the TOTP challenge for the current session. On success, the old recovery code removes from the metadata in case the session has the `recoveryCodeHash` set. This way, the old recovery code is no longer usable. Finally, after successfully creating a new Time-based One-Time Password (TOTP) device, the frontend should redirect the user to the page which shows the new recovery code for the user. Refer to step 2 above. --- ## See also --- # OTP required for all users Source: https://supertokens.com/docs/additional-verification/mfa/email-sms-otp/otp-for-all-users ## Overview This page shows how to implement an MFA policy that requires all users to complete an OTP challenge before accessing your application. The OTP can be sent via email or phone. ## Single tenant setup ### Backend setup To start with, configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [MultiFactorAuth.FactorIds.OTP_EMAIL]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig, MFARequirementList from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [FactorIds.OTP_EMAIL] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` - Notice that the Passwordless recipe initializes in the `recipeList`. In this example, only email-based OTP is enabled, with `contactMethod` set to `EMAIL` and `flowType` to `USER_INPUT_CODE` (that is, `otp`). If you want to use phone SMS-based OTP, set the contact method to `PHONE`. If you want to give users both options, or for some users use email, and for others use phone, set `contactMethod` to `EMAIL_OR_PHONE`. - We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/post-authentication/account-linking/automatic-account-linking). - `shouldRequireVerification: true` prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session. - The `getMFARequirementsForAuth` function is overridden to indicate that `otp-email` must be completed before the user can access the app. Notice that `userId` is not checked there, and `otp-email` is returned for all users. You can also return `otp-phone` instead if you want users to complete the OTP challenge via a phone SMS. Finally, if you want to give users an option for email or phone, you can return the following array from the function: ```json [ { "oneOf": ["otp-email", "otp-phone"] } ] ``` Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `otp-email`, the payload looks like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "otp-email": 1702877999 }, "v": true } } ``` This indicates that the user has finished all required factors and should be allowed to access the app. :::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly.] ::: ### Frontend setup We start by modifying the `init` function call on the frontend like this: You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import supertokens from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init(/* ... */), EmailPassword.init(/* ... */), Passwordless.init({ contactMethod: "EMAIL", }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ supertokensUIThirdParty.init(/* ... */), supertokensUIEmailPassword.init(/* ... */), supertokensUIPasswordless.init({ contactMethod: "EMAIL", }), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, ], }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Passwordless from "supertokens-web-js/recipe/passwordless"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), Passwordless.init(), ], }); ``` - Like on the backend, the `passwordless` recipe initializes in the `recipeList`. The `contactMethod` needs to be consistent with the backend setting. - The `MultiFactorAuth` recipe is also initialized, and the first factors to use are included. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. Next, add the Passwordless pre-built UI when rendering the SuperTokens component: :::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] ::: ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI, ])} // ... other routes
); } ```
```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; function App() { if ( canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) ) { return getRoutingComponent([ EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI, ]); } return {/*Your app*/}; } ```
With the above configuration, users see `emailpassword` or social login UI when they visit the auth page. After completing that, users redirect to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they are asked to complete the OTP challenge. The UI for this screen looks like: - [Factor Setup UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--setup-email) (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login). - [Verification UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--verification). :::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly.] :::
We start by initializing the MFA and Passwordless recipe on the frontend like this: :::success[This step is not applicable for mobile apps. Please continue reading.] ::: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Passwordless from "supertokens-web-js/recipe/passwordless"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), Passwordless.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... supertokensMultiFactorAuth.init(), supertokensPasswordless.init(), ], }); ``` After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](/additional-verification/mfa/initial-setup#12-add-the-mfa-flow). If it's not, then the user can redirect to the application page. If it's `false`, the frontend then needs to [call the MFA endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) to get information about which factor the user should complete next. Based on the backend configuration in this page, the `next` array contains `["otp-email"]`. Two possibilities exist here: - Case 1: The user needs to set up an email to send the OTP to. This only happens if the first factor doesn't provide an email from the user (for example, if you used phone-based `otp` as the first factor). In this example on this doc, an email is always obtained from the first factor, so you do not need to build UI for this step (but this will still be discussed later on). - Case 2: The user already has an email associated with them and needs to complete the OTP challenge. We can know which case it is by checking if the `emails` object returned from [MFA Info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) contains any emails associated with the `otp-email` key. If the `emails["otp-email"]` property of the response is `undefined` or an empty array, then it's case 1, else it's case 2. #### Case 1 implementation: User needs to enter their email In this case, a form needs to be created wherein the user can enter their email. Once they submit the form, the [`createCode` API](/authentication/passwordless/initial-setup#21-creating-and-sending-the-otp) needs to be called. After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/authentication/passwordless/initial-setup#23-verifying-the-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by asking the user to enter their email once again and then call the `createCode` function. #### Case 2 implementation: User needs to complete the OTP challenge This case is when the user already has an email associated with their account and you can directly send a code to that email. You can get the email to send the code to from the result of the [MFA Info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). Specifically, from the response, you can read the email from the `emails` property like this: `emails["otp-email"][0]`. The first item in the array of emails is picked since the emails are ordered based on: - Index 0 contains the email that belongs to the session's user. If the user's first factor was email password, the email in the 0th index of the array is that email. - The other emails in the array (if they exist), are from other login methods for this user ordered based on the oldest login method first. You can even show a UI here asking the user to pick an email from the array if you like. Either way, when you have an email, you can call the [`createCode` API](/authentication/passwordless/initial-setup#21-creating-and-sending-the-otp) to send the code to that email. After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/authentication/passwordless/initial-setup#23-verifying-the-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by calling the `createCode` function once again in the background. :::note[In Case 2, there is no UI for the user to enter an email. The user only sees the enter OTP screen.] ::: We recommend that you add a sign out button when showing the second factor (case 1 or case 2) so that users can use this to escape out of the flow in case they are unable to complete the second factor. When the sign out button is clicked, you want to: - Call the `await clearLoginAttemptInfo()` function (if on web) to clear the state that's set in the browser storage when calling the `createCode` function. - Call the sign out function / API to clear the tokens. On successful verification of the code, the `otp-email` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). ## Multi tenant setup In a multi-tenancy setup, you may want to enable email / phone OTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#backend-setup) section above, so in this section, we will focus on enabling OTP for all users within specific tenants. ### Backend setup To start, initialize the Passwordless and the MultiFactorAuth recipes in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; import AccountLinking from "supertokens-node/recipe/accountlinking"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), MultiFactorAuth.init(), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Optional, Union async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), multifactorauth.init(), ], ) ``` Unlike the single tenant setup, no configuration is provided to the `MultiFactorAuth` recipe because all the necessary configuration is done on a tenant level. To configure otp-email requirement for a tenant, the following API can be called: :::note[At the moment this feature is not supported through the Go SDK.] ::: To configure otp-email requirement for a tenant, the following API can be called: Enable EmailPassword and ThirdParty, OTP-Email As shown above, enable **Email Password** and **Third Party** in the Login methods section and enable **OTP - Email** in the Secondary Factors Section. ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.OTP_EMAIL], ), ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.OTP_EMAIL], ), ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty"], "requiredSecondaryFactors": ["otp-email"] }' ``` - In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. - In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished otp-email challenge, the payload looks like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "otp-email": 1702877999 }, "v": true } } ``` This indicates that the user has finished all required factors and should be allowed to access the app. :::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly.] ::: ### Frontend setup We start by modifying the `init` function call on the frontend like this: You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import supertokens from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [ ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", }), MultiFactorAuth.init(), Multitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO"; }, }; }, }, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [ supertokensUIThirdParty.init({ //... }), supertokensUIEmailPassword.init({ //... }), supertokensUIPasswordless.init({ contactMethod: "EMAIL", }), supertokensUIMultiFactorAuth.init(), supertokensUIMultitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO"; }, }; }, }, }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` - Like on the backend, the `Passwordless` recipe initializes in the `recipeList`. Make sure that the configuration for it is consistent with what's on the backend. - The `MultiFactorAuth` recipe is also initialized. Notice that unlike the single tenant setup, the `firstFactors` are not specified here. That information is fetched based on the `tenantId` you provide the SDK with. - `usesDynamicLoginMethods: true` is set so that the SDK knows to fetch the login methods dynamically based on the `tenantId`. - Finally, the multi-tenancy recipe initializes and a method for getting the `tenantId` is provided. Next, add the Passwordless pre-built UI when rendering the SuperTokens component: :::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] ::: ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI, ])} // ... other routes
); } ```
```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; function App() { if ( canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) ) { return getRoutingComponent([ EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI, ]); } return {/*Your app*/}; } ```
With the above configuration, users see the first and second factor based on the tenant configuration. For the tenant configured above, users see email password or social login first. After completing that, users redirect to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they are asked to complete the OTP challenge. The UI for this screen looks like: - [Factor Setup UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--setup-email) (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login). - [Verification UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--verification). :::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly.] :::
The steps here are the same as in [the single tenant setup above](#frontend-setup). ## See also --- # OTP for specific users Source: https://supertokens.com/docs/additional-verification/mfa/email-sms-otp/otp-for-opt-in-users :::note Before reading the below, please first go through the setup for [OTP for all users](./otp-for-all-users) to understand the basics of how MFA with OTP works, and then come back here. ::: This page shows how to implement an MFA policy that requires certain users to do the OTP challenge via email or SMS. You can decide which users based on any criteria. For example: - Only users that have an `admin` role are required to complete OTP; OR - Only users that have enabled OTP on their account require to do OTP; OR - Only users that have a paid account require to do OTP. Whatever the criteria, the steps for implementing this type of flow are the same. ## Single tenant setup ### Backend setup #### Example 1: Only enable OTP for users that have an `admin` role To start with, configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); if (roles.roles.includes("admin")) { // we only want otp-email for admins return [MultiFactorAuth.FactorIds.OTP_EMAIL]; } else { // no MFA for non-admin users. return []; } }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, passwordless, session, thirdparty, userroles, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union from supertokens_python.recipe.userroles.asyncio import get_roles_for_user async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if "admin" in roles.roles: # We only want OTP_EMAIL for admins return [FactorIds.OTP_EMAIL] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), userroles.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` Override the `getMFARequirementsForAuth` function to indicate that `otp-email` applies only to users with the `admin` role. You can also have any other criteria here. #### Example 2: Ask for OTP only for users that have enabled OTP on their account To start with, configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", override: { apis: (oI) => { return { ...oI, consumeCodePOST: async function (input) { let response = await oI.consumeCodePOST!(input); if (response.status === "OK" && input.session !== undefined) { // We do this only if a session exists, which means that it's not being called for first factor login. // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the OTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL, ); } return response; }, }; }, }, }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Optional, Union from supertokens_python.recipe.passwordless.interfaces import ( RecipeInterface as PasswordlessRecipeInterface, ConsumeCodeOkResult, ) from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: PasswordlessRecipeInterface): 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], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, 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, session, should_try_linking_with_session_user, tenant_id, user_context, ) if isinstance(response, ConsumeCodeOkResult) and session is not None: await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.OTP_EMAIL ) return response original_implementation.consume_code = consume_code return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=passwordless.ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig(functions=override_functions), ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], ), ], ) ``` - Initialize the multi-factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors a user has enabled and returns those. All that is needed is to mark `otp-email` as enabled for a user as soon as they have completed the OTP challenge successfully. This happens in the `consumeCodePOST` API override as shown above. Once the code is consumed successfully, mark the `otp-email` factor as enabled for the user, and the next time they login, they will be asked to complete the OTP challenge. - Notice that before calling `addToRequiredSecondaryFactorsForUser`, check if there is an input session or not. Only call `addToRequiredSecondaryFactorsForUser` function if there is a session which indicates that the user has finished some first factor already. In both of the examples above, notice that the Passwordless recipe initializes in the `recipeList`. In this example, only email-based OTP is enabled, set the `contactMethod` to `EMAIL` and `flowType` to `USER_INPUT_CODE` (that is, OTP). If instead, you want to use phone SMS-based OTP, set the contact method to `PHONE`. If you want to give users both options, or for some users use email, and for others use phone, set `contactMethod` to `EMAIL_OR_PHONE`. We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/post-authentication/account-linking/automatic-account-linking). `shouldRequireVerification: true` prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this (for those that require OTP): ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished otp-email, the payload looks like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "otp-email": 1702877999 }, "v": true } } ``` This indicates that the user has finished all required factors and should be allowed to access the app. :::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again.] ::: ### Frontend setup This consists of two parts: - Configuring the frontend to show the OTP challenge UI when required during login / sign up - Allowing users to enable / disable OTP challenge on their account via the settings page (If you are following Example 2 from above). The first part is identical to the steps mentioned in [this section](./otp-for-all-users#frontend-setup), please follow that. The second part, which is only applicable in case you want to allow users to enable / disable OTP themselves, can be done by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if OTP challenge is active or not. - If enabled, you can allow them to disable it, or vice versa. To know if the user has enabled OTP, you can create an API on your backend that calls the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isOTPEmailEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.OTP_EMAIL); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_otp_email_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.OTP_EMAIL in factors ``` If the user wants to enable or disable `otp-email`, you can create an API on your backend that calls the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL); } async function disableMFAForUser(userId: string) { await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str) -> None: await add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) ``` :::note[If instead you want to work with `otp-phone`, you can replace `otp-email` with `otp-phone` in the above snippets. Also make sure that the `contactMethod` configures to `PHONE` in the Passwordless recipe on the frontend (for pre-built UI) and backend.] ::: ## Multi tenant setup ### Backend setup A user can be a part of multiple tenants. If you want OTP to be active for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#backend-setup) section above. However, if you want OTP to be active for a specific user, for a specific tenant (or a subset of tenants that the user is a part of), then additional logic must be added to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#backend-setup) section above: #### Example 1: Only enable OTP for users that have an `admin` role :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); if ( roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL) ) { // we only want otp-email for admins return [MultiFactorAuth.FactorIds.OTP_EMAIL]; } else { // no MFA for non-admin users. return []; } }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, passwordless, session, thirdparty, userroles, ) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union from supertokens_python.recipe.userroles.asyncio import get_roles_for_user async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if ( "admin" in roles.roles and FactorIds.OTP_EMAIL in await required_secondary_factors_for_tenant() ): # We only want OTP_EMAIL for admins return [FactorIds.OTP_EMAIL] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), userroles.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` - This override requires `otp-email` only when the user has the `admin` role and the tenant's `requiredSecondaryFactors` includes `otp-email`. #### Example 2: Ask for OTP only for users that have enabled OTP on their account :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", override: { apis: (oI) => { return { ...oI, consumeCodePOST: async function (input) { let response = await oI.consumeCodePOST!(input); if (response.status === "OK" && input.session !== undefined) { // We do this only if a session exists, which means that it's not being called for first factor login. // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the OTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL, ); } return response; }, }; }, }, }), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { return [MultiFactorAuth.FactorIds.OTP_EMAIL]; } } // no otp-email required for input.user, with the input.tenant. return []; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink, ) from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List, Optional, Union from supertokens_python.recipe.passwordless.interfaces import ( APIInterface, APIOptions, ConsumeCodePostOkResult, ) async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: if FactorIds.OTP_EMAIL in await required_secondary_factors_for_user(): if FactorIds.OTP_EMAIL in await required_secondary_factors_for_tenant(): return [FactorIds.OTP_EMAIL] # no otp-email required for input.user, with the input.tenant. return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation def passwordless_override(original_implementation: APIInterface): original_consume_code_post = original_implementation.consume_code_post async def consume_code_post( pre_auth_session_id: str, user_input_code: Union[str, None], device_id: Union[str, None], link_code: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): response = await original_consume_code_post( pre_auth_session_id, user_input_code, device_id, link_code, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) if isinstance(response, ConsumeCodePostOkResult) and session is not None: await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.OTP_EMAIL ) return response original_implementation.consume_code_post = consume_code_post return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig(apis=passwordless_override), ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` The `getMFARequirementsForAuth` override requires `otp-email` only when it is enabled for the user and the tenant's `requiredSecondaryFactors` includes `otp-email`. This lets the same user require OTP in selected tenants only. ### Frontend setup The frontend setup is identical to the [frontend setup](#frontend-setup) section above. ## Protecting frontend and backend routes See the section on [protecting frontend and backend routes](../protect-routes). ## Email / SMS sending and design By default, the email template used for otp-email login is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and SMS sending service that is available. If you would like to learn more about this, change the content of the email, or change the method by which messages are sent, check out the email / SMS delivery section in the recipe docs: - [Email delivery configuration](/platform-configuration/email-delivery) - [SMS delivery configuration](/platform-configuration/sms-delivery) --- # Embed the pre-built UI component Source: https://supertokens.com/docs/additional-verification/mfa/embed-the-prebuilt-ui ## Overview ## Before you start These instructions only apply to interfaces that use the pre-built UI components. If you are using a custom UI, the embed instructions depend on your implementation details. The tutorial configures `TOTP` as a secondary factor, but the same set of steps are applicable for other secondary factor types. --- ## Render the TOTP Widget in a page The following example shows the scenario where you have a dedicated route, such as `/totp`, for rendering the TOTP Widget. Upon a successful login, the user will be automatically redirected to the return value of `getRedirectionURL` (defaulting to `/`). #### With React Router ##### React Router v6 :::warning[Not applicable to non-react apps. Please build your own custom UI instead.] ::: ```tsx check=false reason="application example imports local modules defined elsewhere" import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Header from "./header"; import Footer from "./footer"; import { useNavigate } from "react-router-dom"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ TOTP.init({ totpMFAScreen: { disableDefaultUI: true, }, }), MultiFactorAuth.init({ getRedirectionURL: async (context) => { if (context.action === "GO_TO_FACTOR") { if (context.factorId === "totp") { return "/totp"; } } }, }), // ... ], }); function TOTPPage() { const navigate = useNavigate(); return (
); } ```
##### React Router v5 ```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Header from "./header"; import Footer from "./footer"; import { useHistory } from "react-router-dom5"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ TOTP.init({ totpMFAScreen: { disableDefaultUI: true, }, }), MultiFactorAuth.init({ getRedirectionURL: async (context) => { if (context.action === "GO_TO_FACTOR") { if (context.factorId === "totp") { return "/totp"; } } }, }), // ... ], }); function TOTPPage() { const history = useHistory(); return (
); } ```
#### Without React Router ```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Header from "./header"; import Footer from "./footer"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ TOTP.init({ totpMFAScreen: { disableDefaultUI: true, }, }), MultiFactorAuth.init({ getRedirectionURL: async (context) => { if (context.action === "GO_TO_FACTOR") { if (context.factorId === "totp") { return "/totp"; } } }, }), // ... ], }); function TOTPPage() { return (
); } ```
In the above code snippet, we: 1. Disabled the default TOTP UI by setting `disableDefaultUI` to `true` inside the TOTP recipe config. 2. Overrode the `getRedirectionURL` function inside the MFA recipe config to redirect to `/totp` whenever we want to show the TOTP factor. Feel free to customize the redirection URLs as needed. --- ## Render the TOTP Widget in a popup The following example shows the scenario where you embed the TOTP Widget in a popup, and upon successful login, you aim to close the popup. This is especially useful for step up auth. #### With React Router ##### React Router v6 :::warning[Not applicable to non-react apps. Please build your own custom UI instead.] ::: ```tsx import React, { useEffect, useRef, useState } from "react"; import Modal from "react-modal"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; import { useLocation, useNavigate } from "react-router-dom"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ TOTP.init(/* ... */), MultiFactorAuth.init(/* ... */), // ... ], }); function TOTPPopup() { const sessionContext = Session.useSessionContext(); const navigate = useNavigate(); const location = useLocation(); const retryStarted = useRef(false); const [isModalOpen, setIsModalOpen] = useState(false); const [error, setError] = useState(); const openModal = () => { const url = new URL(window.location.href); const returnUrl = new URL(url); returnUrl.searchParams.delete("stepUp"); returnUrl.searchParams.delete("redirectToPath"); returnUrl.searchParams.set("retryProtectedOperation", "true"); url.searchParams.delete("retryProtectedOperation"); url.searchParams.set("stepUp", "true"); url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`); window.history.replaceState(window.history.state, "", url); retryStarted.current = false; setError(undefined); setIsModalOpen(true); }; const cancelModal = () => { const url = new URL(window.location.href); url.searchParams.delete("stepUp"); url.searchParams.delete("redirectToPath"); url.searchParams.delete("retryProtectedOperation"); window.history.replaceState(window.history.state, "", url); setIsModalOpen(false); }; useEffect(() => { const params = new URLSearchParams(location.search); if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) { return; } retryStarted.current = true; void (async () => { try { await MultiFactorAuth.resyncSessionAndFetchMFAInfo(); const response = await fetch("/api/sensitive-operation", { method: "POST" }); if (!response.ok) { throw new Error("The protected operation was rejected"); } const url = new URL(window.location.href); url.searchParams.delete("retryProtectedOperation"); navigate(`${url.pathname}${url.search}${url.hash}`, { replace: true }); setIsModalOpen(false); } catch (error) { setError(error instanceof Error ? error.message : "The protected operation failed"); } })(); }, [location.search, navigate]); if (sessionContext.loading) { return null; } return (
{

You are logged In!

UserId: {sessionContext.userId}

} {error !== undefined &&

{error}

}
); } ```
##### React Router v5 ```tsx import React, { useEffect, useRef, useState } from "react"; import Modal from "react-modal"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; import { useHistory, useLocation } from "react-router-dom5"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ TOTP.init(/* ... */), MultiFactorAuth.init(/* ... */), // ... ], }); function TOTPPopup() { const sessionContext = Session.useSessionContext(); const history = useHistory(); const location = useLocation(); const retryStarted = useRef(false); const [isModalOpen, setIsModalOpen] = useState(false); const [error, setError] = useState(); const openModal = () => { const url = new URL(window.location.href); const returnUrl = new URL(url); returnUrl.searchParams.delete("stepUp"); returnUrl.searchParams.delete("redirectToPath"); returnUrl.searchParams.set("retryProtectedOperation", "true"); url.searchParams.delete("retryProtectedOperation"); url.searchParams.set("stepUp", "true"); url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`); window.history.replaceState(window.history.state, "", url); retryStarted.current = false; setError(undefined); setIsModalOpen(true); }; const cancelModal = () => { const url = new URL(window.location.href); url.searchParams.delete("stepUp"); url.searchParams.delete("redirectToPath"); url.searchParams.delete("retryProtectedOperation"); window.history.replaceState(window.history.state, "", url); setIsModalOpen(false); }; useEffect(() => { const params = new URLSearchParams(location.search); if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) { return; } retryStarted.current = true; void (async () => { try { await MultiFactorAuth.resyncSessionAndFetchMFAInfo(); const response = await fetch("/api/sensitive-operation", { method: "POST" }); if (!response.ok) { throw new Error("The protected operation was rejected"); } const url = new URL(window.location.href); url.searchParams.delete("retryProtectedOperation"); history.replace(`${url.pathname}${url.search}${url.hash}`); setIsModalOpen(false); } catch (error) { setError(error instanceof Error ? error.message : "The protected operation failed"); } })(); }, [history, location.search]); if (sessionContext.loading) { return null; } return (
{

You are logged In!

UserId: {sessionContext.userId}

} {error !== undefined &&

{error}

}
); } ```
#### Without React Router ```tsx import React, { useEffect, useRef, useState } from "react"; import Modal from "react-modal"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ TOTP.init(/* ... */), MultiFactorAuth.init(/* ... */), // ... ], }); function TOTPPopup() { const sessionContext = Session.useSessionContext(); const retryStarted = useRef(false); const [isModalOpen, setIsModalOpen] = useState(false); const [error, setError] = useState(); const openModal = () => { const url = new URL(window.location.href); const returnUrl = new URL(url); returnUrl.searchParams.delete("stepUp"); returnUrl.searchParams.delete("redirectToPath"); returnUrl.searchParams.set("retryProtectedOperation", "true"); url.searchParams.delete("retryProtectedOperation"); url.searchParams.set("stepUp", "true"); url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`); window.history.replaceState(window.history.state, "", url); retryStarted.current = false; setError(undefined); setIsModalOpen(true); }; const cancelModal = () => { const url = new URL(window.location.href); url.searchParams.delete("stepUp"); url.searchParams.delete("redirectToPath"); url.searchParams.delete("retryProtectedOperation"); window.history.replaceState(window.history.state, "", url); setIsModalOpen(false); }; useEffect(() => { const params = new URLSearchParams(window.location.search); if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) { return; } retryStarted.current = true; void (async () => { try { await MultiFactorAuth.resyncSessionAndFetchMFAInfo(); const response = await fetch("/api/sensitive-operation", { method: "POST" }); if (!response.ok) { throw new Error("The protected operation was rejected"); } const url = new URL(window.location.href); url.searchParams.delete("retryProtectedOperation"); window.history.replaceState(window.history.state, "", url); setIsModalOpen(false); } catch (error) { setError(error instanceof Error ? error.message : "The protected operation failed"); } })(); }, []); if (sessionContext.loading) { return null; } return (
{

You are logged In!

UserId: {sessionContext.userId}

} {error !== undefined &&

{error}

}
); } ```
The `retryProtectedOperation` return marker is not proof that step-up authentication succeeded. After the factor flow returns, call `MultiFactorAuth.resyncSessionAndFetchMFAInfo()` to synchronize the session and retry the server-protected operation. The MFA freshness validator on the backend is authoritative and must reject the operation if the required factor is missing or too old. The **Cancel** button only cancels the popup; it must not retry or authorize the operation. --- # Hooks and overrides Source: https://supertokens.com/docs/additional-verification/mfa/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `mfa` recipe to your needs. Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Frontend event hooks The pre-built UI emits a few events that you can listen to on the frontend. As an example, you can use these for analytics: ```tsx import SuperTokens from "supertokens-auth-react"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import TOTP from "supertokens-auth-react/recipe/totp"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", onHandleEvent: (context) => { if (context.action === "PASSWORDLESS_CODE_SENT") { // this event is fired when the user has successfully sent out an OTP email / SMS } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { // This event is fired when the user's OTP has expired, or // they have reached the max limit of number of failed OTP attempts. } else if (context.action === "SUCCESS" && !context.createdNewSession) { // this event is fired when successfully completing the OTP email / SMS challenge // and if it's not used in first factor (cause we do !context.createdNewSession) } }, }), TOTP.init({ onHandleEvent: (context) => { if (context.action === "TOTP_DEVICE_CREATED") { // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. } else if (context.action === "TOTP_DEVICE_VERIFIED") { // this event is fired during factor setup, when the user has successfully verified the TOTP device } else if (context.action === "TOTP_CODE_VERIFIED") { // this event is fired when the user has successfully verified the TOTP code // marking the TOTP factor as completed } }, }), MultiFactorAuth.init({ firstFactors: [ /*...*/ ], onHandleEvent: (context) => { if (context.action === "FACTOR_CHOOSEN") { let chosenFactorId = context.factorId; // this event is fired when the user is shown the screen for // picking one factor out of a choice of multiple factors } }, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", onHandleEvent: (context) => { if (context.action === "PASSWORDLESS_CODE_SENT") { // this event is fired when the user has successfully sent out an OTP email / SMS } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { // This event is fired when the user's OTP has expired, or // they have reached the max limit of number of failed OTP attempts. } else if (context.action === "SUCCESS" && !context.createdNewSession) { // this event is fired when successfully completing the OTP email / SMS challenge // and if it's not used in first factor (cause we do !context.createdNewSession) } }, }), supertokensUITOTP.init({ onHandleEvent: (context) => { if (context.action === "TOTP_DEVICE_CREATED") { // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. } else if (context.action === "TOTP_DEVICE_VERIFIED") { // this event is fired during factor setup, when the user has successfully verified the TOTP device } else if (context.action === "TOTP_CODE_VERIFIED") { // this event is fired when the user has successfully verified the TOTP code // marking the TOTP factor as completed } }, }), supertokensUIMultiFactorAuth.init({ firstFactors: [ /*...*/ ], onHandleEvent: (context) => { if (context.action === "FACTOR_CHOOSEN") { let chosenFactorId = context.factorId; // this event is fired when the user is shown the screen for // picking one factor out of a choice of multiple factors } }, }), ], }); ``` ## Backend overrides It's a common use case to want to override the default behavior of SuperTokens after a user signs up or signs in. For example, you may want to change your database state whenever someone signs up. You can do this by overriding the sign up / sign in recipe functions in the backend SDK: - [Passwordless recipe](/authentication/passwordless/hooks-and-overrides) - [EmailPassword recipe](/authentication/email-password/hooks-and-overrides) - [ThirdParty recipe](/authentication/social/hooks-and-overrides) Since the sign up / sign in APIs share functionality for first factor and second factor login, your override applies to both first and second factor login. If you want to have different behavior for first and second factor login, you can use the `input` argument to the function to determine if the user is doing first or second factor login. The `input` argument contains the `session` object using which you can determine if the user is doing first or second factor login. If the `session` property is `undefined`, it means it's a first factor login, else it's a second factor login. In the links above, the code snippets check for `input.session === undefined` to determine if it's a first factor login. --- # Important concepts Source: https://supertokens.com/docs/additional-verification/mfa/important-concepts ## Overview If you are new to Multi-factor authentication, MFA, this page provides a quick summary of how it works and the main terminology used in it. MFA enhances security by requiring users to authenticate through: 1. **Initial login** The user enters their primary credentials, typically a username and password (first factor). 2. **Authentication challenge** Upon successful entry of the primary credentials, the user receives an authentication challenge, prompting them to provide a secondary factor, such as an OTP or biometric verification. 3. **Access granted ** Access to the account or service is only granted when both the primary and secondary factors are successfully verified. This layered approach reduces the risk of unauthorized access, as an attacker would need to compromise multiple authentication methods to gain access. ## Terminology ### Authentication challenge An authentication challenge is a prompt that requires the user to provide additional credentials beyond the primary factor to verify their identity. This typically involves requesting a secondary factor, such as entering an OTP received via SMS/email or approving a push notification from an authenticator app. Authentication challenges are crucial in detecting and preventing unauthorized access by requiring proof of possession or identity beyond a password. ### Factors Factors refer to the different categories of credentials used in MFA: - **Something You Know**: Information only the user knows, such as a password or personal identification number (`PIN`). - **Something You Have**: A physical item the user possesses, such as a smartphone or hardware token used for generating one-time passwords (OTPs). - **Something You Are**: Biometric characteristics unique to the user, such as fingerprints, facial recognition, or voice patterns. Each auth challenge has a factor ID in SuperTokens: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | | TOTP | `totp` | | WebAuthn/Passkeys | `webauthn` | These factor IDs get used to configure the MFA requirements for users (except the `access-denied` one). They are also used to indicate which authentication challenges have completed in the current session. #### Factor completion status You can determine the status of the authentication factors by checking the session's access token payload. In the payload you can find the following claim structure: ```json { "st-mfa": { // c stands for completed, and // Shows that only the emailpassword factor has been completed "c": { // the timestamp when the factor was completed "emailpassword": 1702877939 }, // v stands for value // In this example, the false value indicates that the MFA flow is not completed // Once the second factor is finalized v will change to true "v": false } } ``` Each time an authentication factor completes, the SuperTokens backend adds it to the `c` (completed) object, and then re-evaluates the `v` boolean based on the MFA requirements for the user. ### First factor vs. secondary factor - **First Factor**: The primary authentication method, traditionally a password or `PIN`. This is the initial layer of security in the authentication process. - **Secondary Factor**: An additional security layer that complements the first factor. Common secondary factors include OTPs sent via email/SMS, authenticator apps like Google Authenticator, or biometric data. Secondary factors enhance security by adding an extra step to the verification process. A clear distinction exists between first and additional factors in the SDK. When you call `MFA.init` on the backend, you need to provide a list of allowed first factors. If using multi-tenancy, you can configure the first factors on a per-tenant basis and leave the `init` array empty. The first factors are those allowed to create a new session, whereas any other factor can only modify an existing session. In case the user calls an additional factor's API without a session, the API responds with a `401` error. ### Account linking Account linking refers to the process of connecting multiple accounts or identities across platforms or services. In the context of MFA, account linking ensures that a user's secondary authentication factors (for example, an authenticator app or backup method) are correctly associated with their primary account. This is crucial for streamlined access and maintaining secure authentication across different services without the need to set up separate MFA methods for each account. #### The relation between account linking and MFA During account linking, the individual login methods create their own "recipe user", and each recipe user links to another recipe user to create one primary user. Theoretically, one can link any recipe user to another (there is no need for them to have the same email or phone number). However, for first factor automatic linking, only link login methods if they have been verified and have the same email. From an MFA point of view, whenever the user sets up a new passwordless factor (`otp-email` or `otp-phone`), this creates a new recipe user for the passwordless recipe. It then auto-links it to the existing session's recipe user. Therefore, it is necessary to enable account linking for MFA to work. In the MFA guide, first factor account linking is not enabled, but you can enable that by following the automatic account linking guide in other parts of the docs. --- ## See also --- # Initial setup Source: https://supertokens.com/docs/additional-verification/mfa/initial-setup Design and implement SuperTokens MFA in this application. Inspect the existing recipes, tenant model, and deployment type first. Ask which first factors, second factors, and enforcement policy are required if they cannot be inferred. Check managed-service and SDK support, configure account linking, tenant-specific factors, email verification dependencies, frontend and backend recipes, and protected routes. Do not commit secrets. Validate first-factor login, second-factor enrollment, challenge completion, recovery, and route protection with the relevant tests and build. ## Overview To integrate multi-factor authentication, MFA, in your application, you first need to decide on what factors you want to support and when to ask for them. The following guide shows you how to implement a basic setup while also covering customization methods. ## Before you start These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/additional-verification/mfa/important-concepts). If you plan to use the `otp-email` factor as a form of email verification, you also need to initialize the `emailverification` recipe in `REQUIRED` mode on the backend. This configuration ensures that the email verification process passes only if the originally provided email has been verified. ## Steps ### 1. Set up the backend #### 1.1 Enable account linking MFA requires account linking to be active. You can enable it in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import SuperTokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext, ) => { if (session === undefined) { // we do not want to do first factor account linking by default. To enable that, // please see the automatic account linking docs in the recipe docs for your first factor. return { shouldAutomaticallyLink: false, }; } if (user === undefined || session.getUserId() === user.id) { // if it comes here, it means that a session exists, and we are trying to link the // newAccountInfo to the session user, which means it's an MFA flow, so we enable // linking here. return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking from supertokens_python.types import User from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink from typing import Dict, Any, Optional, Union async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is None: # We do not want to do first factor account linking by default. # To enable that, please see the automatic account linking docs # in the recipe docs for your first factor. return ShouldNotAutomaticallyLink() if user is None or session.get_user_id() == user.id: # If it comes here, it means that a session exists, and we are trying to link the # account_info to the session user, which means it's an MFA flow, so we enable # linking here. return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', recipe_list=[ accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - The above snippet enables auto account linking only during the second factor and not for the first factor login. This means that if a user has an email password account, and then they login via Google (with the same email), those two accounts are not linked. However, if the second factor for logging in is email or phone OTP, then that passwordless account links to the first factor login method of that session. - `shouldRequireVerification: true` prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session. - If you also want to enable first factor automatic account linking, see [this link](/post-authentication/account-linking/automatic-account-linking). :::note[Account linking is also a paid feature. Enabling MFA enables the account-linking capability required by this flow, so you do not need to enable both features separately.] ::: #### 1.2 Configure the first factors We start by initializing the MFA recipe on the backend and specifying the list of first factors using their [factor IDs](/additional-verification/mfa/important-concepts#factors). You still have to initialize all the auth recipes in the `recipeList`, and configure them based on your needs. For example, the code below initializes `thirdparty`, `emailpassword` and `passwordless` recipes and sets the `firstFactor` array to be `["emailpassword", "thirdparty"]`. This means that email password and social login appear to the user as the first factor (using the `thirdparty` + `emailpassword` recipe), and `passwordless` serves as the second factor. :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Passwordless from "supertokens-node/recipe/passwordless"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init(contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"), multifactorauth.init(first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY]) ], ) ``` Other combinations of first factors exists. For example, if you want passwordless as the first factor, then you would init the passwordless recipe and add `"passwordless"` in the `firstFactors` array. For a multi-tenancy setup, where each tenant can have a different set of first factors, you can leave the `firstFactors` array as `undefined` in the `MultiFactorAuth.init`. Configure the `firstFactors` on a per-tenant basis when creating or updating a tenant as shown below: :::note[At the moment this feature is not supported through the Go SDK.] ::: Email Password enabled In the above setting, Email Password is active in the **Login Methods** section. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. By default, no login methods activate for a tenant. ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate(first_factors=[FactorIds.EMAILPASSWORD]) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate(first_factors=[FactorIds.EMAILPASSWORD]) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword"] }' ``` `firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. :::note[- If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default.] - To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. ::: `firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. :::note[- If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default.] - To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. ::: #### 1.3 Configure the second factor This section explains how to configure SuperTokens such that a second factor is necessary for all users during sign up and during sign in. TOTP serves as an example for the second factor. The following code snippet accomplishes this: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import SuperTokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Passwordless from "supertokens-node/recipe/passwordless"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... ThirdParty.init({ //... }), EmailPassword.init({ //... }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", }), totp.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [MultiFactorAuth.FactorIds.TOTP]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, totp, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [FactorIds.TOTP] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` In the above snippet, you configure email password and social login as the first factor, followed by TOTP as the second factor. After sign in or sign up, SuperTokens calls the `getMFARequirementsForAuth` function to get a list of secondary factors for the user. The returned value determines the boolean value of `v` that's stored in the session's access token payload. If the returned factor is already completed (it's in the `c` object of the session's payload), then the value of `v` is `true`, else `false`. In the above example, `"totp"` returns as a required factor for all users. However, you can also dynamically decide which factor to return based on the `input` arguments, which contains the `User` object, the `tenantId`, and the current session's access token payload. The default implementation of `getMFARequirementsForAuth` returns the set of factors specifically enabled for this user (see next section) or for the tenant (see later section). The output of this function can be more complex than a `string[]`. You can also return an object which tells SuperTokens that any one of the factors must satisfy: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [ { oneOf: [MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL], }, ]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, totp, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [FactorIds.TOTP, FactorIds.OTP_EMAIL] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [ { allOfInAnyOrder: [MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL], }, ]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, totp, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [{"allOfInAnyOrder": [FactorIds.TOTP, FactorIds.OTP_EMAIL]}] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let currentCompletedFactors = MultiFactorAuth.MultiFactorAuthClaim.getValueFromPayload( input.accessTokenPayload, ); if (MultiFactorAuth.FactorIds.TOTP in currentCompletedFactors.c) { // this means the totp factor is completed return [MultiFactorAuth.FactorIds.OTP_EMAIL]; } else { // this means we have not finished totp yet, and we want // to do that right after first factor login return [MultiFactorAuth.FactorIds.TOTP]; } }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, totp, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: current_completed_factors = MultiFactorAuthClaim.get_value_from_payload( access_token_payload ) if current_completed_factors and FactorIds.TOTP in current_completed_factors.c: # this means the totp factor is completed return [FactorIds.OTP_EMAIL] else: # this means we have not finished totp yet, and we want # to do that right after first factor login return [FactorIds.TOTP] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ```
:::note[You can return an empty array from `getMFARequirementsForAuth` if you don't want any further MFA done for the current user.] :::
For a multi tenant setup, you can configure a list of secondary factors when creating / modifying a tenant as shown below: :::note[At the moment this feature is not supported through the Go SDK.] ::: OTP - Email enabled As shown above, you turn on `OTP - Email` in the **Secondary Factors** section which means that all users who log into that tenant must complete `otp-email` as a second factor. You can also turn off all factors to have no secondary factors required for the tenant. If you turn on more than one factor, it means that the user must complete any one of factors that are active. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.OTP_EMAIL], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.OTP_EMAIL], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword"], "requiredSecondaryFactors": ["otp-email"] }' ``` In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. To remove the `requiredSecondaryFactors` configuration for a tenant, you can pass a `null` value for the `requiredSecondaryFactors` key in the tenant configuration. If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. To remove the `requiredSecondaryFactors` configuration for a tenant, you can turn off all the toggles. If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [ { allOfInAnyOrder: await input.requiredSecondaryFactorsForTenant, }, ]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, passwordless, session, thirdparty, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [{"allOfInAnyOrder": await required_secondary_factors_for_tenant()}] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ session.init(), emailpassword.init(), thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` Notice that the input to the function contains the `requiredSecondaryFactorsForTenant` array. This would be the same list that you passed to the tenant configuration when creating / modifying the tenant as shown in the previous steps. #### 1.4 Remove the second factor requirement (optional) Instead of configuring a factor for all users in your app, or for all users within a tenant, you may want to implement a flow in which users do MFA only if they have enabled it for themselves. Here, users may also want to choose what factors they would like to enable for themselves. This flow allows users to configure their MFA preferences in the settings page in your app's frontend. A pre-built UI for this is not yet provided, but in this section, we explain the setup on the backend. You want to start by creating an API that does [session verification](/additional-verification/session-verification/protect-api-routes), and then enable the desired factor for the user. For example, if the user wants to enable TOTP, then you would call the following function in your API: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str): await add_to_required_secondary_factors_for_user( user_id, FactorIds.TOTP ) ``` ```python from supertokens_python.recipe.multifactorauth.syncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str): add_to_required_secondary_factors_for_user( user_id, FactorIds.TOTP ) ``` The effect of the above function call is that in the default implementation of `getMFARequirementsForAuth`, the factors specifically enabled for the input user are considered. By default, if you add multiple factors for a user ID, then it would require them to complete any one of those secondary factors during login. If you want to change the default behavior from "any one of" to something else (like "all of"), you can do this by overriding the `getMFARequirementsForAuth` function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // ... MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [ { allOfInAnyOrder: await input.requiredSecondaryFactorsForUser, }, ]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.types import User from supertokens_python.recipe.multifactorauth.types import MFARequirementList def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: return [{"allOfInAnyOrder": await required_secondary_factors_for_user()}] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` Once you call the `addToRequiredSecondaryFactorsForUser` function for a user, SuperTokens stores this preference in the user metadata JSON of the user. For example, if you add `"totp"` as a required secondary factor for a user, this preference is stored in the metadata JSON as: ```json { "_supertokens": { "requiredSecondaryFactors": ["totp"] } } ``` You can view this JSON on the [user details page of the user management dashboard](/post-authentication/dashboard/user-management) and modify it manually if you like. To know the factors that a user has enabled, you can use the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_enabled_for_user(user_id: str): factors = await get_required_secondary_factors_for_user( user_id ) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_enabled_for_user(user_id: str): factors = get_required_secondary_factors_for_user( user_id ) return FactorIds.TOTP in factors ``` Using the above function, you can build your settings page on the frontend which displays the existing enabled factors for the user. Allow users to enable or disable factors as they like. Once you have enabled a factor for a user, you take them to that factor setup screen if they have not previously already setup the factor. To know if a factor is setup, you can call the following function (on the backend): :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpSetupForUser(userId: string) { let factors = await MultiFactorAuth.getFactorsSetupForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_enabled_for_user(user_id: str): factors = await get_factors_setup_for_user( user_id ) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_enabled_for_user(user_id: str): factors = get_factors_setup_for_user( user_id ) return FactorIds.TOTP in factors ``` Or you can call the [`MFAInfo` endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) from the frontend which returns information indicating which factors have already been setup for the user and which not. A factor is considered setup if the user has gone through that factor's flow at least once. For example, if the user has created and verified a TOTP device, only then does the `getFactorsSetupForUser` function return `totp` as part of the array. Likewise, if the user has completed `otp-email` or `link-email` once, only then do these factors become a part of the returned array. Let's take two examples: - The first time the user enables TOTP, then the result of `getFactorsSetupForUser` does not contain `"totp"`. You should redirect the user to the TOTP setup screen. Once they add and verify a device, then `getFactorsSetupForUser` returns `["totp"]` even if they later disable TOTP from the settings page and re-enable it. - Let's say that the first factor for a user is `emailpassword`, and the second factor is `otp-email`. Once they sign up, SuperTokens already knows the email for the user, when they are doing the `otp-email` step, then they are not asked to enter their email again (that is, an OTP is directly sent to them). However, until they actually complete the OTP flow, `getFactorsSetupForUser` does not return `["otp-email"]` as part of the output. :::warning[In the edge case that a factor is active for a user, but they sign out before setting it up, then when they login next, SuperTokens still asks them to complete the factor at that time. If SuperTokens doesn't have the required information (like no TOTP device for TOTP auth), then users need to set up a device at that point in time.] If you would like to change how this works and only want users to set up their factor via the settings page, and not during sign in, you can do this by overriding the `getMFARequirementsForAuth` function, which takes as an input the list of factors that are setup for the current user. ::: The subsequent sections in this doc walk through frontend setup, and also specific examples of common MFA flows. ### 2. Set up the frontend The pre-built UI provides support for the following MFA methods: - TOTP - Email / phone OTP If you want other types of MFA (like magic links, or password), please consider checking out the custom UI second. We start by initialising the MFA recipe on the frontend and providing the list of first factors as shown below: You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import supertokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init(/* ... */), Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init(/* ... */), supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", }), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, ], }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` In the above snippet, `thirdparty` and email password are configured as first factors. The second factor is determined [on the backend](/additional-verification/mfa/initial-setup#1-set-up-the-backend), based on the boolean value of [`v` in the MFA claim in the session](/additional-verification/mfa/important-concepts#factors). If the `v` is `false` in the session, it means that there are still factors pending before the user has completed login. In this case, the frontend SDK calls the `MFAInfo` endpoint (see more about this later) on the backend which returns the list of factors (`string[]`) that the user must complete next. For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. If you notice, in the above code snippet, `Passwordless.init` is also included, and this handles cases where the second factor is `otp-email` or `otp-phone`. For TOTP, a different recipe is used as shown later in this guide. For a multi-factor setup, the first factors are selected based on [the configuration of the tenant](/additional-verification/mfa/initial-setup#12-configure-the-first-factors). Each tenant has a `firstFactors` array configuration which determines the login options shown for that tenant. For MFA, the login options are determined by the [`requiredSecondaryFactors` configuration on the tenant](/additional-verification/mfa/initial-setup#13-configure-the-second-factor), or based on the customisations for `getMFARequirementsForAuth` on the backend. To tell the frontend to dynamically load the factors based on the tenant, four things need to supply: - The current `tenantId` - Enable dynamic login methods - Add `MultiFactorAuth.init` to the recipe list without any configured `firstFactors` - Init all the recipes that can be possibly used by any tenant as the first or second factor. You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import supertokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [ Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { // Implement the following based on the UX flow you want for // tenant discovery return "TODO.."; }, }; }, }, }), EmailPassword.init(/* ... */), Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", }), MultiFactorAuth.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [ supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { // Implement the following based on the UX flow you want for // tenant discovery return "TODO.."; }, }; }, }, }), supertokensUIEmailPassword.init(/* ... */), supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", }), supertokensUIMultiFactorAuth.init(), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` - In the above code snippet, `ThirdPartyEmailPassword` and `Passwordless` are included as the auth methods. This works for a variety of use cases like: - The first factor for any tenant can be third party or email password login, and the second factor can be passwordless login (`otp-email` or `otp-phone`). - The first factor for any tenant can be email password, with, or without a second factor (like `otp-email`).. - The first factor for any tenant can be third party, with, or without a second factor (like `otp-email`).. - The first factor for any tenant can be passwordless login (with magic link), with or without a second factor (like `otp-email`). - You can even change `passwordles.init` to using `thirdpartypasswordless.init` if you want to have the first factor for any tenant to be `thirdparty` or passwordless login, with or without a second factor (like `otp-email`). - The `MultiFactorAuth` is configured without any configured `firstFactors` because the frontend is set to dynamically load the first factors based on the tenant. Therefore, `usesDynamicLoginMethods: true` is included in the `SuperTokens.init` call. - The `Multitenancy` is configured as well, and a skeleton for `getTenantId` is provided. You need to implement this function based on the UX flow desired for tenant discovery. For example, [here is a common UX flow in which the tenant ID is determined based on the current sub domain](/authentication/enterprise/subdomain-login). :::note[- If you do initialize the `firstFactors` array for `MultiFactorAuth.init()` on the frontend, it is not considered when `usesDynamicLoginMethods: true` is included.] - If the tenant doesn't have the `firstFactors` array set, then the list of first factors that appear is determined by the [login methods that are enabled in that tenant's configuration](/authentication/enterprise/manage-tenants#create-a-new-tenant). ::: The second factor for a tenant is selected based on the [`secondaryFactors` configuration for the tenant](/additional-verification/mfa/initial-setup#13-configure-the-second-factor), or based on any custom implementation for the `getMFARequirementsForAuth` function. If the current user has specific MFA methods enabled for them, those are also shown as options as well. Overall, the list of secondary factors is used to build the `next` array returned from the `MFAInfo` endpoint (see more about this later). For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. In the subsequent sections, specific MFA setup examples are given for your reference. #### Usage with email verification If you are also requiring email verification, the user must verify the email first, and then all the MFA challenges. For example, if the user has email password as the first factor, and then TOTP as a second factor, SuperTokens prompts the user to do email password login, followed by email verification, followed by TOTP. To switch the order such that email verification happens after the secondary factors of MFA, follow the next code snippet. ```tsx import supertokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import Session from "supertokens-auth-react/recipe/session"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes... EmailVerification.init({ mode: "REQUIRED", }), Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getGlobalClaimValidators: (input) => { let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find( (v) => v.id === EmailVerification.EmailVerificationClaim.id, )!; let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter( (v) => v.id !== EmailVerification.EmailVerificationClaim.id, ); return [...filteredValidators, emailVerificationClaimValidator]; }, }; }, }, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes... supertokensUIEmailVerification.init({ mode: "REQUIRED", }), supertokensUISession.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getGlobalClaimValidators: (input) => { let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find( (v) => v.id === supertokensUIEmailVerification.EmailVerificationClaim.id, )!; let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter( (v) => v.id !== supertokensUIEmailVerification.EmailVerificationClaim.id, ); return [...filteredValidators, emailVerificationClaimValidator]; }, }; }, }, }), ], }); ``` In the snippet above, the `getGlobalClaimValidators` function in the Session recipe is overridden to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which redirects the user to complete the MFA factors. #### Handle misconfigurations There can be situations of misconfigurations. For example you may have enabled `otp-email` for a user as a secondary factor, but did not add `Passwordless` (or `ThirdPartyPasswordless`) in the `recipeList` on the frontend. In such (and similar) situations, the pre-built UI on the frontend throws an error which is sent to the error boundary of your app. The way to solve these errors is to recheck the `recipeList` on the frontend, and make sure that it has all the recipes initialized that are necessary for any factor configured on the backend. #### The access denied screen Sometimes, users may end up seeing [an access denied screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup-access-denied-reload) during the login flow. This appears if there is a 500 (backend sends a 500 status code) error during the MFA flow for API calls that are automatically initiated (without user action). For example: - When the user wants to setup a new `TOTP` device, the pre-built UI calls the `createDevice` function from the `totp` recipe on page load, and if that fails, users see the access denied screen asking them to retry. - When the user needs to complete an OTP email factor, and if the API call to send an email (which starts on page load) fails, then users see the access denied screen asking them to retry. You can override this component in the following way: :::warning[You cannot override the pre-built UI in non react apps yet.] ::: ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session"; function App() { return ( { return (
Access denied! {props.error === undefined ? null : props.error}
); }, }} > {/* Rest of the JSX */}
); } export default App; ```
```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui"; import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session"; function App() { if ( canHandleRoute([ /*...*/ ]) ) { return ( { return (
Access denied! {props.error === undefined ? null : props.error}
); }, }} > {getRoutingComponent([ /*...*/ ])}
); } return {/* Rest of the JSX */}; } export default App; ```
After the first factor sign in is over, to know the next auth challenge, the frontend should rely on the session's access token payload MFA claim's `n` array. For example, the access token payload may have the following content: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` This means that the user has completed the email password login, and that there are still MFA login challenge(s) remaining (`v` is `false`). #### 1.1 Initialize the MFA recipe :::success[This step is not applicable for mobile apps. Please continue reading.] ::: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... supertokensMultiFactorAuth.init(), ], }); ``` #### 1.2 Add the MFA flow The overall lifecycle of a factor post sign in is as follows: 1. **Asking for the first factor** This is the same as setting up a recipe per the other recipe guides. Please follow those. 2. **Checking the v boolean value in the MFA claim** After the first factor is complete, the frontend needs to check if there are any pending MFA challenges. This can be done by reading the `v` claim from the session as shown below: ```tsx import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Session from "supertokens-web-js/recipe/session"; async function isAllMFACompleted() { if (await Session.doesSessionExist()) { let mfaClaim = await Session.getClaimValue({ claim: MultiFactorAuth.MultiFactorAuthClaim, }); if (mfaClaim === undefined) { // this can happen during migration where the session is an older one // that was created before MFA was introduced on the backend return true; } else { return mfaClaim.v; } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function isAllMFACompleted() { if (await supertokensSession.doesSessionExist()) { let mfaClaim = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim, }); if (mfaClaim === undefined) { // this can happen during migration where the session is an older one // that was created before MFA was introduced on the backend return true; } else { return mfaClaim.v; } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); } } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function isAllMFACompleted() { if (await SuperTokens.doesSessionExist()) { let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; return isMFACompleted; } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun isAllMFACompleted(): Boolean { val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean; return isMFACompleted; } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func isAllMFACompleted() -> Bool { // Attempt to retrieve the access token payload securely if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), // Extract the mfaObject from the accessTokenPayload let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any], // Determine if MFA has been completed let isMFACompleted: Bool = mfaObject["v"] as? Bool { // Return the MFA completion status return isMFACompleted } // Return false if any of the unwrapping fails, indicating MFA completion status cannot be confirmed return false } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future isAllMFACompleted() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-mfa")) { Map mfaObject = accessTokenPayload["st-mfa"]; if (mfaObject.containsKey("v")) { bool isMFACompleted = mfaObject["v"]; return isMFACompleted; } } return false; // Return false if "st-mfa" is not present or "v" is not found } ``` 3. **Checking the next array** Once it is verified that MFA is still pending, the list of factors the user must do next needs to be retrieved. This can be done by calling the [MFA Info endpoint](#the-mfa-info-endpoint) which returns a list of next (`string[]`) factors: - If there are multiple values in this array, then the frontend needs to show these options to the user and ask them to pick one of them. - If there is only one item, then the UI can directly ask the user to complete that factor. - If this array is empty, then: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. 4. **Checking for factor setup** Once the user has picked a specific factor (or if `next` contains only one item), you need to check if that factor has already been setup for that user. A factor is setup already if: - For `totp`: The user has already added a `totp` device and verified it. - For `otp-email`: The user has a passwordless `loginMethod` that has an email associated with it. - For `link-email`: The user has a passwordless `loginMethod` that has an email associated with it. Note that this is not a valid secondary factor, but is a valid first factor. - For `otp-phone`: The user has a passwordless `loginMethod` that has a phone number associated with it. - For `link-phone`: The user has a passwordless `loginMethod` that has a phone number associated with it. Note that this is not a valid secondary factor, but is a value first factor. - For `emailpassword`: The user has an email password `loginMethod`. - For `thirdparty`: The user has a third party `loginMethod`. If the user has the factor already setup, you can skip the setup step and directly ask them for the challenge: - For `totp`: Ask them to enter the OTP. - For `otp-email`: Send them an email with the OTP, and ask them to enter the OTP. - For `otp-phone`: Send them an SMS with the OTP, and ask them to enter the OTP. - For `emailpassword`: Ask them to enter their password. - For `thirdparty`: Ask them to login using the third party provider. In case the user does not have the factor setup, you need to ask them to set it up first: - For `totp`: Ask them to scan the QR code and enter the TOTP to verify the device. - For `otp-email`: Ask them to enter their email and send them an email with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. - For `otp-phone`: Ask them to enter their phone number and send them an SMS with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's phone number from another login method (see later), you do not need to ask them to enter their phone number again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. - For `emailpassword`: Ask them to enter their email and password. Once they enter the password, an email password user is created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. Here you would be calling the sign up API, vs in the other case (where the factor is already setup), you would be calling the sign in API. - For `thirdparty`: Ask them to login using the third party provider. Once they login, a third party user is created and associated with their user object. In the later guides of this recipe, the use cases are described. If you want to know the status of any factor, you can get that by calling the [MFA Info endpoint](#the-mfa-info-endpoint). ## References ### The MFA info endpoint This is an important endpoint which can be utilized to: - Know which factors are pending for the user (referred to as the the `next` array in the documentation). - Update the `v` and `c` values in the MFA claim. - Get a list of all factors that are already setup for the session user. - For each factor, get a list of emails / phone numbers that can be utilized for that factor. Our pre-built UI uses this API automatically, but you can also always call this API manually if you are building a custom UI: Call the following API when you want to know the status of any factor. Notice that the API call requires the session's access token as an input (this should be included by the frontend SDK automatically): ```tsx import MultifactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Session from "supertokens-web-js/recipe/session"; async function fetchMFAInfo() { if (await Session.doesSessionExist()) { try { let mfaInfo = await MultifactorAuth.resyncSessionAndFetchMFAInfo(); let factorEmails = mfaInfo.emails; let factorPhoneNumbers = mfaInfo.phoneNumbers; let emailsForOTPEmail = factorEmails["otp-email"]; let phoneNumbersForOTPPhone = factorPhoneNumbers["otp-phone"]; let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); let isOTPPhoneSetup = mfaInfo.factors.alreadySetup.includes("otp-phone"); let next = mfaInfo.factors.next; let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function fetchMFAInfo() { if (await supertokensSession.doesSessionExist()) { try { let mfaInfo = await supertokensMultiFactorAuth.resyncSessionAndFetchMFAInfo(); let factorEmails = mfaInfo.emails; let factorPhoneNumbers = mfaInfo.phoneNumbers; let emailsForOTPEmail = factorEmails["otp-email"]; let phoneNumbersForOTPPhone = factorPhoneNumbers["otp-phone"]; let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); let isOTPPhoneSetup = mfaInfo.factors.alreadySetup.includes("otp-phone"); let next = mfaInfo.factors.next; let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); } } ``` ```bash curl --location --request PUT '/auth/mfa/info' \ --header 'Authorization: Bearer ...' ``` - In the above code snippet, the list of factors which the user must complete next (in the `next` array) is retrieved along with all the relevant information to know what state each factor is in to decide if the user should be prompted to setup the factor (for example create a new TOTP device), or solve the auth challenge instead (for example, showing the enter TOTP screen). - The function is called `resyncSessionAndFetchMFAInfo` because it does two things: - fetches the MFA info that you can consume to know the `next` array and what state each factor is in. - resynchronizes the value of the `v` and `c` in the session's MFA claim. - The structure of the raw JSON response is as follows: ```json { "status": "OK", "factors": { "alreadySetup": ["totp", "otp-email", "..."], "allowedToSetup": ["otp-phone", "otp-email", "..."], "next": ["otp-phone", "..."] }, "emails": { "otp-email": ["user1@example.com", "user2@example.com"], "link-email": ["user1@example.com", "user2@example.com"] }, "phoneNumbers": { "otp-phone": ["+1234567890", "+1098765432"], "link-phone": ["+1234567890", "+1098765432"] } } ``` - `factors.alreadySetup` is an array that contains all factors that have been setup by the user. If the current factor is a part of this array, it means that you can directly take the user to the factor challenge screen. If your factor depends on an email or phone number (like in the case of `otp-phone` or `otp-email`), then you can find the email or phone number in the `emails` or `phoneNumbers` object in the response, keyed by the current factor ID. - `factors.allowedToSetup` is an array that contains all factors that the user can setup at this point. This is not that useful during the sign in process, but may be useful post sign in if you want to know what are the factors that the user can setup at any point in time. - `emails` is an object in which the key are all the factor IDs supported by SuperTokens (and any custom factor ID added by you). The values against each of the keys is a list of emails that can be utilized to complete the factor. The first email (index 0) in the list is the preferred email to use for the factor. The order is determined based on the first factor chosen by the user, and if the factor was already setup or not. If the array is empty, it means that there is no email associated with the user for that factor. This can happen only if the factor was not already setup. In this case, you should take the user to a screen to ask them to first enter an email, and then to the challenge screen. The flow is further explained in the common flows guide later on. - `phoneNumbers` is similar to the `emails` object, except that it contains phone numbers for factors that are dependent on phone numbers. - The `factors.next` array determines the list of factors which the user must completed next. For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items: - For the pre-built UI, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - For custom UI, you would need to make this screen on your own. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `checkAllowedToSetupFactorElseThrowInvalidClaimError` function, on the backend, to not allow a factor setup until the email is verified. ### Handle support cases Some situations exist in which users may be locked out of their accounts and would need you to do certain steps to unlock their accounts. These cases are: - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_009)" } ``` - This can happen if the email password account you are trying to do MFA with is not verified. - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_010)" } ``` - This can happen if the email password account you are trying to do MFA with is already linked to another primary user that is not equal to the session user. - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_011)" } ``` - This can happen if the email password account you are trying to do MFA cannot link to the session user because there already exists another primary user with the same email. - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_012)" } ``` - To link the email password user with the session user, it must be confirmed that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is sent to the frontend. - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_013)" } ``` - An example scenario of when in the following scenario: - A user signs up with their phone number and OTP - Post sign up, they are prompted to add their email and a password for the account. In this case, since the entered email is not verified, this error will display. - To resolve this, it is advised to change the flow to first ask the user to go through the email OTP flow post the first factor sign up, and then add a password to the account. This way, the email will be verified. - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is set up to not have automatic account linking during the first factor. - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. - The user logs out, and then creates a social login account with email `e1`. Then, they are prompted to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. - To resolve this, it is advised to manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" } ``` - An example scenario of when in the following scenario: - A user creates a social login account with email `e1` which becomes a primary user. - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. - The user is prompted to add a password for the new account with an option to also specify an email with it (this is strange, but theoretically possible). They now enter the email `e1` for the email password account. - This will cause this type of error since the linking of the new social login and email account will fail since there already exists another primary user with the same (`e1`) email. - To resolve this, it is advised not allowing users to specify an email when asking them to add a password for their account. - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is set up to not have automatic account linking during the first factor. - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. - The user logs out and creates another social login account with email `e1` (say `GitHub`), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the `GitHub` login a primary user, but fail, since the email `e1` is already a primary user (with Google login). - To resolve this, it is advised to manually link the `e1` `GitHub` social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code/consume POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_017)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but it can't because the passwordless account is already linked with another primary user. - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code/consume POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_018)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but it can't because there exists another primary user with the same email as the passwordless account. - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code POST` or `/signinup/code/consume POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_019)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but, the first factor account cannot become a primary user because there exists another account with the same email as the first factor user account which is already primary. If you are using otp-email MFA factor as a form of email verification, you should also have `emailverification` recipe initialised in `REQUIRED` mode on the backend (no need to add it on the frontend since users won't see that UI). This is for security reasons wherein during the sign up process, when asking for the otp-email challenge, the email the OTP is sent to is determined on the frontend (automatically). In this case, the following scenario is possible: - User signs up with email `A` - An email OTP challenge is displayed to the user, and an OTP to email `A` is sent automatically. - The user manually calls the OTP create code API with email `B` and their session token, and verifies the OTP via a call to the consume code API. - The user refreshes the page and the otp-email challenge is complete. Of course, this is not the desired flow when you want to use otp-email as a form of email verification. To prevent this, you should have the `emailverification` recipe initialised in `REQUIRED` mode on the backend. This ensures that the email verification claim validator only passes if the email that's verified is the one from the first factor (email `A`). The above case is only possible during sign up, and not sign in. ::: ### Security considerations SuperTokens enforces that a user has completed all the required factors by keeping track of and checking them in the user's access token payload. - If a user is required to complete a MFA challenge, for example TOTP, if they already have a verified TOTP device, they cannot setup any other factor before completing this factor challenge, and if they do not yet have a verified TOTP device, then the only action they are allowed to take is to create a new TOTP device. This ensures that a user cannot bypass the MFA challenges of the current or future step. - When a user creates a new TOTP device, it cannot be utilized unless they first verify it by entering the initial TOTP code. - If the email of the 2nd factor login method is not confirmed, by default, it is not allowed to be setup or used as a 2nd factor, unless the session user has a login method that has the same email which is verified. - A fixed number of times (5 times by default) a user can enter an invalid TOTP code, after which they have to wait for 15 minutes before trying again. This timeout and the max attempts count can be modified in the core configuration. - During sign up (not sign in), for email / SMS OTP challenge, the email / SMS that the OTP is sent to is determined by the frontend. This is intentional because it allows you to create a flow in which the email the OTP is sent to may not be the same as the login method of the first factor. However, from a security point of view, it allows a malicious actor to send an OTP to a different email / phone number than the first factor's phone or email. This is not an issue if you are using email OTP as a method for email verification because the email verification recipe checks that the email of the first factor is verified, and in the case of the malicious user, the email of the first factor won't be verified because they entered a different email for otp-email challenge. --- ## See also --- # Multi-Factor Authentication Source: https://supertokens.com/docs/additional-verification/mfa/introduction ## MFA summary - SuperTokens supports email or SMS OTP, TOTP, and WebAuthn or Passkeys as MFA factors. - Require an additional challenge for sensitive routes or actions with step-up authentication. - Magic links work only as a first factor with the prebuilt UI because the link may open on another device. Use email or SMS OTP as a later factor instead. - MFA is available for managed SuperTokens deployments. ## Overview Multi-factor authentication (MFA) is a security process that requires users to verify their identity through multiple forms of credentials before gaining access to a system. **SuperTokens** allows you to integrate MFA in your application using Email/SMS One-Time Password (OTP), Time-based One-Time Password (TOTP), or WebAuthn/Passkeys. ## Prerequisites Magic link via email or SMS is only supported as a first factor for pre-built UI. It will not work as a second factor because if the magic link is opened on a different device, there would be no reference to the existing session (which was created before first factor completion). Instead, you can use OTP based authentication, using email or SMS. It achieves the same level of security as a magic link. ## Getting started The quickest way to get a glimpse of how MFA works with **SuperTokens** is to use the example app. Run the following command to get started: ```bash npx create-supertokens-app@latest --recipe=multifactorauth ``` Besides that, you can check the initial quickstart guide for step-by-step instructions, along with the other guides for more specific use cases. Before you explore a guide, read through the **Important Concepts** page first. It explains multiple topics that get used in each tutorial. Go through a quick explanation of how MFA works and some common terminologies. Implement an authentication flow that uses MFA. Require additional authentication challenges on specific routes or actions. Allow users to recover their account if they lose access to one of the factors. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Force all users to use TOTP. Enable TOTP only for some of the users. Force all users to use OTP. Enable OTP only for some of the users. Use WebAuthn as a secondary factor Check for the MFA status on specific routes. --- # Protect frontend and backend routes Source: https://supertokens.com/docs/additional-verification/mfa/protect-routes ## Overview This page shows you how to protect your frontend and backend routes to make them accessible only when the user has finished all the MFA challenges configured for them. In both the backend and the frontend, routes are protected based on value of the MFA claim, in the session's access token payload. :::caution[The backend is the authorization boundary] Frontend MFA checks only control rendering and navigation. They can be bypassed. Every protected API must verify the session and enforce the MFA claim on the backend; never rely on a mobile or web payload check to protect backend data. ::: ## Before you start :::info This guide only applies to scenarios involving **SuperTokens Session Access Tokens**. ::: One thing to note here is that, with **OAuth2 Access Tokens**, you don't need to check the MFA claims. You will get the token once the MFA flow is done. --- ## Protect API routes When you call `MultiFactorAuth.init` in the `supertokens.init` on the backend, SuperTokens **automatically adds a session claim validator globally**. This validator checks that the value of `v` in the [MFA claim](./important-concepts#factors) is `true` before allowing the request to proceed. If the value of `v` is `false`, the validator will send a 403 error to the frontend. :::note[This validator is added globally, which means that every time you use `Verify Session` or `Get Session` from the backend SDKs, this check will happen.] This means that you don't need to add any extra code on a per API level to enforce MFA. ::: ### Exclude routes from the default check To exclude the default validator check in a certain backend route, you have to update `Verify Session` call. :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, ], }, handler: async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function updateBlog(awsEvent: SessionEvent) { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let router = new KoaRouter(); router.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (ctx: SessionContext, next) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; class Example { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), ) @response(200) async handler() { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; export default async function example(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })(req, res, next); }, req, res, ); // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; @Controller() export class ExampleController { @Post("example") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), ) async postExample(@Session() session: SessionContainer): Promise { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators return true; } } ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] ) )): # All validator checks have passed and the user has a verified email address pass ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim @app.route('/update-jwt', methods=['POST']) @verify_session( # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim @verify_session( # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; 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 }); } // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators return NextResponse.json({}); }, { overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }, ); } ``` The same modification can be done for `getSession` as well. ### Check MFA claim manually To account for a more complex logic when you check the MFA claim (other than checking if `v` is `true`), look over the next code snippet. :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, ], }, handler: async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), }, async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; async function updateBlog(awsEvent: SessionEvent) { let mfaClaimValue = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await awsEvent.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } } exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), async (ctx: SessionContext, next) => { let mfaClaimValue = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await ctx.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; class Example { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), ) @response(200) async handler() { let mfaClaimValue = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await (this.ctx as any).session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; export default async function example(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, })(req, res, next); }, req, res, ); let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: await superTokensNextWrapper( async (next) => { throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); }, req, res, ); } } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; @Controller() export class ExampleController { @Post("example") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }), ) async postExample(@Session() session: SessionContainer): Promise { let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); } return true; } } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post("/update-blog") async def update_blog_api(session: SessionContainer = Depends(verify_session())): mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. # This can happen if the session was created prior to enabling the MFA recipe on the backend. # So here, we add the value of the MFA claim to the session: await session.fetch_and_set_claim(MultiFactorAuthClaim) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None completed_factors = mfa_claim_value.c if "totp" not in completed_factors: # The user has not finished TOTP. We throw a claim validation error: raise_invalid_claims_exception( "User has not finished TOTP", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP ``` ```python check=false reason="route fragment assumes an existing framework application" from flask import Flask, g from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route('/update-blog', methods=['POST']) @verify_session() def check_mfa_api(): session: SessionContainer = g.supertokens mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. # This can happen if the session was created prior to enabling the MFA recipe on the backend. # So here, we add the value of the MFA claim to the session: session.sync_fetch_and_set_claim(MultiFactorAuthClaim) mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None completed_factors = mfa_claim_value.c if "totp" not in completed_factors: # The user has not finished TOTP. We throw a claim validation error: raise_invalid_claims_exception("User has not finished TOTP", [ ClaimValidationError(MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }) ]) # If we reach here, it means the user has completed TOTP ``` ```python check=false reason="session attribute is injected by framework middleware" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def get_user_info_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. # This can happen if the session was created prior to enabling the MFA recipe on the backend. # So here, we add the value of the MFA claim to the session: await session.fetch_and_set_claim(MultiFactorAuthClaim) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None completed_factors = mfa_claim_value.c if "totp" not in completed_factors: # The user has not finished TOTP. We throw a claim validation error: raise_invalid_claims_exception( "User has not finished TOTP", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { backendConfig } from "@/app/config/backend"; import { Error as STError } from "supertokens-node/recipe/session"; SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession( request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { // this means that there is no MFA claim information in the session. This can happen if the session was created // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session // in the following way: await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { // the user has finished totp } else { // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a // claim validation error in the following way: const error = new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: "totp", }, }, ], }); return NextResponse.json(error, { status: 403 }); } return NextResponse.json({}); }, { overrideGlobalClaimValidators: async (globalValidators) => { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, }, ); } ``` - In the code snippet above, we remove the default validator that was added to the global validators (which checks if the `v` value in the claim is true or not). You don't need to do this, but in the code snippet above, we show it anyway. - Then in the API logic, we manually fetch the claim value, and then check if TOTP has been completed or not. If it hasn't, we send back a 403 error to the frontend. You can use a similar approach as shown above to do any kind of check. :::info[important] If you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true`. This would be equivalent to doing a check as our default claim validator mentioned above. Make sure to also do other checks on the JWT payload. For example, if you require all users to have finished email verification, then we need to check for that claim as well in the JWT. ::: --- ## Protect frontend routes When you call `MultiFactorAuth.init` in the `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you use the `SessionAuth` component. This validator checks if the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true` or not. If it is not, then the user will be redirected to the MFA auth screen. ### Other forms of authorization If you do not want to run our default validator on a specific route, you can modify the use of `SessionAuth` in the following way: By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true` or not. ```tsx import React from "react"; import { SessionAuth, useSessionContext, useClaimValue } from "supertokens-auth-react/recipe/session"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; const VerifiedRoute = (props: React.PropsWithChildren) => { return ( { return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id); }} > {props.children} ); }; function InvalidClaimHandler(props: React.PropsWithChildren) { const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (claimValue.loading) { return null; } if (claimValue.value === undefined || !("totp" in claimValue.value.c)) { return (
You do not have access to this page because you have not completed TOTP. Please{" "} click here to finish to proceed.
); } // the user has finished TOTP, so we can render the children return
{props.children}
; } ```
```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has finished all MFA factors. return true; } else { for (const err of validationErrors) { if (err.id === MultiFactorAuthClaim.id) { // user has not finished MFA factors. let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim, }); if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { // the user has not finished totp return false; } } } } } // a session does not exist, or email is not verified return false; } ```
- In the snippet above, we remove the default claim validator that is added to `SessionAuth`, and add out own logic that reads from the session's payload. - Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks. In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen.
By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true` or not. The examples below read the raw access token payload for user-interface decisions only. They do not use or imply a mobile-specific MFA authorization API. Backend MFA claim validation remains required for protected APIs. ```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // user has finished all MFA factors. return true; } else { for (const err of validationErrors) { if (err.id === MultiFactorAuthClaim.id) { // user has not finished MFA factors. let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim, }); if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { // the user has not finished totp return false; } } } } } // a session does not exist, or email is not verified return false; } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let validationErrors = await supertokensSession.validateClaims(); if (validationErrors.length === 0) { // user has finished all MFA factors. return true; } else { for (const err of validationErrors) { if (err.id === supertokensMultiFactorAuth.MultiFactorAuthClaim.id) { // user has not finished MFA factors. let mfaClaimValue = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim, }); if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { // the user has not finished totp return false; } } } } } // a session does not exist, or email is not verified return false; } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function checkIfMFAIsCompleted() { if (await SuperTokens.doesSessionExist()) { let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; if (isMFACompleted) { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun checkIfMFAIsCompleted() { val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); val isMFACompleted: Boolean = (accessTokenPayload.get("st-mfa") as JSONObject).get("v") as Boolean if (isMFACompleted) { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func checkIfMFAIsCompleted() { // Attempt to retrieve the access token payload securely if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely() { // Extract the mfaObject from the accessTokenPayload if let mfaObject: [String: Any] = accessTokenPayload["st-mfa"] as? [String: Any] { // Determine if MFA has been completed if let isMFACompleted: Bool = mfaObject["v"] as? Bool { if isMFACompleted { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future checkIfMFAIsCompleted() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-mfa")) { Map mfaObject = accessTokenPayload["st-mfa"]; if (mfaObject.containsKey("v")) { bool isMFACompleted = mfaObject["v"]; if (isMFACompleted) { // All required factors for MFA have been completed } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } ``` In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. If the MFA claim value is missing in the access token payload, then it means that the session was created before you enabled MFA on the backend. In this case, you can call the [MFA Info](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) endpoint which will add the MFA claim to the session and check again. --- ## See also --- # Implement step-up authentication Source: https://supertokens.com/docs/additional-verification/mfa/step-up-auth ## Overview Step-up authentication enforces the user to complete an authentication challenge before navigating to a page, or before doing a specific action. You can implement it with **SuperTokens** as full page navigation, or as popups on the current page. ## Before you start These instructions assume that you already have some knowledge of MFA. If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/additional-verification/mfa/important-concepts). ### Prerequisites Step-up authentication supports the following factors: - `TOTP` - `WebAuthn/Passkeys` - Password (available only for custom UI) - Email or SMS `OTP` If you are using **OAuth2** in your configuration, step-up authentication is not supported at the moment. ## Steps ### 1. Add the backend validators To protect sensitive APIs with step up auth, you need to check that the user has completed the required auth challenge within a certain amount of time. If they haven't, you should return a `403` to the frontend which highlights which factor is necessary. The frontend can then consume this and show the auth challenge to the user. :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let app = express(); app.post("/update-blog", verifySession(), async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... }); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; async function updateBlog(awsEvent: SessionEvent) { let mfaClaim = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... } exports.handler = verifySession(updateBlog); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { let mfaClaim = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; class Example { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept(verifySession()) @response(200) async handler() { let mfaClaim = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; export default async function example(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again await superTokensNextWrapper( async (next) => { throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); }, req, res, ); } // continue with API logic... } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; @Controller() export class ExampleController { @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise { let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } // continue with API logic... return true; } } ``` ```python check=false reason="route fragment assumes an existing framework application" import time from fastapi import Depends from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post("/update-blog") async def update_blog_api(session: SessionContainer = Depends(verify_session())): mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None totp_completed_time = mfa_claim_value.c.get("totp") if totp_completed_time is None or totp_completed_time < (int(time.time()) - 5 * 60): # TOTP hasn't been completed or was completed more than 5 minutes ago raise_invalid_claims_exception( "TOTP validation required", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "TOTP validation required or has expired", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP ``` ```python check=false reason="route fragment assumes an existing framework application" import time from flask import Flask, g from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route('/update-blog', methods=['POST']) @verify_session() def check_mfa_api(): session: SessionContainer = g.supertokens mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None totp_completed_time = mfa_claim_value.c.get("totp") if totp_completed_time is None or totp_completed_time < (int(time.time()) - 5 * 60): # TOTP hasn't been completed or was completed more than 5 minutes ago raise_invalid_claims_exception( "TOTP validation required", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "TOTP validation required or has expired", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP ``` ```python check=false reason="session attribute is injected by framework middleware" import time from typing import cast from django.http import HttpRequest from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def get_user_info_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None totp_completed_time = mfa_claim_value.c.get("totp") if totp_completed_time is None or totp_completed_time < (int(time.time()) - 5 * 60): # TOTP hasn't been completed or was completed more than 5 minutes ago raise_invalid_claims_exception( "TOTP validation required", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "TOTP validation required or has expired", "factorId": "totp", }, ) ], ) # If we reach here, it means the user has completed TOTP ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { backendConfig } from "@/app/config/backend"; import { Error as STError } from "supertokens-node/recipe/session"; SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again const error = new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); return NextResponse.json(error, { status: 403 }); } // continue with API logic... return NextResponse.json({}); }); } ``` - When calling the `verifySession`, SuperTokens makes sure that the session is valid and that the user has completed all the required auth factors at some point in time. This enforces the basic check that the user has finished MFA during login. - Further check if the user has finished the TOTP login method within the last 5 minutes. If they haven't, send back a 403 to the frontend for the frontend to handle. - You can check other factor types in this was as well. For example, if you want to check that the user has done email OTP in the last 5 minutes, you can use the factor ID of `otp-email`, or if you want to check that the user has entered their account password in the last 5 minutes, you can check `emailpassword` factor ID. - If users have different login methods, and / or different MFA configurations, you may want to first check what factor applies to them. You can check their login method by fetching the user object using the `getUser` function from the SDK, and then matching the `session.getRecipeId()` to the login methods in the user object. Per the MFA factors, you can see which ones this user has enabled by using the `MultiFactorAuth.getRequiredSecondaryFactorsForUser` function. For performance reasons, you may want to put this information in the session's access token payload of the user in the `createNewSession` override function of the session recipe. ### 2. Prevent factor setup during step-up authentication By default, SuperTokens allows a factor setup, such as creating a new TOTP device, as long as the user has a session and has completed all the MFA factors required during login. This opens up a security issue when it comes to completing step up auth. Consider the following scenario: - The user has logged in and completed TOTP - After 5 minutes, the user tries to do a sensitive action and the API for that fails with a 403 (cause of the check in step 1, above). - The user sees the TOTP challenge on the frontend. However, instead of completing that, they call the create TOTP device API which would succeed and then use the new TOTP device to complete the factor challenge required for the API. This allows someone malicious to bypass step up auth. To prevent this, override one of the MFA recipe functions on the backend. This enforces that the factor setup can only happen if the user is not in a step-up auth state. :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import { Error as STError } from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ MultiFactorAuth.init({ firstFactors: [ /*...*/ ], override: { functions: (originalImplementation) => { return { ...originalImplementation, assertAllowedToSetupFactorElseThrowInvalidClaimError: async (input) => { await originalImplementation.assertAllowedToSetupFactorElseThrowInvalidClaimError(input); let claimValue = await input.session.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (claimValue === undefined || !claimValue.v) { return; } // if the above did not throw, it means that the user has logged in and has completed all the required // factors for login. So now we check specifically for the step up auth case: if ( input.factorId === MultiFactorAuth.FactorIds.TOTP && (await input.factorsSetUpForUser).includes(MultiFactorAuth.FactorIds.TOTP) ) { // this is an example of checking for totp, but you can also use other factor IDs. const totpCompletedTime = claimValue.c[MultiFactorAuth.FactorIds.TOTP]; if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { // this means that the user had completed the TOTP challenge more than 5 minutes ago // so we should ask them to complete it again throw new STError({ type: "INVALID_CLAIMS", message: "User has not finished TOTP", payload: [ { id: MultiFactorAuth.MultiFactorAuthClaim.key, reason: { message: "Factor validation failed: totp not completed", factorId: MultiFactorAuth.FactorIds.TOTP, }, }, ], }); } } }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" import time from typing import Any, Awaitable, Callable, Dict, List from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, MFARequirementList, OverrideConfig, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) def override_functions(original_implementation: RecipeInterface): original_assert_allowed_to_setup_factor_else_throw_invalid_claim_error = ( original_implementation.assert_allowed_to_setup_factor_else_throw_invalid_claim_error ) async def assert_allowed_to_setup_factor_else_throw_invalid_claim_error( session: SessionContainer, factor_id: str, mfa_requirements_for_auth: Callable[[], Awaitable[MFARequirementList]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> None: await original_assert_allowed_to_setup_factor_else_throw_invalid_claim_error( session=session, factor_id=factor_id, mfa_requirements_for_auth=mfa_requirements_for_auth, factors_set_up_for_user=factors_set_up_for_user, user_context=user_context, ) claim_value = await session.get_claim_value(MultiFactorAuthClaim) if claim_value is None or not claim_value.v: return # Check specifically for the step up auth case if ( factor_id == FactorIds.TOTP and FactorIds.TOTP in await factors_set_up_for_user() ): totp_completed_time = claim_value.c.get(FactorIds.TOTP) if totp_completed_time is None or totp_completed_time < ( int(time.time()) - 5 * 60 ): # User completed TOTP challenge more than 5 minutes ago raise_invalid_claims_exception( "User has not finished TOTP", [ ClaimValidationError( MultiFactorAuthClaim.key, { "message": "Factor validation failed: totp not completed", "factorId": "totp", }, ) ], ) original_implementation.assert_allowed_to_setup_factor_else_throw_invalid_claim_error = ( assert_allowed_to_setup_factor_else_throw_invalid_claim_error ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` - SuperTokens calls the function `assertAllowedToSetupFactorElseThrowInvalidClaimError` whenever the client calls an API to setup a new factor (for example, create a new TOTP device). Perform checks in this function and throw an error if necessary to prevent factor setup. - In the override logic, first call the original implementation and check that the `v` value in the MFA session claim is `true`. This throws / exits the function early if the user has not logged in yet (for example, they have finished the first factor, but not the required second factor). - Then check if the user has TOTP already setup for them, if they haven't, then allow the factor setup (otherwise the user would not be able to complete the step-up auth challenge). If they have, perform the same check as in step 1 - checking if the user has finished TOTP in the last 5 minutes or not. If they haven't, disallow factor setup. The customisation above prevents the security issue highlighted in the beginning of this step. ### 3. Handle `403` on the frontend The JSON body of the step-up auth claim failure looks like this: ```json { "message": "invalid claim", "claimValidationErrors": [ { "id": "st-mfa", "reason": { "message": "Factor validation failed: totp not completed", "factorId": "totp" } } ] } ``` You can check for this structure and the `factorId` to decide what factor to show on the frontend. You have two options to show the UI to the user: #### Full page redirect to the factor To redirect the user to as factor challenge page and then navigate them back to the current page, you can use the following function: Redirect the user to `/{websiteBasePath}/mfa/totp?stepUp=true&redirectToPath={currentPath}`. This shows the TOTP challenge in step-up mode. The `redirectToPath` query parameter tells the SDK to redirect the user back to the current page after they complete the challenge. ```tsx import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; async function redirectToTotpSetupScreen() { MultiFactorAuth.redirectToFactor({ factorId: "totp", stepUp: true, redirectBack: true, }); } ``` - In the snippet above, redirect to the [TOTP factor setup screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup). Set the `stepUp` argument to `true` otherwise the MFA screen would detect that the user has already completed basic MFA requirements and would not show the verification screen. Set the `redirectBack` argument to `true` since the intention is to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?stepUp=true&redirectToPath={currentPath}` if you don't want to use the above function. #### Show the factor in a popup Checkout [the documentation](/additional-verification/mfa/embed-the-prebuilt-ui) for embedding the pre-built UI factor components in a page or a popup. You can check for this structure and the `factorId` to decide what factor to show on the frontend. ### 4. Check for step-up authentication on page navigation Sometimes, you may want to ask users to complete step up auth before displaying a page on the frontend. This is a different scenario than the above steps cause. Here, you do not want to rely on an API call to fail. Instead, you want to check for the step-up auth condition before rendering the page itself. To do this, read the access token payload on the frontend and check the completed time of the factor of interest before rendering the page. If the completed time is older than 5 minutes (as an example), redirect the user to the factor challenge page. ```tsx import React from "react"; import { SessionAuth, useClaimValue } from "supertokens-auth-react/recipe/session"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import { DateProviderReference } from "supertokens-auth-react/utils/dateProvider"; const VerifiedRoute = (props: React.PropsWithChildren) => { return ( {props.children} ); }; function InvalidClaimHandler(props: React.PropsWithChildren) { let claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (claimValue.loading) { return null; } let totpCompletedTime = claimValue.value?.c[MultiFactorAuth.FactorIds.TOTP]; if ( totpCompletedTime === undefined || totpCompletedTime < Math.floor(DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000) - 5 * 60 ) { return (
You need to complete TOTP before seeing this page. Please{" "} click here to finish to proceed.
); } // the user has finished TOTP, so we can render the children return
{props.children}
; } ```
```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; import { DateProviderReference } from "supertokens-web-js/utils/dateProvider"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // since all default claim validators have passed, we now check for if the user has finished TOTP // within the last 5 mins let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); let totpCompletedTime = mfaClaimValue?.c["totp"]; if ( totpCompletedTime === undefined || totpCompletedTime < Math.floor(DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000) - 5 * 60 ) { // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. return false; } return true; } else { // handle other validation failure events... } } // a session does not exist, or email is not verified return false; } ```
- Check if the user has completed TOTP within the last 5 minutes or not. If not, show a message to the user, and ask them to complete TOTP. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. - In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server.
```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; import { DateProviderReference } from "supertokens-web-js/utils/dateProvider"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims(); if (validationErrors.length === 0) { // since all default claim validators have passed, we now check for if the user has finished TOTP // within the last 5 mins let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); let totpCompletedTime = mfaClaimValue?.c["totp"]; if ( totpCompletedTime === undefined || totpCompletedTime < Math.floor(DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000) - 5 * 60 ) { // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. return false; } return true; } else { // handle other validation failure events... } } // a session does not exist, or email is not verified return false; } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let validationErrors = await supertokensSession.validateClaims(); if (validationErrors.length === 0) { // since all default claim validators have passed, we now check for if the user has finished TOTP // within the last 5 mins let mfaClaimValue = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim, }); let totpCompletedTime = mfaClaimValue?.c["totp"]; if ( totpCompletedTime === undefined || totpCompletedTime < Math.floor( supertokensDateProviderReference.DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000, ) - 5 * 60 ) { // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. return false; } return true; } else { // handle other validation failure events... } } // a session does not exist, or email is not verified return false; } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function checkIfMFAIsCompleted() { if (await SuperTokens.doesSessionExist()) { let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; if (isMFACompleted) { let completedFactors = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].c; if (completedFactors["totp"] === undefined || completedFactors["totp"] < Math.floor(Date.now() / 1000) - 5 * 60) { // user has not finished TOTP MFA in the last 5 minutes } } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun checkIfMFAIsCompleted() { try { val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this) val mfaObject = accessTokenPayload.optJSONObject("st-mfa") mfaObject?.let { val isMFACompleted = it.optBoolean("v", false) if (isMFACompleted) { val completedFactors = it.optJSONObject("c") completedFactors?.let { factors -> val totpCompletionTime = factors.optLong("totp", -1) if (totpCompletionTime == -1L || totpCompletionTime < System.currentTimeMillis() - 1000 * 60 * 5) { // User has not finished TOTP MFA in the last 5 minutes } } } else { // MFA is not completed; you can check the `c` object from "st-mfa" prop to see which factors have been completed } } } catch (e: Exception) { // Handle exceptions such as ClassCastException or JSONException } } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func checkIfMFAIsCompleted() { if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let mfaObject = accessTokenPayload["st-mfa"] as? [String: Any], let isMFACompleted = mfaObject["v"] as? Bool { // Corrected the extraction of mfaObject from the accessTokenPayload if isMFACompleted { // All required factors for MFA have been completed if let mfaCompletedFactors = mfaObject["c"] as? [String: Any], let totpTime = mfaCompletedFactors["totp"] as? Double { // Corrected unwrapping of mfaCompletedFactors and casting of totpTime if totpTime < (Date().timeIntervalSince1970 - 1000*60*5) { // user has not finished TOTP MFA in the last 5 minutes } } } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future checkIfMFAIsCompleted() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-mfa")) { Map mfaObject = accessTokenPayload["st-mfa"]; if (mfaObject.containsKey("v")) { bool isMFACompleted = mfaObject["v"] as bool; // Casting to bool if (isMFACompleted) { // All required factors for MFA have been completed Map mfaCompletedFactors = mfaObject["c"]; if (mfaCompletedFactors["totp"] == null || mfaCompletedFactors["totp"] < (DateTime.now().millisecondsSinceEpoch - 1000 * 60 * 5)) { // user has not finished TOTP MFA in the last 5 minutes } } else { // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } } } } ``` - In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. - In your protected routes, you need to first check if a session exists, and then check that the user has finished all the basic MFA factors for logging in (by checking the value of the `v` boolean in the MFA claim session). If that passes, then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session, and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. --- ## See also --- # Require TOTP for all users Source: https://supertokens.com/docs/additional-verification/mfa/totp/totp-for-all-users ## Overview This guide shows you how to implement an MFA policy that requires all users to use TOTP before they get access to your application. ## Before you start The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. ## Steps ### 1. Configure the backend To start with, we configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import Session from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { return [MultiFactorAuth.FactorIds.TOTP]; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user return [FactorIds.TOTP] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` - Notice that we have initialised the TOTP recipe in the `recipeList`. By default, no configs are required for it, but you can provide: - `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. - `defaultSkew`: The default value of this is `1`, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course). - `defaultPeriod`: The default value of this is `30`, which means that the current tick is value for 30 seconds. So by default, a TOTP code that's just shown to the user, is valid for 60 seconds (`defaultPeriod + defaultSkew*defaultPeriod` seconds) - We also override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed before the user can access the app. Notice that we do not check for the userId there, and return `totp` for all users. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `totp`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "totp": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend We start by modifying the `init` function call on the frontend like so: You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx import supertokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import totp from "supertokens-auth-react/recipe/totp"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. totp.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. supertokensUITOTP.init(), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, ], }), ], }); ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Totp from "supertokens-web-js/recipe/totp"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), Totp.init(), ], }); ``` - Just like on the backend, we init the `totp` recipe in the `recipeList`. - We also init the `MultiFactorAuth` recipe, and pass in the first factors that we want to use. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component: :::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] ::: ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ /* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI, ])} // ... other routes
); } ```
```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; function App() { if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]); } return {/*Your app*/}; } ```
With the above configuration, users will see `emailpassword` or social login UI when they visit the auth page. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: - [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) - [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before).
We start by initialising the MFA and TOTP recipe on the frontend like so: :::success[This step is not applicable for mobile apps. Please continue reading.] ::: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Totp from "supertokens-web-js/recipe/totp"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), Totp.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... supertokensMultiFactorAuth.init(), supertokensTotp.init(), ], }); ``` After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](/additional-verification/mfa/initial-setup#12-add-the-mfa-flow). If it's not, then we can redirect the user to the application page. If it's `false`, the frontend then needs to [call the MFA endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the `next` array will contain `["totp"]`. Two possibilities exist here: - Case 1: The user needs to setup a TOTP device cause they don't have any. - Case 2: The user already has a verified device setup and needs to complete the TOTP challenge. We can know which case it is by checking if `"totp"` is one of the items in the `factorsThatAreAlreadySetup` array that is returned from the API call above. If it is in the array, then it's case 2, otherwise it's case 1. #### Case 1 implementation: User needs to setup a new TOTP device In this case, we do two things: - Call an API on the backend to create a device. This returns the device secret that can be displayed to the user. The user is supposed to scan this using their authenticator app, to add a new entry for your app in their authenticator app. - Then the user needs to enter the TOTP code that's displayed to them in the app, and this needs to be sent to the backend to mark the device as verified. Once a device is marked as verified, only then will the `factorsThatAreAlreadySetup` array contain `"totp"` the next time they login. To create a new device, call the following API: The above API call returns the following response: ```tsx import Totp from "supertokens-web-js/recipe/totp"; import Session from "supertokens-web-js/recipe/session"; async function createNewTotpDevice() { if (await Session.doesSessionExist()) { try { let deviceResponse = await Totp.createDevice(); if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { // this should only come here if you are passing a custom device name when calling the above function. throw new Error("Should never come here"); // device created successfully } // device created successfully let qrCodeString = deviceResponse.qrCodeString; let secret = deviceResponse.secret; // TODO: display a QR code based on qrCodeString, and also an option to view // the secret if the user is unable to scan the QR code. } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error( "TOTP device creation can only happen after the first factor is complete and when a session exists", ); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function createNewTotpDevice() { if (await supertokensSession.doesSessionExist()) { try { let deviceResponse = await supertokensTotp.createDevice(); if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { // this should only come here if you are passing a custom device name when calling the above function. throw new Error("Should never come here"); // device created successfully } // device created successfully let qrCodeString = deviceResponse.qrCodeString; let secret = deviceResponse.secret; // TODO: display a QR code based on qrCodeString, and also an option to view // the secret if the user is unable to scan the QR code. } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error( "TOTP device creation can only happen after the first factor is complete and when a session exists", ); } } ``` ```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "issuerName": "...", "deviceName": "TOTP Device 1", "secret": "....", "userIdentifier": "user@example.com", "qrCodeString": "..." } | { "status": "DEVICE_ALREADY_EXISTS_ERROR" | "GENERAL_ERROR" } ``` - When device registration is successful, the API returns: - The `secret` and `qrCodeString` which are to be displayed to the user. For React apps, we recommend using the [react-qr-code library](https://github.com/rosskhanas/react-qr-code) to display the QR code. - The `issuerName` is the name will show up on the TOTP app for the user. By default, this is equal to the `appName` config on the backend SDK, however, you can change it to something else in the backend `totp.init` config. - The `userIdentifier` is the email / phone number of the user based on the first factor. This will also be shown in the TOTP app along with the `issuerName`. - The API call can also take a `deviceName` (as a POST body prop) which attempts to create a TOTP device with the provided name. A status of `"DEVICE_ALREADY_EXISTS_ERROR"` is returned in case a verified device with the input name already exists. In this case, you should ask the user to enter a different name. Note that this status is only returned in case you are passing in a custom device name. The default naming strategy is to name the device "TOTP Device N", where we start N from 1, and keep increasing it. This value can be used to identify a device from the backend point of view, for operations like deleting a device. - A status of `"GENERAL_ERROR"` is returned in case you specifically return that from a backend API override. Once a device has been created, and scanned, you need to ask the user to enter the TOTP and call the API below to verify it: The above API call returns the following response: ```tsx import Totp from "supertokens-web-js/recipe/totp"; import Session from "supertokens-web-js/recipe/session"; async function verifyTotpDevice(deviceName: string, userInputTotp: string) { if (await Session.doesSessionExist()) { try { let verifyResponse = await Totp.verifyDevice({ deviceName, totp: userInputTotp, }); if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { // this can happen due to a race condition wherein the device is deleted before verifying. window.alert("Something went wrong. Please reload and try again"); } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Device verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error( "TOTP device verification can only happen after the first factor is complete and when a session exists", ); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function verifyTotpDevice(deviceName: string, userInputTotp: string) { if (await supertokensSession.doesSessionExist()) { try { let verifyResponse = await supertokensTotp.verifyDevice({ deviceName, totp: userInputTotp, }); if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { // this can happen due to a race condition wherein the device is deleted before verifying. window.alert("Something went wrong. Please reload and try again"); } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Device verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error( "TOTP device verification can only happen after the first factor is complete and when a session exists", ); } } ``` ```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "wasAlreadyVerified": false } | { "status": "INVALID_TOTP_ERROR", "currentNumberOfFailedAttempts": 1, "maxNumberOfFailedAttempts": 5 } | { "status": "LIMIT_REACHED_ERROR", "retryAfterMs": 900000 } | { "status": "UNKNOWN_DEVICE_ERROR" | "GENERAL_ERROR" } ``` - The `deviceName`, which is an input to the API is one of the props returned from the previous API call to create a device. - When verification is successful (`status: "OK"`), the device is marked as verified in the database and can be used for the TOTP challenge next time around. The boolean `wasAlreadyVerified` indicates if the device was already verified before this call was made. - A status of `INVALID_TOTP_ERROR` means that the user has entered an incorrect TOTP and needs to retry. The response contains two other props: - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. - A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise valid TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 minutes, but it can be changed by setting the value for `totp_rate_limit_cooldown_sec` in the core config. - A status of `UNKNOWN_DEVICE_ERROR` is possible due to a race condition in which the device is somehow deleted before the verification call is made. - A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. On successful verification of a device, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). #### Case 2 implementation: User needs to complete the TOTP challenge This case is when the user already has a device setup (`totp` is in `factorsThatAreAlreadySetup`), and needs to complete the TOTP challenge. In this case, you should show the user an input box asking them to enter their TOTP from the authenticator app and then call the following API: The above API call returns the following response: ```tsx import Totp from "supertokens-web-js/recipe/totp"; import Session from "supertokens-web-js/recipe/session"; async function verifyTotpCode(userInputTotp: string) { if (await Session.doesSessionExist()) { try { let verifyResponse = await Totp.verifyCode({ totp: userInputTotp, }); if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Code verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error( "TOTP code verification can only happen after the first factor is complete and when a session exists", ); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function verifyTotpCode(userInputTotp: string) { if (await supertokensSession.doesSessionExist()) { try { let verifyResponse = await supertokensTotp.verifyCode({ totp: userInputTotp, }); if (verifyResponse.status === "LIMIT_REACHED_ERROR") { // this can happen if the user has entered a wrong TOTP too many times. window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { window.alert("Totp incorrect. Please try again"); } else { // Code verified successfully } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error( "TOTP code verification can only happen after the first factor is complete and when a session exists", ); } } ``` ```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK" | "UNKNOWN_USER_ID_ERROR" } | { "status": "INVALID_TOTP_ERROR", "currentNumberOfFailedAttempts": 1, "maxNumberOfFailedAttempts": 5, } | { "status": "LIMIT_REACHED_ERROR", "retryAfterMs": 900000, } | { "status": "GENERAL_ERROR" } ``` - A `status: OK` indicates that verification was successful. SuperTokens tries and verifies the input TOTP against all verified devices that belong to this user. - A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. - A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even value TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 minutes, but it can be changed by setting the value for `totp_rate_limit_cooldown_sec` in the core config. - A status of `UNKNOWN_USER_ID_ERROR` is possible due to a race condition in which all devices that the user had are deleted by the time this API is called. In this case, you can ask users to setup a new device. - A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. On successful verification of the code, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint).
In a multi tenancy setup, you may want to enable TOTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#1-configure-the-backend) section above, so in this section, we will focus on enabling TOTP for all users within specific tenants. ### 1. Configure the backend To start, we will initialise the TOTP and the MultiFactorAuth recipes in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import Session from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init(), MultiFactorAuth.init(), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ totp.init(), multifactorauth.init(), ], ) ``` Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. To configure TOTP requirement for a tenant, we can call the following API: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.TOTP], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.TOTP], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.TOTP], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` - In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - We set the `requiredSecondaryFactors` to `["totp"]` to indicate that TOTP is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `totp`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "totp": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend We start by modifying the `init` function call on the frontend like so: You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx import supertokens from "supertokens-auth-react"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import totp from "supertokens-auth-react/recipe/totp"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [ // other recipes... totp.init(), MultiFactorAuth.init(), Multitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO"; }, }; }, }, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, usesDynamicLoginMethods: true, recipeList: [ // other recipes... supertokensUITOTP.init(), supertokensUIMultiFactorAuth.init(), supertokensUIMultitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO"; }, }; }, }, }), ], }); ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` - Just like on the backend, we init the `totp` recipe in the `recipeList`. - We also init the `MultiFactorAuth` recipe. Notice that unlike the single tenant setup, we do not specify the `firstFactors` here. That information is fetched based on the tenantId you provide the SDK with. - We have set `usesDynamicLoginMethods: true` so that the SDK knows to fetch the login methods dynamically based on the tenantId. - Finally, we init the multi tenancy recipe and provide a method for getting the tenantId. Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component: :::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] ::: ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ /* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI, ])} // ... other routes
); } ```
```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; function App() { if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]); } return {/*Your app*/}; } ```
With the above configuration, users will see the first and second factor based on the tenant configuration. For the tenant we configured above, users will see email password or social login first. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: - [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) - [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before).
The steps here are the same as in [the single tenant setup above](#2-configure-the-frontend).
--- # Require TOTP for specific users Source: https://supertokens.com/docs/additional-verification/mfa/totp/totp-for-opt-in-users ## Overview In this page, we will show you how to implement an MFA policy that requires certain users to do TOTP. You can decide which those users are based on any criteria. For example: - Only users that have an `admin` role require to do TOTP; OR - Only users that have enabled TOTP on their account require to do TOTP; OR - Only users that have a paid account require to do TOTP. Whatever the criteria is, the steps to implementing this type of a flow is the same. ## Before you start The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. ## Steps ### 1. Configure the backend #### Enable TOTP for users that have an `admin` role To start with, we configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); if (roles.roles.includes("admin")) { // we only want totp for admins return [MultiFactorAuth.FactorIds.TOTP]; } else { // no MFA for non admin users. return []; } }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.recipe.userroles.asyncio import get_roles_for_user def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if "admin" in roles.roles: # We only want TOTP for admins return [FactorIds.TOTP] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` We override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed only for users that have the `admin` role. You can also have any other criteria here. #### Ask for TOTP only for users that have enabled TOTP on their account To start with, we configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import Session from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init({ override: { apis: (oI) => { return { ...oI, verifyDevicePOST: async function (input) { let response = await oI.verifyDevicePOST!(input); if (response.status === "OK") { // device successfully verified. We save that this user has enabled TOTP in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the TOTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP, ); } return response; }, }; }, }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, ) from typing import Dict, Any from supertokens_python.recipe.totp.types import ( TOTPConfig, OverrideConfig, VerifyDeviceOkResult, ) from supertokens_python.recipe.totp.interfaces import APIInterface, APIOptions from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) def totp_override(original_implementation: APIInterface): original_verify_device_post = original_implementation.verify_device_post async def verify_device_post( device_name: str, totp: str, options: APIOptions, session: SessionContainer, user_context: Dict[str, Any], ): response = await original_verify_device_post( device_name, totp, options, session, user_context ) if isinstance(response, VerifyDeviceOkResult): await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.TOTP ) return response original_implementation.verify_device_post = verify_device_post return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY] ), ], ) ``` We initialise the multi factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors are enabled for a user and returns those. Therefore all we need to do is mark `totp` as enabled for a user as soon as they have setup a device successfully. This happens in the `verifyDevicePOST` API override as shown above. Once a device is verified, we mark the `totp` factor as enabled for the user, and the next time they login, they will be asked to complete the TOTP challenge. In both of the examples above, notice that we have initialised the TOTP recipe in the `recipeList`. Here are some of the configrations you can add to the `totp.init` function: - `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. - `defaultSkew`: The default value of this is `1`, which means that TOTP codes that were generated 1 tick before, and that will be generated 1 tick after from the current tick will be accepted at any given time (including the TOTP of the current tick, of course). - `defaultPeriod`: The default value of this is `30`, which means that the current tick is value for 30 seconds. By default, a TOTP code that's shown to the user, is valid for 60 seconds (`defaultPeriod + defaultSkew*defaultPeriod` seconds) Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this (for those that require TOTP): ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished TOTP, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "totp": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Configure the frontend Two parts exist to this: - Configuring the frontend to show the TOTP UI when required during login / sign up - Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). The first part is identical to the steps in [Configure the frontend](/additional-verification/mfa/totp/totp-for-all-users#2-configure-the-frontend). The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if TOTP is enabled or not. - If enabled, you can show them a list of current TOTP devices with options to remove any. - If enabled, you can show them an option to add a new TOTP device. In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } async function disableMFAForUser(userId: string) { await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str) -> None: await add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` ```python from supertokens_python.recipe.multifactorauth.syncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str) -> None: add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) def disable_mfa_for_user(user_id: str) -> None: remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` In order to list existing TOTP devices on the frontend, you can call the following API: Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```tsx import Session from "supertokens-web-js/recipe/session"; import Totp from "supertokens-web-js/recipe/totp"; async function fetchTOTPDevices() { if (await Session.doesSessionExist()) { try { let totpDevicesResponse = await Totp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name); // by default, this will be like "TOTP Device 1" console.log(currDevice.verified); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function fetchTOTPDevices() { if (await supertokensSession.doesSessionExist()) { try { let totpDevicesResponse = await supertokensTotp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name); // by default, this will be like "TOTP Device 1" console.log(currDevice.verified); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```bash curl --location --request GET '/auth/totp/device/list' \ --header 'Authorization: Bearer ...' ``` The output from the API call is as follows: ```json check=false reason="response type excerpt uses array and union notation" { "status": "OK", "devices": { "name": "TOTP Device 1", "period": 30, "skew": 1, "verified": true }[]; } | { "status": "GENERAL_ERROR" } ``` - A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. - A `status: GENERAL_ERROR`: This is possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend In order to remove a device, you can call the following API from the frontend: Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```tsx import Session from "supertokens-web-js/recipe/session"; import Totp from "supertokens-web-js/recipe/totp"; async function removeTOTPDevices(deviceName: string) { if (await Session.doesSessionExist()) { try { await Totp.removeDevice({ deviceName, }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function removeTOTPDevices(deviceName: string) { if (await supertokensSession.doesSessionExist()) { try { await supertokensTotp.removeDevice({ deviceName, }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```bash curl --location --request POST '/auth/totp/device/remove' \ --header 'Authorization: Bearer ...' --header 'Content-Type: application/json' \ --data-raw '{ "deviceName": "..." }' ``` The output from the API call is as follows: ```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "didDeviceExist": true; } | { "status": "GENERAL_ERROR" } ``` In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre-built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: - We add the query param of `setup=true` because we want to create a new device. - The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. ```tsx import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; async function redirectToTotpSetupScreen() { MultiFactorAuth.redirectToFactor({ factorId: "totp", forceSetup: true, redirectBack: true, }); } ``` - In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). We set the `forceSetup` to `true` since we want the user to setup a new TOTP device. The `redirectBack` boolean is also `true` since we want to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#1-configure-the-backend) above) will add TOTP as a required factor for this user, ensuring that next time they login, they will be asked to complete the TOTP challenge. To create a new device, redirect the user to a page that creates a TOTP device on the backend, asks the user to scan the QR code, and then verifies a TOTP. Use the functions in [Case 1: Set up a new TOTP device](/additional-verification/mfa/totp/totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device). ### 1. Configure the backend A user can be a part of multiple tenants. If you want TOTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#1-configure-the-backend) section above. However, if you want TOTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#1-configure-the-backend) section above: #### Only enable TOTP for users that have an `admin` role :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), UserRoles.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); if ( roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP) ) { // we only want totp for admins return [MultiFactorAuth.FactorIds.TOTP]; } else { // no MFA for non admin users. return []; } }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.recipe.userroles.asyncio import get_roles_for_user def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Get roles for the user roles = await get_roles_for_user(tenant_id, (await user()).id) if ( "admin" in roles.roles and FactorIds.TOTP in await required_secondary_factors_for_tenant() ): # We only want TOTP for admins return [FactorIds.TOTP] else: # No MFA for non-admin users return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), totp.init(), ], ) ``` - The override checks `requiredSecondaryFactorsForTenant` (Python: `required_secondary_factors_for_tenant`) so TOTP is required only when the tenant configuration includes it. #### Ask for TOTP only for users that have enabled TOTP on their account :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; import totp from "supertokens-node/recipe/totp"; import Session from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), totp.init({ override: { apis: (oI) => { return { ...oI, verifyDevicePOST: async function (input) { let response = await oI.verifyDevicePOST!(input); if (response.status === "OK") { // device successfully verified. We save that this user has enabled TOTP in the user metadata. // The multifactorauth recipe will pick this value up next time the user is trying to login, and // ask them to enter the TOTP code. await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP, ); } return response; }, }; }, }, }), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.TOTP)) { // this means that the user has finished setting up a device from their settings page. if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) { return [MultiFactorAuth.FactorIds.TOTP]; } } // no totp required for input.user, with the input.tenant. return []; }, }; }, }, }), ], }); ``` ```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig as MFAOverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import User from typing import Dict, Any, Callable, Awaitable, List from supertokens_python.recipe.totp.interfaces import APIInterface, APIOptions from supertokens_python.recipe.totp.types import ( TOTPConfig, OverrideConfig, VerifyDeviceOkResult, ) def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: if FactorIds.TOTP in await required_secondary_factors_for_user(): if FactorIds.TOTP in await required_secondary_factors_for_tenant(): return [FactorIds.TOTP] # no otp-email required for input.user, with the input.tenant. return [] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation def totp_override(original_implementation: APIInterface): original_verify_device_post = original_implementation.verify_device_post async def verify_device_post( device_name: str, totp: str, options: APIOptions, session: SessionContainer, user_context: Dict[str, Any], ): response = await original_verify_device_post( device_name, totp, options, session, user_context ) if isinstance(response, VerifyDeviceOkResult): await add_to_required_secondary_factors_for_user( session.get_user_id(), FactorIds.TOTP ) return response original_implementation.verify_device_post = verify_device_post return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="...", recipe_list=[ totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=MFAOverrideConfig(functions=override_functions), ), ], ) ``` - The `getMFARequirementsForAuth` override checks both the user's required factors and `requiredSecondaryFactorsForTenant` (Python: `required_secondary_factors_for_tenant`). TOTP is required only when it is enabled for that user and allowed by the current tenant configuration. ### 2. Configure the frontend Two parts exist to this: - Configuring the frontend to show the TOTP UI when required during login / sign up - Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). The first part is identical to [Configure the frontend](/additional-verification/mfa/totp/totp-for-all-users#2-configure-the-frontend). The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if TOTP is enabled or not. - If enabled, you can show them a list of current TOTP devices with options to remove any. - If enabled, you can show them an option to add a new TOTP device. In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpEnabledForUser(userId: string) { let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds async def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } async function disableMFAForUser(userId: string) { await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } ``` ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds async def enable_mfa_for_user(user_id: str) -> None: await add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` ```python from supertokens_python.recipe.multifactorauth.syncio import ( add_to_required_secondary_factors_for_user, remove_from_required_secondary_factors_for_user, ) from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str) -> None: add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) def disable_mfa_for_user(user_id: str) -> None: remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` In order to list existing TOTP devices on the frontend, you can call the following API: Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```tsx import Session from "supertokens-web-js/recipe/session"; import Totp from "supertokens-web-js/recipe/totp"; async function fetchTOTPDevices() { if (await Session.doesSessionExist()) { try { let totpDevicesResponse = await Totp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name); // by default, this will be like "TOTP Device 1" console.log(currDevice.verified); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function fetchTOTPDevices() { if (await supertokensSession.doesSessionExist()) { try { let totpDevicesResponse = await supertokensTotp.listDevices(); for (let i = 0; i < totpDevicesResponse.devices.length; i++) { let currDevice = totpDevicesResponse.devices[i]; console.log(currDevice.name); // by default, this will be like "TOTP Device 1" console.log(currDevice.verified); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```bash curl --location --request GET '/auth/totp/device/list' \ --header 'Authorization: Bearer ...' ``` The output from the API call is as follows: ```json check=false reason="response type excerpt uses array and union notation" { "status": "OK", "devices": { "name": "TOTP Device 1", "period": 30, "skew": 1, "verified": true }[]; } | { "status": "GENERAL_ERROR" } ``` - A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. - A `status: GENERAL_ERROR`: This is possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend In order to remove a device, you can call the following API from the frontend: Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): ```tsx import Session from "supertokens-web-js/recipe/session"; import Totp from "supertokens-web-js/recipe/totp"; async function removeTOTPDevices(deviceName: string) { if (await Session.doesSessionExist()) { try { await Totp.removeDevice({ deviceName, }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function removeTOTPDevices(deviceName: string) { if (await supertokensSession.doesSessionExist()) { try { await supertokensTotp.removeDevice({ deviceName, }); // device is removed } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } else { throw new Error("Illegal function call: Please only call this function if a session exists"); } } ``` ```bash curl --location --request POST '/auth/totp/device/remove' \ --header 'Authorization: Bearer ...' --header 'Content-Type: application/json' \ --data-raw '{ "deviceName": "..." }' ``` The output from the API call is as follows: ```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "didDeviceExist": true; } | { "status": "GENERAL_ERROR" } ``` In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre-built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: - We add the query param of `setup=true` because we want to create a new device. - The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. ```tsx import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; async function redirectToTotpSetupScreen() { MultiFactorAuth.redirectToFactor({ factorId: "totp", forceSetup: true, redirectBack: true, }); } ``` - In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). We set the `forceSetup` to `true` since we want the user to setup a new TOTP device. The `redirectBack` boolean is also `true` since we want to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#1-configure-the-backend) above) will add TOTP as a required factor for this user, so that next time they login, they will be asked to complete the TOTP challenge. To create a new device, redirect the user to a page that creates a TOTP device on the backend, asks the user to scan the QR code, and then verifies a TOTP. Use the functions in [Case 1: Set up a new TOTP device](/additional-verification/mfa/totp/totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device). --- # Passkeys as an MFA Factor Source: https://supertokens.com/docs/additional-verification/mfa/webauthn-setup ## Overview This guide shows how to implement an MFA policy that requires all users to use WebAuthn before they get access to your application. For standalone passwordless sign-in with passkeys, use the [Passkey Authentication guide](/authentication/passkeys/introduction) instead. ## Before you start The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. ## Steps ### 1. Configure the backend To start with, we configure the backend in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import webauthn from "supertokens-node/recipe/webauthn"; import Session from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), webauthn.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], override: { functions: (originalImplementation) => { return { ...originalImplementation, getMFARequirementsForAuth: async function (input) { // Change this implementation if you want to require webauthn only for specific users return [MultiFactorAuth.FactorIds.WEBAUTHN]; }, }; }, }, }), ], }); ``` ```python from typing import Any, Awaitable, Callable, Dict, List, Optional, Union from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, session, thirdparty, webauthn, ) from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldAutomaticallyLink, ShouldNotAutomaticallyLink, ) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, MFARequirementList, ) from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session import SessionContainer from supertokens_python.types import User async def should_link_webauthn_mfa_account( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], current_session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if current_session is None or current_session.get_tenant_id() != tenant_id: return ShouldNotAutomaticallyLink() is_making_session_user_primary = ( user is None and new_account_info.recipe_user_id is not None and new_account_info.recipe_user_id.get_as_string() == current_session.get_recipe_user_id().get_as_string() ) is_linking_webauthn_to_session_user = ( new_account_info.recipe_id == "webauthn" and user is not None and user.id == current_session.get_user_id() ) if ( not is_making_session_user_primary and not is_linking_webauthn_to_session_user ): return ShouldNotAutomaticallyLink() return ShouldAutomaticallyLink(should_require_verification=True) def override_functions(original_implementation: RecipeInterface): async def get_mfa_requirements_for_auth( tenant_id: str, access_token_payload: Dict[str, Any], completed_factors: Dict[str, int], user: Callable[[], Awaitable[User]], factors_set_up_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: # Change this implementation if you want to require webauthn only for specific users return [FactorIds.WEBAUTHN] original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth ) return original_implementation init( app_info=InputAppInfo( app_name="Example App", api_domain="http://localhost:3001", website_domain="http://localhost:3000", ), supertokens_config=SupertokensConfig( connection_uri="http://localhost:3567", ), framework="fastapi", recipe_list=[ session.init(), thirdparty.init(), emailpassword.init(), accountlinking.init( should_do_automatic_account_linking=should_link_webauthn_mfa_account ), webauthn.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), ], ) ``` The MFA recipe override is required to indicate that `webauthn` must be completed before the user can access the app. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "webauthn": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. In a multi tenancy setup, you may want to enable WebAuthn for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#1-configure-the-backend) section above, so in this section, we will focus on enabling WebAuthn for all users within specific tenants. To start, we will initialise the WebAuthn and the MultiFactorAuth recipes in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```ts import supertokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; import webauthn from "supertokens-node/recipe/webauthn"; import Session from "supertokens-node/recipe/session"; supertokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ Session.init(), ThirdParty.init({ //... }), EmailPassword.init({ //... }), webauthn.init(), MultiFactorAuth.init(), ], }); ``` ```python from typing import Any, Dict, Optional, Union from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( accountlinking, emailpassword, multifactorauth, session, thirdparty, webauthn, ) from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldAutomaticallyLink, ShouldNotAutomaticallyLink, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.types import User async def should_link_webauthn_mfa_account( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], current_session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if current_session is None or current_session.get_tenant_id() != tenant_id: return ShouldNotAutomaticallyLink() is_making_session_user_primary = ( user is None and new_account_info.recipe_user_id is not None and new_account_info.recipe_user_id.get_as_string() == current_session.get_recipe_user_id().get_as_string() ) is_linking_webauthn_to_session_user = ( new_account_info.recipe_id == "webauthn" and user is not None and user.id == current_session.get_user_id() ) if ( not is_making_session_user_primary and not is_linking_webauthn_to_session_user ): return ShouldNotAutomaticallyLink() return ShouldAutomaticallyLink(should_require_verification=True) init( app_info=InputAppInfo( app_name="Example App", api_domain="http://localhost:3001", website_domain="http://localhost:3000", ), supertokens_config=SupertokensConfig( connection_uri="http://localhost:3567", ), framework="fastapi", recipe_list=[ session.init(), thirdparty.init(), emailpassword.init(), accountlinking.init( should_do_automatic_account_linking=should_link_webauthn_mfa_account ), webauthn.init(), multifactorauth.init(), ], ) ``` Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. To configure WebAuthn requirement for a tenant, we can call the following API: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], requiredSecondaryFactors: [MultiFactorAuth.FactorIds.WEBAUTHN], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.WEBAUTHN], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( first_factors=[FactorIds.EMAILPASSWORD], required_secondary_factors=[FactorIds.WEBAUTHN], ) ) if resp.created_new: # Tenant created successfully pass else: # Existing tenant's config was modified pass ``` - In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - We set the `requiredSecondaryFactors` to `["webauthn"]` to indicate that WebAuthn is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { "st-mfa": { "c": { "emailpassword": 1702877939 }, "v": false } } ``` The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: ```json { "st-mfa": { "c": { "emailpassword": 1702877939, "webauthn": 1702877999 }, "v": true } } ``` Indicating that the user has finished all required factors, and should be allowed to access the app. ### 2. Authorize account linking on the backend `shouldTryLinkingWithSessionUser: true` in the client calls below only asks the backend to try linking. It is not an authorization decision. The backend `AccountLinking` policy must decide whether linking is allowed. SuperTokens automatically initializes `AccountLinking` with a deny-by-default policy when you omit the recipe. To use WebAuthn as a second factor, initialize one explicitly configured `AccountLinking` recipe in the same recipe list. This replaces the automatic default; do not add a second initialization. The Python examples above already include this policy. For Node.js, add the configuration below to the recipe list shown above. If you already configure account linking, merge these checks into that policy. The following policy only permits linking for the current session and tenant. It also requires verified account information. SuperTokens and the Core still perform the authoritative conflict checks and reject linking if the recipe user or account information belongs to another primary user. The single-tenant and multi-tenant Python examples above already initialize the configured `accountlinking` recipe exactly once. Do not initialize it again. ```ts import { RecipeUserId, User } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; const accountLinkingForWebAuthnMFA = AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, ) => { if (session === undefined || session.getTenantId() !== tenantId) { return { shouldAutomaticallyLink: false }; } const sessionRecipeUserId = session.getRecipeUserId().getAsString(); const isMakingSessionUserPrimary = user === undefined && newAccountInfo.recipeUserId?.getAsString() === sessionRecipeUserId; const isLinkingWebAuthnToSessionUser = newAccountInfo.recipeId === "webauthn" && user?.id === session.getUserId(); if (!isMakingSessionUserPrimary && !isLinkingWebAuthnToSessionUser) { return { shouldAutomaticallyLink: false }; } return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; }, }); // Add accountLinkingForWebAuthnMFA once to the recipeList passed to supertokens.init. ``` ### 3. Configure the WebAuthn RP ID and origin WebAuthn validates the browser origin independently of your API domain. If your website is `https://app.example.com` and your API is `https://api.example.com`, use the website origin for `origin`. The RP ID must be the website hostname (`app.example.com`) or a registrable parent domain (`example.com`) whose scope you intentionally accept. Production origins must use HTTPS. For tenant custom domains, keep the allowed RP ID and exact origin in server-side configuration or a trusted database indexed by the validated tenant ID. Reject unknown tenants. Never derive or reflect either value from `Origin`, `Host`, `X-Forwarded-Host`, or other request headers: an attacker may control those headers, and changing RP values can break credential scoping or allow ceremonies for an unintended domain. ```ts import WebAuthn from "supertokens-node/recipe/webauthn"; const relyingPartyByTenant: Record = { public: { relyingPartyId: "example.com", origin: "https://app.example.com", }, customer1: { relyingPartyId: "login.customer.example", origin: "https://login.customer.example", }, }; function getRelyingParty(tenantId: string) { const relyingParty = relyingPartyByTenant[tenantId]; if (relyingParty === undefined) { throw new Error("WebAuthn is not configured for this tenant"); } return relyingParty; } const webAuthnWithTrustedRelyingParties = WebAuthn.init({ getRelyingPartyId: async ({ tenantId }) => getRelyingParty(tenantId).relyingPartyId, getOrigin: async ({ tenantId }) => getRelyingParty(tenantId).origin, }); // Use webAuthnWithTrustedRelyingParties instead of webauthn.init() in the recipeList above. ``` ```python from typing import Dict, Optional from supertokens_python.framework import BaseRequest from supertokens_python.recipe import webauthn from supertokens_python.recipe.webauthn import WebauthnConfig from supertokens_python.types.base import UserContext relying_party_by_tenant: Dict[str, Dict[str, str]] = { "public": { "relying_party_id": "example.com", "origin": "https://app.example.com", }, "customer1": { "relying_party_id": "login.customer.example", "origin": "https://login.customer.example", }, } def get_relying_party(tenant_id: str) -> Dict[str, str]: relying_party = relying_party_by_tenant.get(tenant_id) if relying_party is None: raise ValueError("WebAuthn is not configured for this tenant") return relying_party async def get_relying_party_id( *, tenant_id: str, request: Optional[BaseRequest], user_context: UserContext, ) -> str: return get_relying_party(tenant_id)["relying_party_id"] async def get_origin( *, tenant_id: str, request: Optional[BaseRequest], user_context: UserContext, ) -> str: return get_relying_party(tenant_id)["origin"] web_authn_with_trusted_relying_parties = webauthn.init( config=WebauthnConfig( get_relying_party_id=get_relying_party_id, get_origin=get_origin, ) ) # Use web_authn_with_trusted_relying_parties instead of webauthn.init() in the recipe_list above. ``` ### 4. Configure the frontend We start by modifying the `init` function call on the frontend like so: You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx import supertokens from "supertokens-auth-react"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import webauthn from "supertokens-auth-react/recipe/webauthn"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. webauthn.init(), MultiFactorAuth.init(), Multitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO"; }, }; }, }, }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. supertokensUIWebAuthn.init(), supertokensUIMultiFactorAuth.init(), supertokensUIMultitenancy.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getTenantId: async (context) => { return "TODO"; }, }; }, }, }), ], }); ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import WebAuthn from "supertokens-web-js/recipe/webauthn"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), WebAuthn.init(), ], }); ``` You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: This change is in your auth route config. ```tsx import supertokens from "supertokens-auth-react"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import webauthn from "supertokens-auth-react/recipe/webauthn"; supertokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. webauthn.init(), MultiFactorAuth.init({ firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], }), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ // other recipes.. supertokensUIWebAuthn.init(), supertokensUIMultiFactorAuth.init({ firstFactors: [ supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, ], }), ], }); ``` This change goes in the `supertokens-web-js` SDK config at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import WebAuthn from "supertokens-web-js/recipe/webauthn"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), WebAuthn.init(), ], }); ``` On the frontend, the `MultiFactorAuth` recipe initialization only requires the first factors to be configured. The secondary factors will be determined based on a request to the backend. Add the WebAuthn pre-built UI to render the SuperTokens component: :::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] ::: ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; function App() { return (
{getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ /* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI, ])} // ... other routes
); } ```
```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; function App() { if (canHandleRoute([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI])) { return getRoutingComponent([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI]); } return {/*Your app*/}; } ```
We start by initialising the MFA and WebAuthn recipe on the frontend like so: :::success[This step is not applicable for mobile apps. Please continue reading.] ::: ```tsx import SuperTokens from "supertokens-web-js"; import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import WebAuthn from "supertokens-web-js/recipe/webauthn"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... MultiFactorAuth.init(), WebAuthn.init(), ], }); ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [ // other recipes... supertokensMultiFactorAuth.init(), supertokensWebAuthn.init(), ], }); ``` After the first factor login, you should start by checking the access token payload and see if the MFA claim's `v` boolean is `false`. 'If it's not, then you can redirect the user to the application page. If it's `false`, the frontend then needs to [call the MFA endpoint](/references/fdi/multifactorauth-recipe/getmfainfo) to get information about which factor the user should be asked to complete next. Based on the initial backend configuration, the `next` array will contain `["webauthn"]`. To complete the secondary factor you need to take into account if the users has previously configured a passkey or not. You can determine this by checking if the `alreadySetup` array contains `"webauthn"`. #### Sign up flow Support for this flow is not available in the mobile SDK. You will have to call the [backend API](/references/fdi/introduction) directly. First, call the [**Register WebAuthn Credential**](/references/fdi/webauthn-recipe/webauthnregistercredential) endpoint to register the passkey. Afterwards call the [**Sign Up with WebAuthn**](/references/fdi/webauthn-recipe/webauthnsignup) to complete the second factor sign up process. ```ts import Webauthn from "supertokens-web-js/recipe/webauthn"; async function secondFactorSignUp(email: string, userContext: Record) { const response = await Webauthn.registerCredentialWithSignUp({ email, shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` ```ts check=false reason="script-tag example relies on the WebAuthn global provided by the loaded SuperTokens bundle" async function secondFactorSignUp(email: string, userContext: Record) { const response = await supertokensWebAuthn.registerCredentialWithSignUp({ email, shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` #### Sign in flow Support for this flow is not available in the mobile SDK. You will have to call the [backend API](/references/fdi/introduction) directly. Call the [**Sign in with WebAuthn**](/references/fdi/webauthn-recipe/webauthnsignin) endpoint to complete the secondary factor flow. ```ts import Webauthn from "supertokens-web-js/recipe/webauthn"; async function secondFactorSignUp(userContext: Record) { const response = await Webauthn.authenticateCredentialWithSignIn({ shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` ```ts check=false reason="script-tag example relies on the WebAuthn global provided by the loaded SuperTokens bundle" async function secondFactorSignUp(userContext: Record) { const response = await supertokensWebAuthn.authenticateCredentialWithSignIn({ shouldTryLinkingWithSessionUser: true, userContext, }); return response.status === "OK"; } ``` That's it! :tada: Based on this configuration, users first access the authentication form which shows the `emailpassword` and `thirdparty` options. After first factor completion, they access the WebAuthn form to finalize the authentication attempt. --- # Claims validation Source: https://supertokens.com/docs/additional-verification/session-verification/claim-validation ## Overview **SuperTokens** provides two approaches for working with authorization data: 1. **Session Claims**: An abstraction that includes automatic validation and refresh capabilities 2. **Access Token Payload**: A basic way to check the token payload In most cases, the recommended approach is to use session claims. Use the following table to understand the differences between the two approaches. :::caution[Enforce authorization on the backend] Frontend claim checks are user-experience controls only. Client code and client-readable payloads can be bypassed or modified. Every protected API must verify the session and enforce its required claim validators on the backend. ::: | Feature | Session Claims | Access Token Payload | | ------------------------------------ | -------------- | -------------------- | | Store simple static data | ✅ | ✅ | | Built-in validation | ✅ | ❌ | | Automatic refresh mechanism | ✅ | ❌ | | Graceful validation failure handling | ✅ | ❌ | | Lightweight implementation | ❌ | ✅ | | No validation overhead | ❌ | ✅ | This guide shows you how to use each method. ## References ### Session claim interface ```tsx import { RecipeUserId } from "supertokens-node"; import { JSONObject, UserContext } from "supertokens-node/types"; interface SessionClaim { // Unique identifier for the claim. // For a `boolean` claim (for example if the email is verified or not), this would be a string like `"st-ev"`. readonly key: string; /** * Fetches the current value of this claim for the user. * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. */ fetchValue( userId: string, recipeUserId: RecipeUserId, tenantId: string, currentPayload: JSONObject | undefined, userContext: UserContext, ): Promise | T | undefined; /** * Removes the claim from the payload, by cloning and updating the entire object. * * @returns The modified payload object */ removeFromPayload(payload: JSONObject, userContext: UserContext): JSONObject; /** * Gets the value of the claim stored in the payload * * @returns Claim value */ getValueFromPayload(payload: JSONObject, userContext: UserContext): T | undefined; } ``` The SDK provides a few base claim classes which make it easy for you to implement your own claims: - [`PrimitiveClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts): Use this to add any primitive type value (`boolean`, `string`, `number`) to the session payload. - [`PrimitiveArrayClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts): Use this to add any primitive array type value (`boolean[]`, `string[]`, `number[]`) to the session payload. - [`BooleanClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/booleanClaim.ts): A special case of the `PrimitiveClaim`, used to add a `boolean` type claim. All the recipe claims are built around these primitives: - [`EmailVerificationClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/emailverification/emailVerificationClaim.ts): This stores information about whether the user has verified their email. - [`RolesClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/userroles/userRoleClaim.ts): This stores the list of roles associated with a user. - [`PermissionClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/userroles/permissionClaim.ts): This stores the list of permissions associated with the user. #### On the frontend Like the backend, the frontend also has the concept of session claim objects which need to conform to the following interface: ```tsx type SessionClaim = { // Refresh the claim values based on an async API call refresh(userContext: any): Promise; // Returns the value from the session claim getValueFromPayload(payload: any, userContext: any): T | undefined; // Returns the last time the claim was refreshed getLastFetchedTime(payload: any, userContext: any): number | undefined; }; ``` When used, these objects provide a way for the SuperTokens SDK to update the claim values when needed. For example, in the built-in email verification claim, the `refresh` function calls the backend API to check if the email has verification. That API in turn updates the session claim to reflect the email verification status. This way, even if the system marked the email as verified in offline mode, the frontend can get the email verification status update automatically. Like the backend SDK, the frontend SDK also exposes a few base claims: - [`BooleanClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/booleanClaim.ts) - [`PrimitiveClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/primitiveClaim.ts) - [`PrimitiveArrayClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/primitiveArrayClaim.ts) Once you add a claim to the session, specify the checks that need to run on them during session verification. For example, if an API should allow access only to `admin` roles, there must be a way to tell SuperTokens to do that check. This is where claim validators come into the picture. Here is the shape for a claim validator object: ```tsx check=false reason="interface excerpt depends on SessionClaim defined in the preceding reference" type SessionClaimValidator = { // Identifies the session claim validator // Used to know which validator failed in case multiple of them undergo checking at the same time. // The value of this is typically the same as the claim object's `key`, but you can set it to anything else. id: string; // A reference to the claim object that's associated with this validator. claim: SessionClaim; // Determines if the value of the claim should undergo fetching again. // In the built-in validators, this function typically returns `true` if the claim does not exist in the `payload`, or if it's too old. shouldRefetch: (payload: any, userContext: any) => boolean | Promise; /** extracts the claim value from the input `payload` (typically using `claim.getValueFromPayload`), and determines if the validator check has passed or not. * For example, if the validator aims to enforce that the user has verified their email, and if the claim value is `false`, then this function would return: * { * isValid: false, * reason: { * message: "wrong value", * expectedValue: true, * actualValue: false * } * } */ validate: (payload: any, userContext: any) => Promise; }; type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: any }; ``` Conceptually, SuperTokens runs the following session claim validation process during session verification. This pseudocode omits recipe user ID, tenant ID, user context, asynchronous operations, and the actual payload update flow: ```tsx check=false reason="algorithm pseudocode intentionally omits declarations and concrete SDK types" function validateSessionClaims(accessToken, claimValidators) { payload = accessToken.getPayload(); // Step 1: refetch claims if required for (validator in claimValidators) { if (validator.shouldRefetch(payload)) { claimValue = validator.claim.fetchValue(accessToken.sub); payload = validator.claim.addToPayload_internal(payload, claimValue); } } failedClaims = []; // Step 2: Validate all claims for (validator in claimValidators) { validationResult = validator.validate(payload); if (!validationResult.isValid) { failedClaims.push({ id: validator.id, reason: validationResult.reason }); } } return failedClaims; } ``` The built-in base claims (`PrimitiveClaim`, `PrimitiveArrayClaim`, `BooleanClaim`) all expose a set of useful validators: - [`PrimitiveClaim.validators.hasValue(val, maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts#L50): This function call returns a validator object that enforces that the session claim has the specified `val`. - [`PrimitiveArrayClaim.validators.includes(val, maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L50): This checks if the the session claims value, which is an array, includes the input `val`. - [`PrimitiveArrayClaim.validators.excludes(val, maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L91): This checks if the the session claims value, which is an array, excludes the input `val`. - [`PrimitiveArrayClaim.validators.includesAll(val[], maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L136): This checks if the session claims value, which is an array, includes all the items in the input `val[]`. - [`PrimitiveArrayClaim.validators.excludesAll(val[], maxAgeInSeconds?)`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts#L178): This checks if the session claims value, which is an array, excludes all the items in the input `val[]`. In all the above claim validators, the `maxAgeInSeconds`/`maxAge` input (which is optional) governs how often to refetch the session claim value: - A value of `0` causes it to refetch the claim value each time a check happens. - If not passed, base claim validators refetch only when the claim is missing. Recipe-specific validators can use other defaults. For example, the email verification `isVerified` validator refetches a `false` value after ten seconds by default and has no default maximum age for a `true` value. The user roles validators do not set a default maximum age. ```tsx interface SessionClaim { readonly key: string; fetchValue(userId: string, userContext: any): Promise; addToPayload(payload: any, value: T): any; getValueFromPayload(payload: any): T | undefined; } ``` ## Before you start :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: --- ## Using session claims SuperTokens sessions have a property called `accessTokenPayload`. This is a `JSON` object which you can access on the frontend and backend. The key-values in this JSON payload refer to **claims**. ### 1. Create a custom claim ```tsx import { BooleanClaim } from "supertokens-node/recipe/session/claims"; const SecondFactorClaim = new BooleanClaim({ key: "2fa-completed", fetchValue: () => false, }); ``` ### 2. Add claim validators #### Backend global validation ```tsx check=false reason="example uses the application-defined SecondFactorClaim from the preceding setup" import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, getGlobalClaimValidators: async function (input) { return [...input.claimValidatorsAddedByOtherRecipes, SecondFactorClaim.validators.isTrue()]; }, }; }, }, }), ], }); ``` #### Backend route-specific validation ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { UserRoleClaim } from "supertokens-node/recipe/userroles"; let app = express(); app.post( "/admin-only", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ], }), async (req, res) => { // Only admin users can access this endpoint }, ); ``` #### Frontend validation This controls frontend rendering only. Apply the equivalent validator to every protected backend route, as shown above. ```tsx import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import { UserRoleClaim } from "supertokens-auth-react/recipe/userroles"; const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ]} > {props.children} ); }; ``` ### 3. Handle validation failures #### Backend custom error handling ```tsx check=false reason="walkthrough excerpt relies on helper functions or values defined in surrounding steps" import { Error as STError } from "supertokens-node/recipe/session"; import { UserRoleClaim } from "supertokens-node/recipe/userroles"; if (roles === undefined || !roles.includes("admin")) { throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoleClaim.key, }, ], }); } ``` #### Frontend redirection ```tsx import { SessionAuth } from "supertokens-auth-react/recipe/session"; import { UserRoleClaim } from "supertokens-auth-react/recipe/userroles"; const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, { ...UserRoleClaim.validators.includes("admin"), onFailureRedirection: () => "/not-an-admin", }, ]} > {props.children} ); }; ``` --- ## Using the Access Token Payload The access token payload is a simple way to store custom data that needs to be accessible on both the frontend and the backend. ### 1. Add Custom Claims to the Access Token Payload :::note[The access token payload has a set of protected claims that cannot be overwritten.] SuperTokens reserves these for standard or internal use cases. Those claims are: `sub`, `iat`, `exp`, `sessionHandle`, `refreshTokenHash1`, `parentRefreshTokenHash1`, `antiCsrfToken`, `rsub`, `tId`, and `stt`. Trying to overwrite these values results in errors in the authentication flow process. ::: You can add custom claims to the access token payload in two ways: #### During session creation ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { let userId = input.userId; // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, someKey: "someValue", }; return originalImplementation.createNewSession(input); }, }; }, }, }), ], }); ``` #### Post Session Creation ```tsx 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("/updateinfo", verifySession(), async (req: SessionRequest, res) => { let session = req.session; await session!.mergeIntoAccessTokenPayload({ newKey: "newValue" }); res.json({ message: "successfully updated access token payload" }); }); ``` ### 2. Read the Access Token Payload #### On the backend ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; let app = express(); app.get("/myApi", verifySession(), async (req, res) => { let session = req.session; let accessTokenPayload = session.getAccessTokenPayload(); let customClaimValue = accessTokenPayload.customClaim; }); ``` #### On the frontend Use frontend payload values only for display and navigation. Do not authorize access to backend data from this check. ```tsx import Session from "supertokens-auth-react/recipe/session"; async function someFunc() { if (await Session.doesSessionExist()) { let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); let customClaimValue = accessTokenPayload.customClaim; } } ``` --- ## See also --- # Protect backend routes Source: https://supertokens.com/docs/additional-verification/session-verification/protect-api-routes ## Overview Use the `Verify Session` middleware when your framework supports middleware. Otherwise, call `Get Session` directly. Both methods validate the complete SuperTokens session token and configured session claims. Manual JWT verification is a fallback for platforms without a released SuperTokens backend SDK. ## Before you start :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: --- ## Using `Verify Session` This function acts as a middleware inside your API endpoints. Hence, it requires that your backend framework supports the concept of middlewares. Besides checking for a session, it also writes responses to the client on its own, based on the session's validity and the provided configuration. ```tsx 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(), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }); ``` ```tsx 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(), }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //... }, }); ``` ```tsx 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(), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { let userId = awsEvent.session!.getUserId(); //.... } exports.handler = verifySession(likeComment); ``` ```tsx 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(), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //.... }); ``` ```tsx 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()) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //.... } } ``` ```tsx 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()(req, res, next); }, req, res, ); let userId = req.session!.getUserId(); //.... } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" 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") @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { let userId = session.getUserId(); //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) }) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(nil), likeCommentAPI) } // This is a function that wraps the supertokens verification function // to work the 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 likeCommentAPI(c *gin.Context) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends(verify_session())): user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="route fragment assumes an existing framework application" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/update-jwt', methods=['POST']) @verify_session() def like_comment(): session: SessionContainer = g.supertokens user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="session attribute is injected by framework middleware" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def like_comment(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() print(user_id) ``` ```tsx check=false reason="application example imports local modules defined elsewhere" 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 }); } let userId = session!.getUserId(); //.... return NextResponse.json({}); }); } ``` ```tsx check=false reason="public interface excerpt omits dependent SDK type declarations" interface Session { /** * Destroys this session in the database and on the frontend. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the session is successfully revoked. */ revokeSession(userContext?: Record): Promise; /** * Retrieves the session data stored in the database associated with the session. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the session data. */ getSessionDataFromDatabase(userContext?: Record): Promise; /** * Sets a new JSON object to the session data stored in the database. * @param newSessionData The new session data to store. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the session data is updated. */ updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; /** * Returns the user ID of the logged-in user. * @param userContext Optional context object for additional data. * @returns The user ID as a string. */ getUserId(userContext?: Record): string; /** * Returns the `RecipeUserId` object for the session. It represents the user ID of the specific login method for this user. * @param userContext Optional context object for additional data. * @returns The `RecipeUserId`. */ getRecipeUserId(userContext?: Record): RecipeUserId; /** * Returns the tenant ID of the session. The default value is "public" if multi-tenancy is not used. * @param userContext Optional context object for additional data. * @returns The tenant ID as a string. */ getTenantId(userContext?: Record): string; /** * Returns the access token's payload for this session. This includes user-defined claims, standard claims, and SuperTokens specific ones. * @param userContext Optional context object for additional data. * @returns The access token payload. */ getAccessTokenPayload(userContext?: Record): any; /** * Returns the `sessionHandle` for this session, a unique string constant for each session. * @param userContext Optional context object for additional data. * @returns The session handle as a string. */ getHandle(userContext?: Record): string; /** * Returns an object containing the raw string representation of all tokens associated with the session, along with an update status. * @returns An object with accessToken, refreshToken, antiCsrfToken, frontToken, and accessAndFrontTokenUpdated. */ getAllSessionTokensDangerously(): { accessToken: string; refreshToken: string | undefined; antiCsrfToken: string | undefined; frontToken: string; accessAndFrontTokenUpdated: boolean; }; /** * Returns the raw string access token for this session. * @param userContext Optional context object for additional data. * @returns The access token as a string. */ getAccessToken(userContext?: Record): string; /** * Adds key/value pairs into a JSON object in the access token. Setting a key to null removes it from the payload. * @param accessTokenPayloadUpdate The updates to apply to the access token payload. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the payload is updated. */ mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record): Promise; /** * Returns the time in milliseconds of when this session was created. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the creation time in milliseconds. */ getTimeCreated(userContext?: Record): Promise; /** * Returns the time in milliseconds of when this session will expire if not refreshed. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the expiry time in milliseconds. */ getExpiry(userContext?: Record): Promise; /** * Asserts the validity of custom session claims using provided validators. * @param claimValidators An array of session claim validators. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the claim assertions are complete. */ assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise; /** * Fetches and sets a custom claim in the session. * @param claim The session claim to fetch and set. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the fetched claim. */ fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise; /** * Sets the value of a session claim. * @param claim The session claim to update. * @param value The new value for the claim. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the claim value is set. */ setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise; /** * Gets the value of a session claim. * @param claim The session claim to retrieve the value for. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the claim value, or undefined if not found. */ getClaimValue(claim: SessionClaim, userContext?: Record): Promise; /** * Removes a session claim. * @param claim The session claim to remove. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the claim is removed. */ removeClaim(claim: SessionClaim, userContext?: Record): Promise; /** * Attaches the session to a request-response cycle. * @param reqResInfo Information about the request-response. * @param userContext Optional context object for additional data. * @returns A promise or void once the session is attached. */ attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record): Promise | void; } ``` ```go check=false reason="public interface excerpt omits dependent SDK types" type TypeSessionContainer struct { // Destroys this session in the database and on the frontend. RevokeSession func() error // Retrieves the session data stored in the database associated with the session. GetSessionDataInDatabase func() (map[string]interface{}, error) // Sets a new JSON object to the session data stored in the database. // `newSessionData` is the new session data to store. UpdateSessionDataInDatabase func(newSessionData map[string]interface{}) error // Returns the user ID of the logged-in user. GetUserID func() string // Returns the tenant ID of the session. // Default value is "public" if multi-tenancy is not used. GetTenantId func() string // Returns the access token's payload for this session. // Includes user-defined claims, standard claims, and SuperTokens specific ones. GetAccessTokenPayload func() map[string]interface{} // Returns the `sessionHandle` for this session, // a unique string constant for each session. GetHandle func() string // Returns an object containing the raw string representation // of all tokens associated with the session, along with an update status. GetAllSessionTokensDangerously func() SessionTokens // Returns the raw string access token for this session. GetAccessToken func() string // Returns the time in milliseconds of when this session was created. GetTimeCreated func() (uint64, error) // Returns the time in milliseconds of when this session will expire if not refreshed. GetExpiry func() (uint64, error) // Context-aware methods that provide the same functionality as their counterparts above while considering user context // Destroys this session in the database and on the frontend with user context. RevokeSessionWithContext func(userContext supertokens.UserContext) error // Retrieves the session data stored in the database associated with the session with user context. GetSessionDataInDatabaseWithContext func(userContext supertokens.UserContext) (map[string]interface{}, error) // Sets a new JSON object to the session data stored in the database with user context. UpdateSessionDataInDatabaseWithContext func(newSessionData map[string]interface{}, userContext supertokens.UserContext) error // Returns the user ID of the logged-in user with user context. GetUserIDWithContext func(userContext supertokens.UserContext) string // Returns the tenant ID of the session with user context. GetTenantIdWithContext func(userContext supertokens.UserContext) string // Returns the access token's payload for this session with user context. GetAccessTokenPayloadWithContext func(userContext supertokens.UserContext) map[string]interface{} // Returns the `sessionHandle` for this session with user context. GetHandleWithContext func(userContext supertokens.UserContext) string // Returns the raw string access token for this session with user context. GetAccessTokenWithContext func(userContext supertokens.UserContext) string // Returns the time in milliseconds of when this session was created with user context. GetTimeCreatedWithContext func(userContext supertokens.UserContext) (uint64, error) // Returns the time in milliseconds of when this session will expire if not refreshed with user context. GetExpiryWithContext func(userContext supertokens.UserContext) (uint64, error) // Adds key/value pairs into a JSON object in the access token with user context. // Setting a key to nil removes it from the payload. MergeIntoAccessTokenPayloadWithContext func(accessTokenPayloadUpdate map[string]interface{}, userContext supertokens.UserContext) error // Asserts the validity of custom session claims using provided validators with user context. AssertClaimsWithContext func(claimValidators []claims.SessionClaimValidator, userContext supertokens.UserContext) error // Fetches and sets a custom claim in the session with user context. FetchAndSetClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error // Sets the value of a session claim with user context. SetClaimValueWithContext func(claim *claims.TypeSessionClaim, value interface{}, userContext supertokens.UserContext) error // Gets the value of a session claim with user context. // Returns the value or nil if not found. GetClaimValueWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) interface{} // Removes a session claim with user context. RemoveClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error // Attaches the session to a request-response cycle with user context. AttachToRequestResponseWithContext func(info RequestResponseInfo, userContext supertokens.UserContext) error // Adds key/value pairs into a JSON object in the access token. // Setting a key to nil removes it from the payload. MergeIntoAccessTokenPayload func(accessTokenPayloadUpdate map[string]interface{}) error // Asserts the validity of custom session claims using provided validators. AssertClaims func(claimValidators []claims.SessionClaimValidator) error // Fetches and sets a custom claim in the session. FetchAndSetClaim func(claim *claims.TypeSessionClaim) error // Sets the value of a session claim. SetClaimValue func(claim *claims.TypeSessionClaim, value interface{}) error // Gets the value of a session claim. // Returns the value or nil if not found. GetClaimValue func(claim *claims.TypeSessionClaim) interface{} // Removes a session claim. RemoveClaim func(claim *claims.TypeSessionClaim) error // Attaches the session to a request-response cycle. AttachToRequestResponse func(info RequestResponseInfo) error } ``` ```python # exclude-from-type-checking class Session: # Destroys this session in the database and on the frontend. # Optional user_context can be used for additional contextual data. async def revoke_session(self, user_context: Optional[Dict[str, Any]] = None) -> None: pass # Retrieves the session data stored in the database associated with the session. # Optional user_context can be used for additional contextual data. async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: pass # Sets a new JSON object to the session data stored in the database. # `new_session_data` is the new session data to store. # Optional user_context can be used for additional contextual data. async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Returns the user ID of the logged-in user. # Optional user_context can be used for additional contextual data. def get_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Returns the `RecipeUserId` object for the session. # This represents the user ID of the specific login method for this user. # Optional user_context can be used for additional contextual data. def get_recipe_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> RecipeUserId: pass # Returns the tenant ID of the session. # Default value is "public" if multi-tenancy is not used. # Optional user_context can be used for additional contextual data. def get_tenant_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Returns the access token's payload for this session. # Includes user-defined claims, standard claims, and SuperTokens specific ones. # Optional user_context can be used for additional contextual data. def get_access_token_payload(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: pass # Returns the `sessionHandle` for this session, # a unique string constant for each session. # Optional user_context can be used for additional contextual data. def get_handle(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Returns an object containing the raw string representation # of all tokens associated with the session, along with an update status. def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: pass # Returns the raw string access token for this session. # Optional user_context can be used for additional contextual data. def get_access_token(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Adds key/value pairs into a JSON object in the access token. # Setting a key to None removes it from the payload. # `access_token_payload_update` contains the updates to apply. # Optional user_context can be used for additional contextual data. async def merge_into_access_token_payload(self, access_token_payload_update: JSONObject, user_context: Optional[Dict[str, Any]] = None) -> None: pass # Returns the time in milliseconds of when this session was created. # Optional user_context can be used for additional contextual data. async def get_time_created(self, user_context: Optional[Dict[str, Any]] = None) -> int: pass # Returns the time in milliseconds of when this session will expire if not refreshed. # Optional user_context can be used for additional contextual data. async def get_expiry(self, user_context: Optional[Dict[str, Any]] = None) -> int: pass # Asserts the validity of custom session claims using provided validators. # `claim_validators` is an array of session claim validators. # Optional user_context can be used for additional contextual data. async def assert_claims(self, claim_validators: List[SessionClaimValidator], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Fetches and sets a custom claim in the session. # `claim` is the session claim to fetch and set. # Optional user_context can be used for additional contextual data. async def fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Sets the value of a session claim. # `claim` is the session claim to update. # `value` is the new value for the claim. # Optional user_context can be used for additional contextual data. async def set_claim_value(self, claim: SessionClaim[_T], value: _T, user_context: Optional[Dict[str, Any]] = None) -> None: pass # Gets the value of a session claim. # `claim` is the session claim to retrieve the value for. # Optional user_context can be used for additional contextual data. # Returns a promise that resolves with the claim value, or None if not found. async def get_claim_value(self, claim: SessionClaim[_T], user_context: Optional[Dict[str, Any]] = None) -> Union[_T, None]: pass # Removes a session claim. # `claim` is the session claim to remove. # Optional user_context can be used for additional contextual data. async def remove_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Attaches the session to a request-response cycle. # `req_res_info` contains information about the request-response. # user_context provides contextual data for request processing. async def attach_to_request_response(self, request: BaseRequest, transfer_method: TokenTransferMethod, user_context: Dict[str, Any]) -> None: pass ```
### `getSessionDataFromDatabase` vs `getAccessTokenPayload`
| | `getSessionDataFromDatabase` | `getAccessTokenPayload` | |--------------------------------------|-----------------------------|-------------------------| | **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | | **Speed** | Slower (requires a network call) | Faster (no network call required) | | **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | | **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | | **Persistence** | Updated via `updateSessionDataInDatabase` | Updated via `mergeIntoAccessTokenPayload` |
### `GetSessionDataFromDatabase` vs `GetAccessTokenPayload`
| | `GetSessionDataFromDatabase` | `GetAccessTokenPayload` | |--------------------------------------|-----------------------------|-------------------------| | **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | | **Speed** | Slower (requires a network call) | Faster (no network call required) | | **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | | **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | | **Persistence** | Updated via `UpdateSessionDataInDatabase` | Updated via `MergeIntoAccessTokenPayload` |
### `get_session_data_from_database` vs `get_access_token_payload`
| | `get_session_data_from_database` | `get_access_token_payload` | |--------------------------------------|-----------------------------|-------------------------| | **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | | **Speed** | Slower (requires a network call) | Faster (no network call required) | | **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | | **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | | **Persistence** | Updated via `update_session_data_in_database` | Updated via `merge_into_access_token_payload` |
### Optional session verification To make an API endpoint accessible even if there is no session update the middleware call to mark the session as not required. ```tsx 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({ sessionRequired: false }), (req: SessionRequest, res) => { if (req.session !== undefined) { let userId = req.session.getUserId(); } else { // user is not logged in... } }); ``` ```tsx 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({ sessionRequired: false }), }, ], }, handler: async (req: SessionRequest, res) => { if (req.session !== undefined) { let userId = req.session.getUserId(); } else { // user is not logged in... } }, }); ``` ```tsx 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({ sessionRequired: false }), }, (req: SessionRequest, res) => { if (req.session !== undefined) { let userId = req.session.getUserId(); } else { // user is not logged in... } }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { if (awsEvent.session !== undefined) { let userId = awsEvent.session.getUserId(); } else { // user is not logged in... } } exports.handler = verifySession(likeComment, { sessionRequired: false }); ``` ```tsx 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({ sessionRequired: false }), (ctx: SessionContext, next) => { if (ctx.session !== undefined) { let userId = ctx.session.getUserId(); } else { // user is not logged in... } }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/like-comment") @intercept(verifySession({ sessionRequired: false })) @response(200) handler() { let session = (this.ctx as SessionContext).session; if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } } } ``` ```tsx 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: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ sessionRequired: false })(req, res, next); }, req, res, ); let session = (req as SessionRequest).session; if (session !== undefined) { let userId = session.getUserId(); // session exists } else { // session doesn't exist } //.... } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import { OptionalAuthGuard } from "./auth/optionalAuth.guard"; @Controller() export class ExampleController { @Post("example") @UseGuards(new OptionalAuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { if (session !== undefined) { let userId = session.getUserId(); // session exists } else { // session doesn't exist } //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession sessionRequired := false session.VerifySession(&sessmodels.VerifySessionOptions{ SessionRequired: &sessionRequired, }, likeCommentAPI).ServeHTTP(rw, r) }) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession sessionRequired := false router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ SessionRequired: &sessionRequired, }), likeCommentAPI) } // This is a function that wraps the supertokens verification function // to work the 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 likeCommentAPI(c *gin.Context) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession sessionRequired := false r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ SessionRequired: &sessionRequired, }, likeCommentAPI)) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession sessionRequired := false router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ SessionRequired: &sessionRequired, }, likeCommentAPI)).Methods(http.MethodPost) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```python check=false reason="route fragment assumes an existing framework application" from typing import Optional from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post("/like_comment") async def like_comment( session: Optional[SessionContainer] = Depends( verify_session(session_required=False) ), ): if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```python check=false reason="route fragment assumes an existing framework application" from typing import Union from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/update-jwt', methods=['POST']) @verify_session(session_required=False) def like_comment(): session: Union[SessionContainer, None] = g.supertokens if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```python check=false reason="session attribute is injected by framework middleware" from typing import Optional, cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session(session_required=False) async def like_comment(request: HttpRequest): session: Optional[SessionContainer] = cast(Optional[SessionContainer], request.supertokens) if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```tsx check=false reason="application example imports local modules defined elsewhere" 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 }); } if (session !== undefined) { let userId = session.getUserId(); // session exists } else { // session doesn't exist } //.... return NextResponse.json({}); }, { sessionRequired: false }, ); } ``` ### Verify the claims of a session To check if there are certain claims in the session as part of the verification process you can override the session validators. For example, you may want to check that the session has the `admin` role claim for certain APIs, or that the user has completed MFA, multi-factor authentication. You can achieve this by including the user role claim validator in the middleware `global validators` option. The `global validators` represent other validators that apply to all API routes by default. This may include things like a validator that ensures that the user's email is verified. ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user is an admin. } exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); router.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user is an admin. }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), ) @response(200) async handler() { // All validator checks have passed and the user is an admin. } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], })(req, res, next); }, req, res, ); // All validator checks have passed and the user is an admin. } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { @Post("example") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), ) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user is an admin. return true; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, exampleAPI).ServeHTTP(rw, r) }) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` ```go import ( "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }), exampleAPI) } // This is a function that wraps the supertokens verification function // to work the 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 exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, exampleAPI)) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, exampleAPI)).Methods(http.MethodPost) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] ) )): # All validator checks have passed and the user has a verified email address pass ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.route('/update-jwt', methods=['POST']) @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` ```python from django.http import HttpRequest from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserRoles from "supertokens-node/recipe/userroles"; 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 }); } // All validator checks have passed and the user is an admin. return NextResponse.json({}); }, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }, ); } ``` :::tip[Feature] You can also [build your own custom claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: --- ## Using `Get Session` The `Get Session` function performs the same verification as the middleware, but it does not complete error responses on its own. It can still attach updated access, front, or anti-CSRF tokens to the supplied response. It throws errors that you can catch and handle. If these errors remain unhandled, the SuperTokens error handler catches these errors and writes to the client (like the `verifySession` middleware). You should use this function if your framework does not support middlewares or if you want additional control over error management. ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; let app = express(); app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res); let userId = session.getUserId(); //.... } catch (err) { next(err); } }); ``` ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", handler: async (req, res) => { let session = await Session.getSession(req, res); let userId = session.getUserId(); //... }, }); ``` ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res); let userId = session.getUserId(); //.... }); ``` ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent); let userId = session.getUserId(); //.... } exports.handler = middleware(likeComment); ``` ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx); let userId = session.getUserId(); //.... }); ``` ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx); let userId = session.getUserId(); //.... } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res); }, req, res, ); let userId = session.getUserId(); //.... } ``` ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import Session from "supertokens-node/recipe/session"; @Controller() export class ExampleController { @Post("example") async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); const userId = session.getUserId(); //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func likeCommentAPI(w http.ResponseWriter, r *http.Request) { sessionContainer, err := session.GetSession(r, w, nil) if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: send 500 to client } return } userID := sessionContainer.GetUserID() // TODO: API logic... fmt.Println(userID) } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi.requests import Request from supertokens_python.recipe.session.asyncio import get_session @app.post('/like-comment') async def like_comment(request: Request): session = await get_session(request) if session is None: raise Exception("Should never come here") user_id = session.get_user_id() print(user_id) # TODO ``` ```python check=false reason="route fragment assumes an existing framework application" from flask import request from supertokens_python.recipe.session.syncio import get_session @app.route('/like-comment', methods=['POST']) def like_comment(): session = get_session(request) if session is None: raise Exception("Should never come here") user_id = session.get_user_id() print(user_id) # TODO ``` ```python from django.http import HttpRequest from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): session = await get_session(request) if session is None: raise Exception("Should never come here") user_id = session.get_user_id() print(user_id) # TODO ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; import Session from "supertokens-node/recipe/session"; import { backendConfig } from "@/app/config/backend"; SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withPreParsedRequestResponse( request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const session = await Session.getSession(baseRequest, baseResponse); let userId = session.getUserId(); return NextResponse.json({}); }, ); } ``` ```tsx check=false reason="public interface excerpt omits dependent SDK type declarations" interface Session { /** * Destroys this session in the database and on the frontend. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the session is successfully revoked. */ revokeSession(userContext?: Record): Promise; /** * Retrieves the session data stored in the database associated with the session. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the session data. */ getSessionDataFromDatabase(userContext?: Record): Promise; /** * Sets a new JSON object to the session data stored in the database. * @param newSessionData The new session data to store. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the session data is updated. */ updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; /** * Returns the user ID of the logged-in user. * @param userContext Optional context object for additional data. * @returns The user ID as a string. */ getUserId(userContext?: Record): string; /** * Returns the `RecipeUserId` object for the session. It represents the user ID of the specific login method for this user. * @param userContext Optional context object for additional data. * @returns The `RecipeUserId`. */ getRecipeUserId(userContext?: Record): RecipeUserId; /** * Returns the tenant ID of the session. The default value is "public" if multi-tenancy is not used. * @param userContext Optional context object for additional data. * @returns The tenant ID as a string. */ getTenantId(userContext?: Record): string; /** * Returns the access token's payload for this session. This includes user-defined claims, standard claims, and SuperTokens specific ones. * @param userContext Optional context object for additional data. * @returns The access token payload. */ getAccessTokenPayload(userContext?: Record): any; /** * Returns the `sessionHandle` for this session, a unique string constant for each session. * @param userContext Optional context object for additional data. * @returns The session handle as a string. */ getHandle(userContext?: Record): string; /** * Returns an object containing the raw string representation of all tokens associated with the session, along with an update status. * @returns An object with accessToken, refreshToken, antiCsrfToken, frontToken, and accessAndFrontTokenUpdated. */ getAllSessionTokensDangerously(): { accessToken: string; refreshToken: string | undefined; antiCsrfToken: string | undefined; frontToken: string; accessAndFrontTokenUpdated: boolean; }; /** * Returns the raw string access token for this session. * @param userContext Optional context object for additional data. * @returns The access token as a string. */ getAccessToken(userContext?: Record): string; /** * Adds key/value pairs into a JSON object in the access token. Setting a key to null removes it from the payload. * @param accessTokenPayloadUpdate The updates to apply to the access token payload. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the payload is updated. */ mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record): Promise; /** * Returns the time in milliseconds of when this session was created. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the creation time in milliseconds. */ getTimeCreated(userContext?: Record): Promise; /** * Returns the time in milliseconds of when this session will expire if not refreshed. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the expiry time in milliseconds. */ getExpiry(userContext?: Record): Promise; /** * Asserts the validity of custom session claims using provided validators. * @param claimValidators An array of session claim validators. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the claim assertions are complete. */ assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise; /** * Fetches and sets a custom claim in the session. * @param claim The session claim to fetch and set. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the fetched claim. */ fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise; /** * Sets the value of a session claim. * @param claim The session claim to update. * @param value The new value for the claim. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the claim value is set. */ setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise; /** * Gets the value of a session claim. * @param claim The session claim to retrieve the value for. * @param userContext Optional context object for additional data. * @returns A promise that resolves with the claim value, or undefined if not found. */ getClaimValue(claim: SessionClaim, userContext?: Record): Promise; /** * Removes a session claim. * @param claim The session claim to remove. * @param userContext Optional context object for additional data. * @returns A promise that resolves when the claim is removed. */ removeClaim(claim: SessionClaim, userContext?: Record): Promise; /** * Attaches the session to a request-response cycle. * @param reqResInfo Information about the request-response. * @param userContext Optional context object for additional data. * @returns A promise or void once the session is attached. */ attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record): Promise | void; } ``` ```go check=false reason="public interface excerpt omits dependent SDK types" type TypeSessionContainer struct { // Destroys this session in the database and on the frontend. RevokeSession func() error // Retrieves the session data stored in the database associated with the session. GetSessionDataInDatabase func() (map[string]interface{}, error) // Sets a new JSON object to the session data stored in the database. // `newSessionData` is the new session data to store. UpdateSessionDataInDatabase func(newSessionData map[string]interface{}) error // Returns the user ID of the logged-in user. GetUserID func() string // Returns the tenant ID of the session. // Default value is "public" if multi-tenancy is not used. GetTenantId func() string // Returns the access token's payload for this session. // Includes user-defined claims, standard claims, and SuperTokens specific ones. GetAccessTokenPayload func() map[string]interface{} // Returns the `sessionHandle` for this session, // a unique string constant for each session. GetHandle func() string // Returns an object containing the raw string representation // of all tokens associated with the session, along with an update status. GetAllSessionTokensDangerously func() SessionTokens // Returns the raw string access token for this session. GetAccessToken func() string // Returns the time in milliseconds of when this session was created. GetTimeCreated func() (uint64, error) // Returns the time in milliseconds of when this session will expire if not refreshed. GetExpiry func() (uint64, error) // Context-aware methods that provide the same functionality as their counterparts above while considering user context // Destroys this session in the database and on the frontend with user context. RevokeSessionWithContext func(userContext supertokens.UserContext) error // Retrieves the session data stored in the database associated with the session with user context. GetSessionDataInDatabaseWithContext func(userContext supertokens.UserContext) (map[string]interface{}, error) // Sets a new JSON object to the session data stored in the database with user context. UpdateSessionDataInDatabaseWithContext func(newSessionData map[string]interface{}, userContext supertokens.UserContext) error // Returns the user ID of the logged-in user with user context. GetUserIDWithContext func(userContext supertokens.UserContext) string // Returns the tenant ID of the session with user context. GetTenantIdWithContext func(userContext supertokens.UserContext) string // Returns the access token's payload for this session with user context. GetAccessTokenPayloadWithContext func(userContext supertokens.UserContext) map[string]interface{} // Returns the `sessionHandle` for this session with user context. GetHandleWithContext func(userContext supertokens.UserContext) string // Returns the raw string access token for this session with user context. GetAccessTokenWithContext func(userContext supertokens.UserContext) string // Returns the time in milliseconds of when this session was created with user context. GetTimeCreatedWithContext func(userContext supertokens.UserContext) (uint64, error) // Returns the time in milliseconds of when this session will expire if not refreshed with user context. GetExpiryWithContext func(userContext supertokens.UserContext) (uint64, error) // Adds key/value pairs into a JSON object in the access token with user context. // Setting a key to nil removes it from the payload. MergeIntoAccessTokenPayloadWithContext func(accessTokenPayloadUpdate map[string]interface{}, userContext supertokens.UserContext) error // Asserts the validity of custom session claims using provided validators with user context. AssertClaimsWithContext func(claimValidators []claims.SessionClaimValidator, userContext supertokens.UserContext) error // Fetches and sets a custom claim in the session with user context. FetchAndSetClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error // Sets the value of a session claim with user context. SetClaimValueWithContext func(claim *claims.TypeSessionClaim, value interface{}, userContext supertokens.UserContext) error // Gets the value of a session claim with user context. // Returns the value or nil if not found. GetClaimValueWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) interface{} // Removes a session claim with user context. RemoveClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error // Attaches the session to a request-response cycle with user context. AttachToRequestResponseWithContext func(info RequestResponseInfo, userContext supertokens.UserContext) error // Adds key/value pairs into a JSON object in the access token. // Setting a key to nil removes it from the payload. MergeIntoAccessTokenPayload func(accessTokenPayloadUpdate map[string]interface{}) error // Asserts the validity of custom session claims using provided validators. AssertClaims func(claimValidators []claims.SessionClaimValidator) error // Fetches and sets a custom claim in the session. FetchAndSetClaim func(claim *claims.TypeSessionClaim) error // Sets the value of a session claim. SetClaimValue func(claim *claims.TypeSessionClaim, value interface{}) error // Gets the value of a session claim. // Returns the value or nil if not found. GetClaimValue func(claim *claims.TypeSessionClaim) interface{} // Removes a session claim. RemoveClaim func(claim *claims.TypeSessionClaim) error // Attaches the session to a request-response cycle. AttachToRequestResponse func(info RequestResponseInfo) error } ``` ```python # exclude-from-type-checking class Session: # Destroys this session in the database and on the frontend. # Optional user_context can be used for additional contextual data. async def revoke_session(self, user_context: Optional[Dict[str, Any]] = None) -> None: pass # Retrieves the session data stored in the database associated with the session. # Optional user_context can be used for additional contextual data. async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: pass # Sets a new JSON object to the session data stored in the database. # `new_session_data` is the new session data to store. # Optional user_context can be used for additional contextual data. async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Returns the user ID of the logged-in user. # Optional user_context can be used for additional contextual data. def get_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Returns the `RecipeUserId` object for the session. # This represents the user ID of the specific login method for this user. # Optional user_context can be used for additional contextual data. def get_recipe_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> RecipeUserId: pass # Returns the tenant ID of the session. # Default value is "public" if multi-tenancy is not used. # Optional user_context can be used for additional contextual data. def get_tenant_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Returns the access token's payload for this session. # Includes user-defined claims, standard claims, and SuperTokens specific ones. # Optional user_context can be used for additional contextual data. def get_access_token_payload(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: pass # Returns the `sessionHandle` for this session, # a unique string constant for each session. # Optional user_context can be used for additional contextual data. def get_handle(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Returns an object containing the raw string representation # of all tokens associated with the session, along with an update status. def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: pass # Returns the raw string access token for this session. # Optional user_context can be used for additional contextual data. def get_access_token(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass # Adds key/value pairs into a JSON object in the access token. # Setting a key to None removes it from the payload. # `access_token_payload_update` contains the updates to apply. # Optional user_context can be used for additional contextual data. async def merge_into_access_token_payload(self, access_token_payload_update: JSONObject, user_context: Optional[Dict[str, Any]] = None) -> None: pass # Returns the time in milliseconds of when this session was created. # Optional user_context can be used for additional contextual data. async def get_time_created(self, user_context: Optional[Dict[str, Any]] = None) -> int: pass # Returns the time in milliseconds of when this session will expire if not refreshed. # Optional user_context can be used for additional contextual data. async def get_expiry(self, user_context: Optional[Dict[str, Any]] = None) -> int: pass # Asserts the validity of custom session claims using provided validators. # `claim_validators` is an array of session claim validators. # Optional user_context can be used for additional contextual data. async def assert_claims(self, claim_validators: List[SessionClaimValidator], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Fetches and sets a custom claim in the session. # `claim` is the session claim to fetch and set. # Optional user_context can be used for additional contextual data. async def fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Sets the value of a session claim. # `claim` is the session claim to update. # `value` is the new value for the claim. # Optional user_context can be used for additional contextual data. async def set_claim_value(self, claim: SessionClaim[_T], value: _T, user_context: Optional[Dict[str, Any]] = None) -> None: pass # Gets the value of a session claim. # `claim` is the session claim to retrieve the value for. # Optional user_context can be used for additional contextual data. # Returns a promise that resolves with the claim value, or None if not found. async def get_claim_value(self, claim: SessionClaim[_T], user_context: Optional[Dict[str, Any]] = None) -> Union[_T, None]: pass # Removes a session claim. # `claim` is the session claim to remove. # Optional user_context can be used for additional contextual data. async def remove_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: pass # Attaches the session to a request-response cycle. # `req_res_info` contains information about the request-response. # user_context provides contextual data for request processing. async def attach_to_request_response(self, request: BaseRequest, transfer_method: TokenTransferMethod, user_context: Dict[str, Any]) -> None: pass ```
### `getSessionDataFromDatabase` vs `getAccessTokenPayload`
| | `getSessionDataFromDatabase` | `getAccessTokenPayload` | |--------------------------------------|-----------------------------|-------------------------| | **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | | **Speed** | Slower (requires a network call) | Faster (no network call required) | | **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | | **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | | **Persistence** | Updated via `updateSessionDataInDatabase` | Updated via `mergeIntoAccessTokenPayload` |
### `GetSessionDataFromDatabase` vs `GetAccessTokenPayload`
| | `GetSessionDataFromDatabase` | `GetAccessTokenPayload` | |--------------------------------------|-----------------------------|-------------------------| | **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | | **Speed** | Slower (requires a network call) | Faster (no network call required) | | **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | | **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | | **Persistence** | Updated via `UpdateSessionDataInDatabase` | Updated via `MergeIntoAccessTokenPayload` |
### `get_session_data_from_database` vs `get_access_token_payload`
| | `get_session_data_from_database` | `get_access_token_payload` | |--------------------------------------|-----------------------------|-------------------------| | **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | | **Speed** | Slower (requires a network call) | Faster (no network call required) | | **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | | **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | | **Persistence** | Updated via `update_session_data_in_database` | Updated via `merge_into_access_token_payload` |
### Optional session verification To make an API endpoint accessible even if there is no session update the middleware call to mark the session as not required. ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; let app = express(); app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } catch (err) { next(err); } }); ``` ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", handler: async (req, res) => { let session = await Session.getSession(req, res, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //... }, }); ``` ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } exports.handler = middleware(likeComment); ``` ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res, { sessionRequired: false }); }, req, res, ); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } ``` ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import Session from "supertokens-node/recipe/session"; @Controller() export class ExampleController { @Post("example") async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { sessionRequired: false }); if (session !== undefined) { const userId = session.getUserId(); } else { // user is not logged in... } //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func likeCommentAPI(w http.ResponseWriter, r *http.Request) { sessionRequired := false sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{ SessionRequired: &sessionRequired, }) if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: send 500 to client } return } if sessionContainer != nil { // session exists userID := sessionContainer.GetUserID() fmt.Println(userID) } else { // user is not logged in } } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi import Request from supertokens_python.recipe.session.asyncio import get_session @app.post("/like-comment") async def like_comment(request: Request): session = await get_session(request, session_required=False) if session is not None: user_id = session.get_user_id() print(user_id) # TODO: else: pass # user is not logged in ``` ```python check=false reason="route fragment assumes an existing framework application" from flask import request from supertokens_python.recipe.session.syncio import get_session @app.route("/like-comment", methods=["POST"]) def like_comment(): session = get_session(request, session_required=False) if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```python from django.http import HttpRequest from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): session = await get_session(request, session_required=False) if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; import Session from "supertokens-node/recipe/session"; import { backendConfig } from "@/app/config/backend"; SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withPreParsedRequestResponse( request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const session = await Session.getSession(baseRequest, baseResponse, { sessionRequired: false }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } return NextResponse.json({}); }, ); } ``` ### Verify the claims of a session To check if there are certain claims in the session as part of the verification process you can override the session validators. For example, you may want to check that the session has the `admin` role claim for certain APIs, or that the user has completed MFA, multi-factor authentication. This can be achieved by including the user role claim validator in the middleware `global validators` option. The `global validators` represent other validators that apply to all API routes by default. This may include things like a validator that ensures that the user's email is verified. ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); //.... } catch (err) { next(err); } }); ``` ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", handler: async (req, res) => { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); //... }, }); ``` ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); //.... }); ``` ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); //.... } exports.handler = middleware(likeComment); ``` ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); //.... }); ``` ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); //.... } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); }, req, res, ); let userId = session.getUserId(); //.... } ``` ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { @Post("example") async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); const userId = session.getUserId(); //.... return true; } } ``` ```go import ( "fmt" "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/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func likeCommentAPI(w http.ResponseWriter, r *http.Request) { sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }) if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: send 500 to client } return } userID := sessionContainer.GetUserID() // TODO: API logic... fmt.Println(userID) } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi.requests import Request from supertokens_python.recipe.session.asyncio import get_session from supertokens_python.recipe.userroles import UserRoleClaim @app.post('/like-comment') async def like_comment(request: Request): session = await get_session(request, override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")]) if session is None: raise Exception("Should never come here") user_id = session.get_user_id() print(user_id) # TODO ``` ```python check=false reason="route fragment assumes an existing framework application" from flask import request from supertokens_python.recipe.session.syncio import get_session from supertokens_python.recipe.userroles import UserRoleClaim @app.route("/like-comment", methods=["POST"]) def like_comment(): session = get_session( request, override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")], ) if session is None: raise Exception("Should never come here") user_id = session.get_user_id() print(user_id) # TODO ``` ```python from django.http import HttpRequest from supertokens_python.recipe.session.asyncio import get_session from supertokens_python.recipe.userroles import UserRoleClaim async def like_comment(request: HttpRequest): session = await get_session( request, override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")], ) if session is None: raise Exception("Should never come here") user_id = session.get_user_id() print(user_id) # TODO ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; import { backendConfig } from "@/app/config/backend"; SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withPreParsedRequestResponse( request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const session = await Session.getSession(baseRequest, baseResponse, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); let userId = session.getUserId(); return NextResponse.json({}); }, ); } ``` :::tip[feature] You can also [build your own custom claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: ### Build your own middleware Both these functions perform session verification. However, `Verify Session` is a middleware that returns a reply directly to the frontend if the input access token is invalid or expired. On the other hand, `Get Session` is a function that returns a session object on successful verification. It throws an exception that you can handle if the access token expires or is invalid. Internally, `Verify Session` uses `Get Session` in the following way: ```tsx import { VerifySessionOptions } from "supertokens-node/recipe/session/types"; import { errorHandler } from "supertokens-node/framework/express"; import { NextFunction, Request, Response } from "express"; import Session from "supertokens-node/recipe/session"; import { Error as SuperTokensError } from "supertokens-node"; function verifySession(options?: VerifySessionOptions) { return async (req: Request, res: Response, next: NextFunction) => { try { (req as any).session = await Session.getSession(req, res, options); next(); } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN) { // This means that the session exists, but the access token // has expired. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.UNAUTHORISED) { // This means that the session does not exist anymore. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.INVALID_CLAIMS) { // The user is missing some required claim. // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } // OR you can use this errorHandler which will // handle all of the above errors in the default way errorHandler()(err, req, res, (err) => { next(err); }); } else { next(err); } } }; } ``` ```go import ( "context" "net/http" defaultErrors "errors" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func VerifySession(options *sessmodels.VerifySessionOptions, otherHandler http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session, err := session.GetSession(r, w, options) if err != nil { if defaultErrors.As(err, &errors.TryRefreshTokenError{}) { // This means that the session exists, but the access token // has expired. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if defaultErrors.As(err, &errors.UnauthorizedError{}) { // This means that the session does not exist anymore. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if defaultErrors.As(err, &errors.InvalidClaimError{}) { // The user is missing some required claim. // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } // OR you can use this errorHandler which will // handle all of the above errors in the default way err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: send a 500 error to the frontend } return } if session != nil { ctx := context.WithValue(r.Context(), sessmodels.SessionContext, session) otherHandler(w, r.WithContext(ctx)) } else { otherHandler(w, r) } }) } ``` ```python from functools import wraps from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast from supertokens_python.framework.flask.flask_request import FlaskRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( InvalidClaimsError, TryRefreshTokenError, UnauthorisedError, ) from supertokens_python.recipe.session.interfaces import SessionClaimValidator from supertokens_python.recipe.session.syncio import get_session from supertokens_python.types import MaybeAwaitable _T = TypeVar("_T", bound=Callable[..., Any]) def verify_session( session_required: bool = True, anti_csrf_check: Union[bool, None] = None, check_database: Optional[bool] = None, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]], ] ] = None, ) -> Callable[[_T], _T]: def session_verify(f: _T) -> _T: @wraps(f) def wrapped_function(*args: Any, **kwargs: Any): from flask import make_response, request baseRequest = FlaskRequest(request) try: session = get_session( baseRequest, session_required, anti_csrf_check, check_database, override_global_claim_validators, ) except Exception as e: if isinstance(e, TryRefreshTokenError): # This means that the session exists, but the access token # has expired. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, UnauthorisedError): # This means that the session does not exist anymore. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, InvalidClaimsError): # The user is missing some required claim. # You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. pass # OR you can raise this error which will # handle all of the above errors in the default way raise e if session is None: if session_required: raise Exception("Should never come here") baseRequest.set_session_as_none() else: baseRequest.set_session(session) response = make_response(f(*args, **kwargs)) return response return cast(_T, wrapped_function) return session_verify ``` The `errorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. The `SuperTokens.ErrorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. If `get_session` throws an error (in case the input access token is invalid or has expired), then the SuperTokens middleware added to your app handles that exception. It sends a `401` to the frontend. ### Get the session using the `Access Token` In the above snippets, `Get Session` requires the `request` object and, depending on your backend language and framework, may also require the `response` object. Either way, this version of `Get Session` automatically reads from the request. And automatically sets the response based on the update to the session tokens. Whilst this is convenient, sometimes, you may not have the `request` or `response` objects, or you may not want SuperTokens to set the tokens in the response automatically. In this case, you can use the `getSessionWithoutRequestResponse` function. This function works similarly to `getSession`, except that it doesn't depend on the `request` or `response` objects. It's your responsibility to provide this function the access token. You must write the update tokens to the response if the tokens update during this API call. ```tsx import { VerifySessionOptions } from "supertokens-node/recipe/session/types"; import { SessionContainer } from "supertokens-node/recipe/session"; import Session from "supertokens-node/recipe/session"; import { Error as SuperTokensError } from "supertokens-node"; async function verifySession(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions) { let session: SessionContainer | undefined; try { session = await Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options); } catch (err) { if (SuperTokensError.isErrorFromSuperTokens(err)) { if (err.type === Session.Error.TRY_REFRESH_TOKEN) { // This means that the session exists, but the access token // has expired. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.UNAUTHORISED) { // This means that the session does not exist anymore. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if (err.type === Session.Error.INVALID_CLAIMS) { // The user is missing some required claim. // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } } throw err; } if (session !== undefined) { // we can use the `session` container as we usually do.. // TODO: API logic... // At the end of the API logic, we must fetch all the tokens from the session container // and set them in the response headers / cookies ourselves. const tokens = session.getAllSessionTokensDangerously(); if (tokens.accessAndFrontTokenUpdated) { // TODO: set access token in response via tokens.accessToken // TODO: set front-token in response via tokens.frontToken if (tokens.antiCsrfToken) { // TODO: set anti-csrf token update in response via tokens.antiCsrfToken } } } } ``` ```go import ( defaultErrors "errors" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func VerifySession(accessToken string, antiCsrfToken *string, options *sessmodels.VerifySessionOptions) error { session, err := session.GetSessionWithoutRequestResponse(accessToken, antiCsrfToken, options) if err != nil { if defaultErrors.As(err, &errors.TryRefreshTokenError{}) { // This means that the session exists, but the access token // has expired. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if defaultErrors.As(err, &errors.UnauthorizedError{}) { // This means that the session does not exist anymore. // You can handle this in a custom way by sending a 401. // Or you can call the errorHandler middleware as shown below } else if defaultErrors.As(err, &errors.InvalidClaimError{}) { // The user is missing some required claim. // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } else { // TODO: send a 500 error to the frontend } return err } if session != nil { // we can use the `session` container as we usually do.. // TODO: API logic... // At the end of the API logic, we must fetch all the tokens from the session container // and set them in the response headers / cookies ourselves. tokens := session.GetAllSessionTokensDangerously() if tokens.AccessAndFrontendTokenUpdated { // TODO: set access token in response via tokens.accessToken // TODO: set front-token in response via tokens.frontToken if tokens.AntiCsrfToken != nil { // TODO: set anti-csrf token update in response via *tokens.AntiCsrfToken } } } return nil } ``` ```python from typing import Any, Callable, Dict, List, Optional, TypeVar from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( InvalidClaimsError, TryRefreshTokenError, UnauthorisedError, ) from supertokens_python.recipe.session.interfaces import SessionClaimValidator from supertokens_python.recipe.session.syncio import ( get_session_without_request_response, ) from supertokens_python.types import MaybeAwaitable _T = TypeVar("_T", bound=Callable[..., Any]) def verify_session( access_token: str, anti_csrf_token: Optional[str], anti_csrf_check: Optional[bool], session_required: Optional[bool], check_database: Optional[bool], override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]], ] ] = None, ): try: session = get_session_without_request_response( access_token, anti_csrf_token, anti_csrf_check, session_required, check_database, override_global_claim_validators, ) except Exception as e: if isinstance(e, TryRefreshTokenError): # This means that the session exists, but the access token # has expired. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, UnauthorisedError): # This means that the session does not exist anymore. # You can handle this in a custom way by sending a 401. # Or you can call the errorHandler middleware as shown below pass if isinstance(e, InvalidClaimsError): # The user is missing some required claim. # You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. pass # OR you can raise this error which will # handle all of the above errors in the default way raise e if session is not None: # we can use the `session` container as we usually do.. # TODO: API logic... # At the end of the API logic, we must fetch all the tokens from the session container # and set them in the response headers / cookies ourselves. tokens = session.get_all_session_tokens_dangerously() if tokens["accessAndFrontTokenUpdated"]: # TODO: set access token in response via tokens["accessToken"] # TODO: set front-token in response via tokens["frontToken"] if tokens["antiCsrfToken"] is not None: # TODO: set anti-csrf token update in response via tokens["antiCsrfToken"] pass ``` --- ## Manual JWT verification Use a released SuperTokens `verifySession`, `getSession`, or equivalent API whenever one is available. These APIs validate more than the JWT signature and expiry. For example, when you already have an access-token string rather than framework request and response objects, use the released `getSessionWithoutRequestResponse` API shown above. Manual verification is only appropriate when your platform has no released SuperTokens backend SDK, or when an API gateway cannot call one. Do not use mutable code snippets without pinned revisions as the verifier for a production system. Pin and test the JWT library and verifier implementation you maintain. A manual verifier must reject the token unless **all** of these checks pass: 1. Select the key by `kid` from `/auth/jwt/jwks.json`, and restrict verification to the expected signing algorithm (`RS256`). Do not derive the accepted algorithm from the token-controlled header. 2. Verify the signature and expiry (`exp`). 3. Validate the token type using released SuperTokens semantics: if `stt` is present, require the numeric value `0`. Released Node.js SDK 24.0.3 also accepts a missing `stt` for backward compatibility. Only accept that absence while also validating a supported SuperTokens token header/version and that version's complete required payload shape. Reject every other `stt` value or type. A valid JWT signed by a key in the JWKS is not necessarily a session access token. 4. Validate the required session payload shape and field types. Current access tokens require string values for `sub`, `sessionHandle`, `refreshTokenHash1`, `rsub`, and `tId`, plus numeric `iat` and `exp`. Treat a changed or unknown token shape as invalid rather than accepting it partially. 5. Validate every application authorization claim required by the route, such as tenant, role, permission, email verification, or MFA state. Signature verification authenticates claims; it does not authorize the request. 6. For unsafe requests authenticated by cookies, perform the configured anti-CSRF check. With `VIA_CUSTOM_HEADER`, require the `rid: session` header. With `VIA_TOKEN`, compare the `anti-csrf` request header with `antiCsrfToken` in the payload. The following access-token fields are managed by SuperTokens and must not be treated as application-defined fields: `sub`, `iat`, `exp`, `sessionHandle`, `parentRefreshTokenHash1`, `refreshTokenHash1`, `antiCsrfToken`, `rsub`, `tId`, and `stt`. :::warning[Manual verification has important limitations] A signature-only verifier does not check session revocation or run the backend SDK's global claim validators. If immediate revocation matters, ask the Core for the authoritative session state using the [Get Session Information](/references/cdi/session-recipe/getsessioninfo) route with the session handle, or the [Verify Session](/references/cdi/session-recipe/verifysession) route with the access token and `checkDatabase` set to `true`. These database-backed checks confirm whether the session still exists and has not been revoked; the Verify Session route defaults `checkDatabase` to `false`, and local JWT verification alone cannot determine revocation. Claim refresh behavior is claim-specific: each claim defines its own refresh policy, and some built-in claims have no default maximum age. Do not assume that all claims refresh on a fixed five-minute interval. ::: --- ## See also --- # Protect frontend routes Source: https://supertokens.com/docs/additional-verification/session-verification/protect-frontend-routes Protect frontend routes by requiring user sessions and verifying session claims for access control. :::caution[Frontend guards are for user experience only] Users can bypass client-side route guards or modify client-side state. Protect every API used by these pages with backend session verification and the required role, permission, MFA, or verification claim validators. A frontend check may control rendering or navigation, but it is not an authorization boundary. ::: ## Before you start :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: ---
## Protect a route You can wrap your components with the `` react component. This ensures that your component renders only if the user has logged in. If they are not logged in, the user gets redirected to the login page. You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import MyDashboardComponent from "./dashboard"; class App extends React.Component { render() { return ( {/*Components that require to be protected by authentication*/} } /> ); } } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ### Optional session requirement You can provide the `requireAuth={false}` prop when using `` as shown below: ```tsx import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SuperTokensWrapper } from "supertokens-auth-react"; import Session, { SessionAuth } from "supertokens-auth-react/recipe/session"; class App extends React.Component { render() { return ( } /> ); } } function MyDashboardComponent(props: any) { let sessionContext = Session.useSessionContext(); if (sessionContext.loading) { return null; } if (sessionContext.doesSessionExist) { // TODO: } else { // TODO: } return null; } ``` ## Check the claims of a session Sometimes, you may also want to check if there are certain claims in the session before granting access to a route. For example, you may want to check that the session has the admin role claim for certain APIs, or that the user has completed 2FA. You can achieve this using the session claims validator feature. Let's take an example of using the user roles claim to check if the session has the admin claim: ```tsx import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import { AccessDeniedScreen } from "supertokens-auth-react/recipe/session/prebuiltui"; import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-auth-react/recipe/userroles"; const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ]} > {props.children} ); }; ``` ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ], }); if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false; } ``` Above, you create a generic component called `AdminRoute`, which enforces that its child components render only if the user has the admin role. In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component, which checks if the validators pass or not. If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass your own custom component to the `accessDeniedScreen` prop. :::note[You can extend the `AdminRoute` component to check for other types of validators as well.] You can then reuse this component to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). ::: If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx import Session from "supertokens-auth-react/recipe/session"; import { UserRoleClaim } from "supertokens-auth-react/recipe/userroles"; function ProtectedComponent() { let claimValue = Session.useClaimValue(UserRoleClaim); if (claimValue.loading || !claimValue.doesSessionExist) { return null; } let roles = claimValue.value; if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin } else { // User doesn't have any roles, or is not an admin.. } } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let roles = await Session.getClaimValue({ claim: UserRoleClaim }); if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin return true; } } // either a session does not exist, or the user is not an admin return false; } ``` ## Protect a route You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function doesSessionExist() { if (await supertokensSession.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function doesSessionExist() { if (await SuperTokens.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun doesSessionExist() { if (!SuperTokens.doesSessionExist(this)) { // user has not logged in yet return } try { SuperTokens.getAccessTokenPayloadSecurely(this) // user is logged in } catch (error: java.io.IOException) { // the session expired, refresh failed, or the payload could not be read } } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func doesSessionExist() { if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely() { // user is logged in } else { // user has not logged } } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future doesSessionExist() async { if (!await SuperTokens.doesSessionExist()) { // user has not logged in yet return; } try { await SuperTokens.getAccessTokenPayloadSecurely(); // user is logged in } catch (error) { // the session expired, refresh failed, or the payload could not be read } } ``` --- ## Check the claims of a session Sometimes, you may also want to check if there are certain claims in the session before granting access to a route. For example, you may want to check that the session has the admin role claim for certain APIs, or that the user has completed 2FA. You can achieve this using the session claims validator feature. Let's take an example of using the user roles claim to check if the session has the admin claim: ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ], }); if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false; } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let validationErrors = await supertokensSession.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, supertokensUserRoles.UserRoleClaim.validators.includes("admin"), /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ ], }); if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === supertokensUserRoles.UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false; } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function getRole() { if (await SuperTokens.doesSessionExist()) { let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; if (roles.includes("admin")) { // TODO.. } else { // TODO.. } } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONArray import org.json.JSONObject class MainApplication: Application() { fun checkIfUserIsAnAdmin() { if (!SuperTokens.doesSessionExist(this)) return try { val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this) val rolesJson: JSONArray = accessTokenPayload.getJSONObject("st-role").getJSONArray("v") val roles = (0 until rolesJson.length()).map { rolesJson.getString(it) } if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } catch (error: java.io.IOException) { // the session expired, refresh failed, or the payload could not be read } } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func checkIfUserIsAnAdmin() { if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let roleObject: [String: Any] = accessTokenPayload["st-role"] as? [String: Any], let roles: [String] = roleObject["v"] as? [String] { if roles.contains("admin") { // user is an admin } else { // user is not an admin } } } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future checkIfUserIsAnAdmin() async { if (!await SuperTokens.doesSessionExist()) return; try { final accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-role")) { final roleObject = accessTokenPayload["st-role"] as Map; if (roleObject.containsKey("v")) { final roles = (roleObject["v"] as List).whereType().toList(); if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } } catch (error) { // the session expired, refresh failed, or the payload could not be read } } ``` - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let roles = await Session.getClaimValue({ claim: UserRoleClaim }); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } } // either a session does not exist, or the user is not an admin return false; } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let roles = await supertokensSession.getClaimValue({ claim: supertokensUserRoles.UserRoleClaim }); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } } // either a session does not exist, or the user is not an admin return false; } ``` :::tip[Feature] You can also [build your own custom claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: --- ## See also --- # Session verification during server-side rendering Source: https://supertokens.com/docs/additional-verification/session-verification/ssr ## Overview In Server Side Rendering (SSR) scenarios, the session verification process is slightly different. Check the following guide to understand how to adjust the flow to work with SSR. ## Before you start For an ordinary browser navigation, the browser can send cookie-based session tokens to the server performing SSR. It cannot add SuperTokens' header-transfer token to that navigation. Cookie transfer is the default; the cookie's domain, path, `SameSite`, and `Secure` attributes must allow it to reach the SSR server. :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: ## Steps ### 1. Enable sharing of cookies across subdomains If your API layer and website are on different subdomains (like `example.com` and `api.example.com`), then by default, the session tokens attach only to `api.example.com`. Change this to ensure that the session tokens attach to `.example.com` and the access token cookie goes to your web server on `example.com`. Enable this by [setting the `cookieDomain` configuration on the backend](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). ### 2. Verify the session Prefer the released session verification API for your backend framework. For example, the Node.js Next.js integration exports `getSSRSession`, which accepts the request's cookies and validates the access-token signature, expiry, and payload shape. It does not run global claim validators or perform an authoritative database check for session revocation. Treat its payload as authenticated claims, not as a complete authorization decision: explicitly validate every claim required by the page, such as tenant, role, permission, email verification, or MFA state. Before rendering sensitive protected data for which immediate revocation matters, call an authoritative backend/API endpoint that verifies the session with database checking enabled. Other backend frameworks can use `getSession` or `verifySession`, provided the SSR response propagates any token updates that the session API attaches. If your platform has no released SuperTokens SDK, follow the strict requirements in the [manual verification fallback](/additional-verification/session-verification/protect-api-routes#manual-jwt-verification), not signature-only JWT verification. If the access token is missing, invalid, or expired, redirect the user to a `/refresh-session?redirectBack=` page. You can use another local path on your website instead. On the `/refresh-session` page, you want to call the `attemptRefreshingSession` function (from the client side). This function attempts to refresh the session. If it succeeds, it returns `true`. If it fails, it returns `false`. If it returns `true`, you want to redirect the user back to the page they were on. If it returns `false`, you want to redirect the user to the login page. ### 3. Implement the refresh-session flow (`/refresh-session` page) On this path, attempt to refresh the session. This can produce either result: - **Success:** The frontend gets new access and refresh tokens. Redirect the user to the validated local path in the `redirectBack` query parameter. - **Failure:** The session has expired or the backend has revoked it. Redirect the user to the login page. Below is the code snippet that you can use on the `/refresh-session` path on the frontend ```tsx import React from "react"; import Session from "supertokens-auth-react/recipe/session"; import SuperTokens from "supertokens-auth-react"; export function AttemptRefresh() { React.useEffect(() => { let cancel = false; Session.attemptRefreshingSession().then((success) => { if (cancel) { // component has unmounted somehow.. return; } if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); window.location.href = redirectBack.origin === window.location.origin ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` : "/"; } else { // we redirect to the login page since the user // is now logged out SuperTokens.redirectToAuth(); } }); return () => { cancel = true; }; }, []); return null; } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; function attemptRefresh() { Session.attemptRefreshingSession().then((success) => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); window.location.href = redirectBack.origin === window.location.origin ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` : "/"; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login"; } }); } ``` :::warning[Server side rendering is not applicable for mobile apps.] ::: ```tsx import Session from "supertokens-web-js/recipe/session"; function attemptRefresh() { Session.attemptRefreshingSession().then((success) => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); window.location.href = redirectBack.origin === window.location.origin ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` : "/"; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login"; } }); } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" function attemptRefresh() { supertokensSession.attemptRefreshingSession().then((success) => { if (success) { // we have new session tokens, so we redirect the user back // to where they were. const urlParams = new URLSearchParams(window.location.search); const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); window.location.href = redirectBack.origin === window.location.origin ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` : "/"; } else { // we redirect to the login page since the user // is now logged out window.location.href = "/login"; } }); } ``` #### Why trigger the refresh session flow instead of redirecting the user to the login page directly? Two reasons why JWT verification can fail are: - The session tokens were not passed from the frontend: This can happen if the route is accessed when the user has logged out. - The session tokens pass from the frontend, but the JWT has expired. If the user goes to the login page directly, then in the second case, the frontend redirects the user back to the current route (since the refresh token is still valid), causing an infinite loop. To counteract this issue, redirect the user to a refresh page, which creates a new access token. In the first case, when the user is actually logged out, the refreshing fails, and they go to the login page anyway. #### Can `verifySession` or `getSession` be used during SSR? Yes. A released, read-only SSR helper such as Node.js Next.js `getSSRSession` can authenticate the access token, but the application must still validate the authorization claims required by the page. The helper does not run global claim validators or authoritatively check database revocation. For sensitive data that requires immediate revocation, verify the session through an authoritative backend/API endpoint with database checking before rendering. You can also use `verifySession` or `getSession`, but those APIs may attach rotated or updated tokens to the response. The SSR server must propagate those updates because frontend SDK network interceptors do not run for the browser's navigation request. If the SSR process should not receive SuperTokens Core credentials, use the read-only helper for authentication and an authoritative backend/API check for sensitive authorization, or use a manual verifier that meets every requirement in the linked fallback guide. --- ## See also --- # WebSocket session verification Source: https://supertokens.com/docs/additional-verification/session-verification/with-websocket ## Overview WebSocket connections begin with an HTTP upgrade request, and Socket.IO may begin with HTTP long-polling. Eligible cookies can accompany these requests. Browser WebSocket clients cannot set arbitrary headers, although non-browser clients can. This guide passes an access token in Socket.IO's handshake `auth` payload when cookie authentication is not suitable. ## Before you start :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: ## Steps ### 1. Expose the JWT to the frontend Ensure that the JWT is available to the frontend. This is already the case in header-based authentication. If you use cookie-based authentication, set the following boolean to `true` in `session.init` on the backend: ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "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{ ExposeAccessTokenToFrontendInCookieBasedAuth: true, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( expose_access_token_to_frontend_in_cookie_based_auth=True ) ] ) ``` ### 2. Send the access token when connecting Fetch the access token before creating the socket connection. Send it in Socket.IO's `auth` payload, not the query string; query-string tokens are commonly retained in URLs, proxy logs, and monitoring systems. Always use `https`/`wss` in production and enforce an approved origin list on the server. ```tsx check=false reason="Socket.IO server instance is created in the surrounding framework setup" import Session from "supertokens-web-js/recipe/session"; async function initSocketConnection() { const token = await Session.getAccessToken(); if (token === undefined) { throw new Error("User is not logged in"); } const socket = io.connect("https://api.example.com", { auth: { token }, }); return socket; } ``` - The `Session.getAccessToken()` function auto refreshes the session before returning the JWT if needed. ### 3. Verify the session Use a released backend session API rather than a signature-only JWT verifier. The Node.js example below validates the complete SuperTokens access-token structure, expiry, session claims, and revocation state before accepting the connection. ```tsx check=false reason="Socket.IO server instance and application authorization validators are defined by the application" import Session from "supertokens-node/recipe/session"; io.use(async (socket, next) => { try { const token = socket.handshake.auth.token; if (typeof token !== "string") { throw new Error("Missing access token"); } const session = await Session.getSessionWithoutRequestResponse(token, undefined, { antiCsrfCheck: false, checkDatabase: true, }); socket.data.accessToken = token; socket.data.session = session; next(); } catch { next(new Error("Authentication error")); } }).on("connection", (socket) => { const payload = socket.data.session.getAccessTokenPayload(); const expiresInMs = Math.max(0, payload.exp * 1000 - Date.now()); const expiryTimer = setTimeout(() => socket.disconnect(true), expiresInMs); socket.on("message", async (message: string, acknowledge?: (error?: string) => void) => { try { // Recheck revocation and configured authorization claims before privileged events. await Session.getSessionWithoutRequestResponse(socket.data.accessToken, undefined, { antiCsrfCheck: false, checkDatabase: true, }); io.emit("message", message); acknowledge?.(); } catch { acknowledge?.("Authentication error"); socket.disconnect(true); } }); socket.on("disconnect", () => clearTimeout(expiryTimer)); }); ``` :::warning[Define a connection-lifetime policy] Authentication at connection time is not enough: a connection can outlive token expiry, session revocation, or an authorization change. Disconnect no later than the access token's `exp`, and revalidate before privileged events or on a short application-defined interval. Use database checking when immediate session revocation matters. After disconnecting, the client must refresh its session and reconnect with a new access token. ::: --- ## See also --- # Initial Setup Source: https://supertokens.com/docs/additional-verification/user-roles/initial-setup Add SuperTokens roles and permissions to this application. Inspect the existing backend, frontend, session configuration, and tenant model first. Define roles and permissions that match the application's resources, initialize the UserRoles recipe, assign roles to users, and protect backend and frontend routes. Check whether role data should be included in access tokens, preserve existing authorization conventions, and validate authorized, unauthorized, and cross-tenant access. ## Overview When you work with the `UserRoles` recipe you should follow these steps: 1. **Create a role and assign permissions to it** 2. **Assign roles to users** 3. **Protect frontend and backend routes by verifying that the user has the correct role and permissions** The next sections show you the actual instructions on how to achieve this. ## Before you start :::info[Multi Tenancy] In a multi tenant setup, roles, and permissions share across all tenants, however, the mapping of users to roles are on a per tenant level. For example, if you create one role (`"admin"`) and add permissions to it for `read:all` and `write:all`, this role can reuse across all tenants. If you have user ID `user1` that has access to `tenant1` and `tenant2`, you can give them the `admin` role in `tenant1`, but not in `tenant2`. ::: ## Steps ### 1. Initialize the recipe ```tsx import SuperTokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [UserRoles.init()], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ userroles.Init(nil), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import userroles init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..." ), framework='...', recipe_list=[ # Initialize other recipes as seen in the quick setup guide userroles.init() ] ) ``` By default, the user roles recipe adds the roles and permission information into a user's session (if they have assigned roles & permissions). If you do not want roles or permissions information in the session, or want to manually add it yourself, you can provide the following input configs to the `UserRoles.init` function: ```tsx import UserRoles from "supertokens-node/recipe/userroles"; UserRoles.init({ skipAddingRolesToAccessToken: true, skipAddingPermissionsToAccessToken: true, }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ AppInfo: supertokens.AppInfo{ /*...*/ }, RecipeList: []supertokens.Recipe{ userroles.Init(&userrolesmodels.TypeInput{ SkipAddingRolesToAccessToken: true, SkipAddingPermissionsToAccessToken: true, }), }, }) } ``` ```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import userroles init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..." ), framework='...', recipe_list=[ userroles.init(skip_adding_roles_to_access_token=True, skip_adding_permissions_to_access_token=True) ] ) ``` ### 2. Create roles and permissions Roles and permissions are simple string values. They should represent entities and actions that are relevant to your business logic. To create them use the next code snippet as a reference. When you create a role you can also include the permissions that the role should have. Create Role ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function createRole() { const response = await UserRoles.createNewRoleOrAddPermissions("user", ["read"]); if (response.createdNewRole === false) { // The role already exists } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func createRole() { resp, err := userroles.CreateNewRoleOrAddPermissions("user", []string{ "read", }, nil) if err != nil { // TODO: Handle error return } if resp.OK.CreatedNewRole == false { // The role already exists } } ``` ```python from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions async def create_role(): res = await create_new_role_or_add_permissions("user", ["read"]) if not res.created_new_role: # The role already existed pass ``` ```python from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions def create_role(): res = create_new_role_or_add_permissions("user", ["read"]) if not res.created_new_role: # The role already existed pass ``` ```bash curl --location --request PUT '/recipe/role' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "user", "permissions": [ "read" ] }' ``` ### 3. Assign roles to users After you create a user account, you can assign roles to them. You can do this by overriding the authentication recipes with a function that calls the `UserRoles` API after a successful sign up. The next code snippet shows you what function to call to connect a user to a role. To figure out where to call that function, check the documentation for the authentication method that you use: [passwordless](/authentication/passwordless/hooks-and-overrides), [email-password](/authentication/email-password/hooks-and-overrides) or [third-party](/authentication/social/hooks-and-overrides). ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function addRoleToUser(userId: string) { const response = await UserRoles.addRoleToUser("public", userId, "user"); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } if (response.didUserAlreadyHaveRole === true) { // The user already had the role } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func addRoleToUser(userId string) { response, err := userroles.AddRoleToUser("public", userId, "user", nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists return } if response.OK.DidUserAlreadyHaveRole { // The user already had the role } } ``` ```python from supertokens_python.recipe.userroles.asyncio import add_role_to_user from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def add_role_to_user_func(user_id: str): role = "user" res = await add_role_to_user("public", user_id, role) if isinstance(res, UnknownRoleError): # No such role exists return if res.did_user_already_have_role: # User already had this role pass ``` ```python from supertokens_python.recipe.userroles.syncio import add_role_to_user from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def add_role_to_user_func(user_id: str): role = "user" res = add_role_to_user("public", user_id, role) if isinstance(res, UnknownRoleError): # No such role exists return if res.did_user_already_have_role: # User already had this role pass ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/user/role' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "role": "user" }' ``` #### Assign roles to a session If you want to associate a role to a user after you create a session, you can do this by manually calling the function described in the next code snippet. For information on how to access the session object that you need to pass to the function, check either the [`Verify Session`](/additional-verification/session-verification/protect-api-routes#using-verify-session) or the [`Get Session`](/additional-verification/session-verification/protect-api-routes#using-get-session) documentation. ```tsx import { UserRoleClaim, PermissionClaim } from "supertokens-node/recipe/userroles"; import { SessionContainer } from "supertokens-node/recipe/session"; async function addRolesAndPermissionsToSession(session: SessionContainer) { // we add the user's roles to the user's session await session.fetchAndSetClaim(UserRoleClaim); // we add the permissions of a user to the user's session await session.fetchAndSetClaim(PermissionClaim); } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" ) func addRolesAndPermissionsToSession(session sessmodels.SessionContainer) error { // we add the user's roles to the user's session err := session.FetchAndSetClaim(userrolesclaims.UserRoleClaim) if err != nil { return err } // we add the user's permissions to the user's session err = session.FetchAndSetClaim(userrolesclaims.PermissionClaim) if err != nil { return err } return nil } ``` ```python from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim async def add_roles_and_permissions_to_session(session: SessionContainer): # we add the user's roles to the user's session await session.fetch_and_set_claim(UserRoleClaim) # we add the user's permissions to the user's session await session.fetch_and_set_claim(PermissionClaim) ``` ```python from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim def add_roles_and_permissions_to_session(session: SessionContainer): # we add the user's roles to the user's session session.sync_fetch_and_set_claim(UserRoleClaim) # we add the user's permissions to the user's session session.sync_fetch_and_set_claim(PermissionClaim) ``` :::info[Multi Tenancy] Whilst roles and permissions share across apps, the association of roles to users is on a per tenant level. If using SDK functions to add a role to a user, you can also pass in a `tenantId` to the function. This tells SuperTokens to add the role for that user for that tenant. In the code examples above, the `"public"` `tenantId` goes in, which is the default `tenantId` for users. You can fetch the user's `tenantId` from their current session, or from their user object (which you can fetch using their `userId`). Note that if you associate a role to a user ID for a tenant, and that user ID doesn't belong to that tenant, then the operation still succeeds. ::: --- ## See also --- # Introduction Source: https://supertokens.com/docs/additional-verification/user-roles/introduction ## Overview **SuperTokens** provides a way to set different levels of authorization control through the `User Roles` feature. With it, you can create roles and permissions, assign them to your users and limit access based on your application logic. Basic User Roles Architecture ## Getting started You can go through the *Initial Setup* page for a quick tutorial on how to configure the feature. Go through a quick tutorial that shows you how to add the **Email Verification** recipe to your application. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Limit the access to your frontend and backend routes based on roles and permissions. Create new roles and permissions, assign them to users and manage them. --- # Protect frontend and backend routes Source: https://supertokens.com/docs/additional-verification/user-roles/protecting-routes ## Overview To limit access to your application resources based on roles and permissions you have to use the `UserRoleClaim` inside the session validation logic. ## Before you start :::info[Access token guidance] If you are implementing [**Unified Login**](/authentication/unified-login/introduction), which uses **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to validate them. You have to check for the `roles` claim in the token payload. ::: --- ## Protect backend routes Override the global claim validators to integrate role verification in the standard flow. The `GlobalValidators` represents other validators that apply to all API routes by default. This may include a validator that enforces that the user has verified their email. To perform the verification follow these steps: - Add the `UserRoleClaim` validator to the `Verify Session` function which makes sure that the user has specific roles. - Optionally, add a `PermissionClaim` validator to enforce a permission. ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, ], }, handler: async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), }, async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; async function updateBlog(awsEvent: SessionEvent) { // All validator checks have passed and the user is an admin. } exports.handler = verifySession(updateBlog, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); router.post( "/update-blog", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), async (ctx: SessionContext, next) => { // All validator checks have passed and the user is an admin. }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; class SetRole { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), ) @response(200) async handler() { // All validator checks have passed and the user is an admin. } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; export default async function setRole(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], })(req, res, next); }, req, res, ); // All validator checks have passed and the user is an admin. } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { @Post("example") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") ], }), ) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user is an admin. return true; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, exampleAPI).ServeHTTP(rw, r) }) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` ```go import ( "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }), exampleAPI) } // This is a function that wraps the supertokens verification function // to work the 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 exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, exampleAPI)) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```go import ( "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "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() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, exampleAPI)).Methods(http.MethodPost) } func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] ) )): # All validator checks have passed and the user has a verified email address pass ``` ```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.route('/update-jwt', methods=['POST']) @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` ```python from django.http import HttpRequest from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserRoles from "supertokens-node/recipe/userroles"; 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 }); } // All validator checks have passed and the user is an admin. return NextResponse.json({}); }, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }, ); } ``` ### Custom validation If you want to have more complex access control you can get the list of roles attached to the session and introduce your own logic. ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; let app = express(); app.post("/update-blog", verifySession(), async (req: SessionRequest, res) => { const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. }); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/update-blog", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post( "/update-blog", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; async function updateBlog(awsEvent: SessionEvent) { const roles = await awsEvent.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. } exports.handler = verifySession(updateBlog); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { const roles = await ctx.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; class UpdateBlog { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/update-blog") @intercept(verifySession()) @response(200) async handler() { const roles = await ((this.ctx as any).session as Session.SessionContainer).getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; export default async function updateBlog(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. await superTokensNextWrapper( async (next) => { throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); }, req, res, ); } // user is an admin.. } ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; import { Error as STError } from "supertokens-node/recipe/session"; @Controller() export class ExampleController { @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise { const roles = await session.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { // this error tells SuperTokens to return a 403 to the frontend. throw new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); } // user is an admin.. return true; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" sessionerror "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { http.ListenAndServe("SERVER ADDRESS", corsMiddleware( supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Handle your APIs.. if r.URL.Path == "/update-blog" && r.Method == "POST" { // Calling the API with session verification session.VerifySession(nil, postExample).ServeHTTP(rw, r) return } })))) } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) { //... }) } func postExample(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) if roles == nil || !contains(roles.([]interface{}), "admin") { err := supertokens.ErrorHandler(sessionerror.InvalidClaimError{ Msg: "User is not an admin", InvalidClaims: []claims.ClaimValidationError{ {ID: userrolesclaims.UserRoleClaim.Key}, }, }, r, w) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } // User is an admin... } func contains(s []interface{}, e string) bool { for _, a := range s { if a == e { return true } } return false } ``` ```go import ( "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" sessionerror "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { router := gin.New() router.POST("/update-blog", verifySession(nil), postExample) } // 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() } } // This is the API handler. func postExample(c *gin.Context) { sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) if roles == nil || !contains(roles.([]interface{}), "admin") { err := supertokens.ErrorHandler(sessionerror.InvalidClaimError{ Msg: "User is not an admin", InvalidClaims: []claims.ClaimValidationError{ {ID: userrolesclaims.UserRoleClaim.Key}, }, }, c.Request, c.Writer) if err != nil { http.Error(c.Writer, "Internal server error", http.StatusInternalServerError) } return } // User is an admin... } func contains(s []interface{}, e string) bool { for _, a := range s { if a == e { return true } } return false } ``` ```go import ( "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" sessionerror "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { r := chi.NewRouter() r.Post("/update-blog", session.VerifySession(nil, postExample)) } // This is the API handler. func postExample(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) if roles == nil || !contains(roles.([]interface{}), "admin") { err := supertokens.ErrorHandler(sessionerror.InvalidClaimError{ Msg: "User is not an admin", InvalidClaims: []claims.ClaimValidationError{ {ID: userrolesclaims.UserRoleClaim.Key}, }, }, r, w) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } } func contains(s []interface{}, e string) bool { for _, a := range s { if a == e { return true } } return false } ``` ```go import ( "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/claims" sessionerror "github.com/supertokens/supertokens-golang/recipe/session/errors" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { router := mux.NewRouter() router.HandleFunc("/update-blog", session.VerifySession(nil, postExample)).Methods(http.MethodPost) } // This is the API handler. func postExample(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) if roles == nil || !contains(roles.([]interface{}), "admin") { err := supertokens.ErrorHandler(sessionerror.InvalidClaimError{ Msg: "User is not an admin", InvalidClaims: []claims.ClaimValidationError{ {ID: userrolesclaims.UserRoleClaim.Key}, }, }, r, w) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } } func contains(s []interface{}, e string) bool { for _, a := range s { if a == e { return true } } return false } ``` ```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.post("/update-blog") async def update_blog_api(session: SessionContainer = Depends(verify_session())): roles = await session.get_claim_value(UserRoleClaim) if roles is None or "admin" not in roles: raise_invalid_claims_exception( "User is not an admin", [ClaimValidationError(UserRoleClaim.key, None)] ) ``` ```python check=false reason="route fragment assumes an existing framework application" from flask import Flask, g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim app = Flask(__name__) @app.route("/update-blog", methods=["POST"]) @verify_session() def set_role_api(): session: SessionContainer = g.supertokens roles = session.sync_get_claim_value(UserRoleClaim) if roles is None or "admin" not in roles: raise_invalid_claims_exception( "User is not an admin", [ClaimValidationError(UserRoleClaim.key, None)] ) ``` ```python check=false reason="initialization excerpt omits deployment connection config" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.exceptions import ( ClaimValidationError, raise_invalid_claims_exception, ) from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @verify_session() async def get_user_info_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) roles = await session.get_claim_value(UserRoleClaim) if roles is None or "admin" not in roles: raise_invalid_claims_exception( "User is not an admin", [ClaimValidationError(UserRoleClaim.key, None)] ) ``` ```tsx check=false reason="application example imports local modules defined elsewhere" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserRoles from "supertokens-node/recipe/userroles"; import { backendConfig } from "@/app/config/backend"; import { Error as STError } from "supertokens-node/recipe/session"; SuperTokens.init(backendConfig()); export function POST(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } const roles = await session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { const error = new STError({ type: "INVALID_CLAIMS", message: "User is not an admin", payload: [ { id: UserRoles.UserRoleClaim.key, }, ], }); return NextResponse.json(error, { status: 403 }); } // user is an admin.. return NextResponse.json({}); }); } ``` --- ## Protect frontend routes :::caution[Backend authorization is mandatory] Frontend role and permission checks are for user experience only; they are not an authorization boundary. Always enforce authorization on the backend before allowing access to a protected resource or operation. ::: On your frontend: 1. Verify that a session exists 2. Use the roles / permissions claim validators to enforce certain roles and permissions. 3. If the user doesn't have the right roles, the system shows an error message indicating they don't have access. ```tsx import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import { AccessDeniedScreen } from "supertokens-auth-react/recipe/session/prebuiltui"; import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-auth-react/recipe/userroles"; const AdminRoute = (props: React.PropsWithChildren) => { return ( [ ...globalValidators, UserRoleClaim.validators.includes("admin"), ]} > {props.children} ); }; ``` ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ], }); if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false; } ``` Above, you create a generic component called `AdminRoute` which enforces that its child components render only if the user has the admin role. In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component which checks if the validators pass or not. If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass a custom component to the `accessDeniedScreen` prop. :::note[You can extend the `AdminRoute` component to check for other types of validators as well. This component can then reuse to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`).] ::: If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: ```tsx import Session from "supertokens-auth-react/recipe/session"; import { UserRoleClaim } from "supertokens-auth-react/recipe/userroles"; function ProtectedComponent() { let claimValue = Session.useClaimValue(UserRoleClaim); if (claimValue.loading || !claimValue.doesSessionExist) { return null; } let roles = claimValue.value; if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin } else { // User doesn't have any roles, or is not an admin.. } } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let roles = await Session.getClaimValue({ claim: UserRoleClaim }); if (Array.isArray(roles) && roles.includes("admin")) { // User is an admin return true; } } // either a session does not exist, or the user is not an admin return false; } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let validationErrors = await Session.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, UserRoleClaim.validators.includes("admin"), /* PermissionClaim.validators.includes("modify") */ ], }); if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false; } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let validationErrors = await supertokensSession.validateClaims({ overrideGlobalClaimValidators: (globalValidators) => [ ...globalValidators, supertokensUserRoles.UserRoleClaim.validators.includes("admin"), /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ ], }); if (validationErrors.length === 0) { // user is an admin return true; } for (const err of validationErrors) { if (err.id === supertokensUserRoles.UserRoleClaim.id) { // user roles claim check failed } else { // some other claim check failed (from the global validators list) } } } // either a session does not exist, or one of the validators failed. // so we do not allow access to this page. return false; } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function getRole() { if (await SuperTokens.doesSessionExist()) { let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; if (roles.includes("admin")) { // TODO.. } else { // TODO.. } } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun checkIfUserIsAnAdmin() { val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); val roles: List = (accessTokenPayload.get("st-role") as JSONObject).get("v") as List; if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func checkIfUserIsAnAdmin() { if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let roleObject: [String: Any] = accessTokenPayload["st-role"] as? [String: Any], let roles: [String] = roleObject["v"] as? [String] { if roles.contains("admin") { // user is an admin } else { // user is not an admin } } } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future checkIfUserIsAnAdmin() async { var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-role")) { Map roleObject = accessTokenPayload["st-role"]; if (roleObject.containsKey("v")) { List roles = roleObject["v"]; if (roles.contains("admin")) { // user is an admin } else { // user is not an admin } } } } ```
- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself:
- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself:
```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { if (await Session.doesSessionExist()) { let roles = await Session.getClaimValue({ claim: UserRoleClaim }); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } } // either a session does not exist, or the user is not an admin return false; } ``` ```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { if (await supertokensSession.doesSessionExist()) { let roles = await supertokensSession.getClaimValue({ claim: supertokensUserRoles.UserRoleClaim }); if (roles !== undefined && roles.includes("admin")) { // User is an admin return true; } } // either a session does not exist, or the user is not an admin return false; } ```
--- ## See also --- # Role management actions Source: https://supertokens.com/docs/additional-verification/user-roles/role-management-actions ## Overview **SuperTokens** exposes a set of functions and APIs that you can use to have fine-grained control over roles and permissions. Actions like listing roles, creating permissions, or checking which roles you assign are available through different SDK calls. ## Before you start :::info[You can also perform most of the actions outlined on this page from the user management dashboard.] To know more about how to use it check [the documentation](/post-authentication/dashboard/user-management) ::: --- ## Create a role Create Role ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function createRole() { const response = await UserRoles.createNewRoleOrAddPermissions("user", ["read"]); if (response.createdNewRole === false) { // The role already exists } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func createRole() { resp, err := userroles.CreateNewRoleOrAddPermissions("user", []string{ "read", }, nil) if err != nil { // TODO: Handle error return } if resp.OK.CreatedNewRole == false { // The role already exists } } ``` ```python from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions async def create_role(): res = await create_new_role_or_add_permissions("user", ["read"]) if not res.created_new_role: # The role already existed pass ``` ```python from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions def create_role(): res = create_new_role_or_add_permissions("user", ["read"]) if not res.created_new_role: # The role already existed pass ``` ```bash curl --location --request PUT '/recipe/role' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "user", "permissions": [ "read" ] }' ``` --- ## List roles ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getAllRoles() { const roles: string[] = (await UserRoles.getAllRoles()).roles; } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getAllRoles() { response, err := userroles.GetAllRoles(nil) if err != nil { // TODO: Handle error return } _ = response.OK.Roles } ``` ```python from supertokens_python.recipe.userroles.asyncio import get_all_roles async def create_role(): _ = (await get_all_roles()).roles ``` ```python from supertokens_python.recipe.userroles.syncio import get_all_roles def create_role(): _ = get_all_roles().roles ``` ```bash curl --location --request GET 'http://localhost:3567/recipe/roles' \ --header 'api-key: ' ``` --- ## Delete a role You can delete any role you have created, if the role you are trying to delete does not exist then this has no effect. ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function deleteRole() { // Delete the user role const response = await UserRoles.deleteRole("user"); if (!response.didRoleExist) { // There was no such role } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func deleteRole() { // Delete the user role response, err := userroles.DeleteRole("user", nil) if err != nil { // TODO: Handle error return } if response.OK.DidRoleExist == false { // There was no such role } } ``` ```python from supertokens_python.recipe.userroles.asyncio import delete_role async def delete_role_function(): res = await delete_role("user") if res.did_role_exist: # The role actually existed pass ``` ```python from supertokens_python.recipe.userroles.syncio import delete_role def delete_role_function(): res = delete_role("user") if res.did_role_exist: # The role actually existed pass ``` ```bash curl --location --request POST 'http://localhost:3567/recipe/role/remove' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "admin" }' ``` --- ## Add permissions The SDK function only adds missing permissions and does not have any effect on permissions that are already assigned to a role. ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function addPermissionForRole() { // Add the "write" permission to the "user" role await UserRoles.createNewRoleOrAddPermissions("user", ["write"]); } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func addPermissionForRole() { // Add the write permission to the user role _, err := userroles.CreateNewRoleOrAddPermissions("user", []string{"write"}, nil) if err != nil { // TODO: Handle error return } } ``` ```python from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions async def add_permission_for_role(): await create_new_role_or_add_permissions("user", ["write"]) ``` ```python from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions def add_permission_for_role(): create_new_role_or_add_permissions("user", ["write"]) ``` :::info[Multi Tenancy] In a multi-tenant setup, roles, and permissions share across all tenants. This means that you can create a role and add permissions to it once, and reuse that role across any tenant in your app. ::: --- ## Remove permissions To remove one or more permissions from a role, first create the role before you use this function. ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function removePermissionFromRole() { // Remove the "write" permission to the "user" role const response = await UserRoles.removePermissionsFromRole("user", ["write"]); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func removePermissionFromRole() { // Remove the write permission to the user role response, err := userroles.RemovePermissionsFromRole("user", []string{"write"}, nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists } } ``` ```python from supertokens_python.recipe.userroles.asyncio import remove_permissions_from_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def remove_permission_from_role_func(): res = await remove_permissions_from_role("user", ["write"]) if isinstance(res, UnknownRoleError): # No such role exists pass ``` ```python from supertokens_python.recipe.userroles.syncio import remove_permissions_from_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def remove_permission_from_role_func(): res = remove_permissions_from_role("user", ["write"]) if isinstance(res, UnknownRoleError): # No such role exists pass ``` --- ## Get permissions by role Get a list of all permissions assigned to a role. ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getPermissionsForRole() { const response = await UserRoles.getPermissionsForRole("user"); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } const permissions: string[] = response.permissions; } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getPermissionsForRole() { // const response = await UserRoles.getPermissionsForRole("user"); response, err := userroles.GetPermissionsForRole("user", nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists return } _ = response.OK.Permissions } ``` ```python from supertokens_python.recipe.userroles.asyncio import get_permissions_for_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def remove_permission_from_role(): res = await get_permissions_for_role("user") if isinstance(res, UnknownRoleError): # No such role exists return _ = res.permissions ``` ```python from supertokens_python.recipe.userroles.syncio import get_permissions_for_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def remove_permission_from_role(): res = get_permissions_for_role("user") if isinstance(res, UnknownRoleError): # No such role exists return _ = res.permissions ``` --- ## Get roles by permission Get a list of all the roles assigned a specific permission. ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getRolesWithPermission() { const response = await UserRoles.getRolesThatHavePermission("write"); const roles: string[] = response.roles; } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getRolesWithPermission() { response, err := userroles.GetRolesThatHavePermission("write", nil) if err != nil { // TODO: Handle error return } _ = response.OK.Roles } ``` ```python from supertokens_python.recipe.userroles.asyncio import get_roles_that_have_permission async def get_roles_with_permission(): res = await get_roles_that_have_permission("write") _ = res.roles ``` ```python from supertokens_python.recipe.userroles.syncio import get_roles_that_have_permission def get_roles_with_permission(): res = get_roles_that_have_permission("write") _ = res.roles ``` --- ## Assign roles to a user ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function addRoleToUser(userId: string) { const response = await UserRoles.addRoleToUser("public", userId, "user"); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } if (response.didUserAlreadyHaveRole === true) { // The user already had the role } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func addRoleToUser(userId string) { response, err := userroles.AddRoleToUser("public", userId, "user", nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists return } if response.OK.DidUserAlreadyHaveRole { // The user already had the role } } ``` ```python from supertokens_python.recipe.userroles.asyncio import add_role_to_user from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def add_role_to_user_func(user_id: str): role = "user" res = await add_role_to_user("public", user_id, role) if isinstance(res, UnknownRoleError): # No such role exists return if res.did_user_already_have_role: # User already had this role pass ``` ```python from supertokens_python.recipe.userroles.syncio import add_role_to_user from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def add_role_to_user_func(user_id: str): role = "user" res = add_role_to_user("public", user_id, role) if isinstance(res, UnknownRoleError): # No such role exists return if res.did_user_already_have_role: # User already had this role pass ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/user/role' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "role": "user" }' ``` ## Assign roles to a session ```tsx import { UserRoleClaim, PermissionClaim } from "supertokens-node/recipe/userroles"; import { SessionContainer } from "supertokens-node/recipe/session"; async function addRolesAndPermissionsToSession(session: SessionContainer) { // we add the user's roles to the user's session await session.fetchAndSetClaim(UserRoleClaim); // we add the permissions of a user to the user's session await session.fetchAndSetClaim(PermissionClaim); } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" ) func addRolesAndPermissionsToSession(session sessmodels.SessionContainer) error { // we add the user's roles to the user's session err := session.FetchAndSetClaim(userrolesclaims.UserRoleClaim) if err != nil { return err } // we add the user's permissions to the user's session err = session.FetchAndSetClaim(userrolesclaims.PermissionClaim) if err != nil { return err } return nil } ``` ```python from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim async def add_roles_and_permissions_to_session(session: SessionContainer): # we add the user's roles to the user's session await session.fetch_and_set_claim(UserRoleClaim) # we add the user's permissions to the user's session await session.fetch_and_set_claim(PermissionClaim) ``` ```python from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim def add_roles_and_permissions_to_session(session: SessionContainer): # we add the user's roles to the user's session session.sync_fetch_and_set_claim(UserRoleClaim) # we add the user's permissions to the user's session session.sync_fetch_and_set_claim(PermissionClaim) ``` :::info[Multi Tenancy] Whilst roles and permissions share across apps, the association of roles to users is on a per-tenant level. If using SDK functions to add a role to a user, you can also pass in a `tenantId` to the function. This tells SuperTokens to add the role for that user for that tenant. In the code examples above, the `"public"` `tenantId` appears, which is the default `tenantId` for users. You can fetch the user's `tenantId` from their current session, or from their user object (which you can fetch using their `userId`). Note that if you associate a role to a user ID for a tenant, and that user ID doesn't actually belong to that tenant, then the operation still succeeds. ::: --- ## Remove role from a user and their sessions You can remove roles from a user. The system removes the role you provide only if the user previously had that role. ```tsx import UserRoles from "supertokens-node/recipe/userroles"; import { SessionContainer } from "supertokens-node/recipe/session"; async function removeRoleFromUserAndTheirSession(session: SessionContainer) { const response = await UserRoles.removeUserRole(session.getTenantId(), session.getUserId(), "user"); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } if (response.didUserHaveRole === false) { // The user was never assigned the role } else { // We also want to update the session of this user to reflect this change. await session.fetchAndSetClaim(UserRoles.UserRoleClaim); await session.fetchAndSetClaim(UserRoles.PermissionClaim); } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/userroles" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" ) func removeRoleFromUserAndTheirSession(session sessmodels.SessionContainer) { response, err := userroles.RemoveUserRole(session.GetTenantId(), session.GetUserID(), "user", nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists return } if response.OK.DidUserHaveRole == false { // The user was never assigned the role } else { // We also want to update the session of this user to reflect this change. session.FetchAndSetClaim(userrolesclaims.UserRoleClaim) session.FetchAndSetClaim(userrolesclaims.PermissionClaim) } } ``` ```python from supertokens_python.recipe.userroles.asyncio import remove_user_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim from supertokens_python.recipe.session import SessionContainer async def remove_role_from_user_and_their_session(session: SessionContainer): res = await remove_user_role(session.get_tenant_id(), session.get_user_id(), "user") if isinstance(res, UnknownRoleError): # No such role exists return if res.did_user_have_role == False: # The user was never assigned the role pass else: # We also want to update the session of this user to reflect this change. await session.fetch_and_set_claim(UserRoleClaim) await session.fetch_and_set_claim(PermissionClaim) ``` ```python from supertokens_python.recipe.userroles.syncio import remove_user_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim from supertokens_python.recipe.session import SessionContainer def remove_role_from_user_and_their_session(session: SessionContainer): res = remove_user_role(session.get_tenant_id(), session.get_user_id(), "user") if isinstance(res, UnknownRoleError): # No such role exists return if res.did_user_have_role == False: # The user was never assigned the role pass else: # We also want to update the session of this user to reflect this change. session.sync_fetch_and_set_claim(UserRoleClaim) session.sync_fetch_and_set_claim(PermissionClaim) ``` ```bash curl --location --request POST 'http://localhost:3567/recipe/user/role/remove' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "role": "user" }' ``` :::info[Multi Tenancy] When using the multi-tenancy feature, in the previous snippets, only the user's role for the tenant they used to log in gets removed. That's the one stored in the session. You can pass in another tenant ID if you like, or call the function above for all the tenants that the user belongs to. ::: --- ## List the roles of a user ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getRolesForUser(userId: string) { const response = await UserRoles.getRolesForUser("public", userId); const roles: string[] = response.roles; } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getRolesForUser(userId string) { response, err := userroles.GetRolesForUser("public", userId, nil) if err != nil { // TODO: Handle error return } _ = response.OK.Roles } ``` ```python from supertokens_python.recipe.userroles.asyncio import get_roles_for_user async def get_roles_for_user_func(user_id: str): _ = (await get_roles_for_user("public", user_id)).roles ``` ```python from supertokens_python.recipe.userroles.syncio import get_roles_for_user def get_roles_for_user_func(user_id: str): _ = get_roles_for_user("public", user_id).roles ``` ```bash curl --location --request GET 'http://localhost:3567/recipe/user/roles?userId=fa7a0841-b533-4478-95533-0fde890c3483' \ --header 'api-key: ' ``` :::info[Multi Tenancy] In the code examples above, the `"public"` `tenantId` appears, which is the default `tenantId` for users. You can fetch the user's `tenantId` from their current session, or from their user object (which you can fetch using their `userId`). ::: --- ## List the users of a role ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getUsersThatHaveRole(role: string) { const response = await UserRoles.getUsersThatHaveRole("public", role); if (response.status === "UNKNOWN_ROLE_ERROR") { // No such role exists return; } const users: string[] = response.users; } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getUsersThatHaveRole(role string) { response, err := userroles.GetUsersThatHaveRole("public", role, nil) if err != nil { // TODO: Handle error return } if response.UnknownRoleError != nil { // No such role exists return } _ = response.OK.Users } ``` ```python from supertokens_python.recipe.userroles.asyncio import get_users_that_have_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def get_users_that_have_role_func(role: str): res = await get_users_that_have_role("public", role) if isinstance(res, UnknownRoleError): # No such role exists return _ = res.users ``` ```python from supertokens_python.recipe.userroles.syncio import get_users_that_have_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def get_users_that_have_role_func(role: str): res = get_users_that_have_role("public", role) if isinstance(res, UnknownRoleError): # No such role exists return _ = res.users ``` ```bash curl --location --request GET 'http://localhost:3567/recipe/role/users?role=user' \ --header 'api-key: ' ``` :::info[Multi Tenancy] In the code examples above, the `"public"` `tenantId` appears, which is the default `tenantId` for users. This returns the list of users that have that role in the `"public"` tenant. You can also pass in a different tenant ID, or call the function in a loop with all the tenants that exist in your app. ::: --- ## See also --- # MCP Authentication Source: https://supertokens.com/docs/authentication/ai-authentication ## Overview This guide explains how to authenticate Model Context Protocol (MCP) Servers using **SuperTokens**. For the public, read-only SuperTokens documentation MCP server, see [Build with AI Tools](/integrate-with-ai). This guide is for MCP servers that you build and host. The instructions make use of the `plugins` functionality. It is a new way to abstract common functionalities into a reusable package. ## Before you start The [MCP authentication flow](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) complies with the OAuth2 specifications. This means that you will have to use the `OAuth2` recipe in your configuration. The functionality is only available alongside the `node` SDK at the moment. Keep in mind that the feature is currently in beta and might be subject to breaking changes. ## Steps ### 1. Install the plugin Add the `supertokens-mcp-plugin` package to your project. ```bash npm i -s supertokens-mcp-plugin ``` ```bash yarn add supertokens-mcp-plugin ``` ```bash pnpm add supertokens-mcp-plugin ``` ### 2. Add the MCP server Use the `SuperTokensMcpServer` class when in your implementation. The class extends the base MCP server exposed by the `@modelcontextprotocol/sdk`, and adds custom authentication logic on top of it. You can authorize the client requests in two different ways. By using the standard [claim validators](/additional-verification/session-verification/claim-validation). Or you can write your own custom validation logic in the `validateTokenPayload` function. The authentication state can be accessed inside a tool call through the second function argument, `extra.authInfo`. ```ts import { UserRoleClaim } from "supertokens-node/recipe/userroles"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import SuperTokensMcpPlugin, { SuperTokensMcpServer } from "supertokens-mcp-plugin"; const server = new SuperTokensMcpServer({ name: "example-mcp", version: "1.0.0", path: "/mcp", validateTokenPayload: async (_accessTokenPayload, _userContext) => { // You can check the access token payload for any specific values return { status: "OK", }; }, // You can use claim validators to determine who can access the MCP server claimValidators: [UserRoleClaim.validators.includes("admin")], }); server.registerTool( "session-info", { inputSchema: {}, description: "Get session information", }, async (_args, extra) => { return { content: [ { type: "text", text: JSON.stringify(extra.authInfo), }, ], }; }, ); ``` ### 3. Update the SDK initialization code Now that you have created your server include it in the SuperTokens SDK configuration. This way, the SDK middleware will expose your new endpoint and authenticate each request. ```ts import supertokens from "supertokens-node"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import SuperTokensMcpPlugin, { SuperTokensMcpServer } from "supertokens-mcp-plugin"; // The server that you have previously created const server = new SuperTokensMcpServer({ name: "example-mcp", version: "1.0.0", path: "/mcp", }); supertokens.init({ supertokens: { connectionURI: "", apiKey: "", }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "", websiteBasePath: "", }, recipeList: [ // Include your existing recipes here // The OAuth2Provider recipe is required for the MCP authorization process OAuth2Provider.init(), ], experimental: { plugins: [ SuperTokensMcpPlugin.init({ mcpServers: [server], }), ], }, }); ``` --- # Customize the Sign In form Source: https://supertokens.com/docs/authentication/email-password/customize-the-sign-in-form ## Before you start The following instructions are only relevant if you are using the pre-built UI components. If you have created your own authentication UI, you can skip this guide. ## Modify labels and placeholders To change the labels and placeholders of the fields update the `formFields` property, in the recipe configuration. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "customFieldName", placeholder: "Custom value", }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "customFieldName", placeholder: "Custom value", }, ], }, }, }), supertokensUISession.init(), ], }); ``` --- ## Set default values Add a `getDefaultValue` option in the `formFields` configuration to pre-fill the inputs. Keep in mind that the function needs to return a string. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "Your Email", getDefaultValue: () => "john.doe@gmail.com", }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "Your Email", getDefaultValue: () => "john.doe@gmail.com", }, ], }, }, }), supertokensUISession.init(), ], }); ``` --- ## Change the optional error message When you try to submit the login form without filling in the required fields, the UI, by default, shows an error stating that the `Field is not optional`. To customize this message set the `nonOptionalErrorMsg` property to a custom string. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "Your Email", placeholder: "Email", nonOptionalErrorMsg: "Please add your email", }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "Your Email", placeholder: "Email", nonOptionalErrorMsg: "Please add your email", }, ], }, }, }), supertokensUISession.init(), ], }); ``` --- ## Custom field validators To add custom validation logic to the sign in form, update the sign up form configuration. The `email` and `password` fields validation synchronizes between the two forms. Check the [sign up form instructions](/authentication/email-password/customize-the-sign-up-form#change-the-default-email-and-password-validators) for more details. --- ## See also --- # Customize the Sign Up Form Source: https://supertokens.com/docs/authentication/email-password/customize-the-sign-up-form ## Before you start The next instructions assume that you have a working application that uses **SuperTokens** for authentication. If not, please refer to the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk) and then return here. ## Add extra fields To include more fields in the sign up form, you need to first update both the frontend and backend configuration. Then, when the sing up payload arrives on the backend, you should add a way to persist those values. ### 1. Add the new fields to the UI You first need to add the new fields to your sign up interface. Given that you are using a custom implementation, the steps vary based on your code. After you have updated the form, ensure that the submit action follows the next example. ```tsx import { signUp } from "supertokens-web-js/recipe/emailpassword"; async function signUpClicked(email: string, password: string, name: string, age: number, country: string) { let response = await signUp({ formFields: [ { id: "email", value: email, }, { id: "password", value: password, }, { id: "name", value: name, }, { id: "age", value: age + "", }, { id: "country", value: country, }, ], }); // ... rest of the code } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function signUpClicked(email: string, password: string, name: string, age: number, country: string) { let response = await supertokensEmailPassword.signUp({ formFields: [ { id: "email", value: email, }, { id: "password", value: password, }, { id: "name", value: name, }, { id: "age", value: age + "", }, { id: "country", value: country, }, ], }); // ... rest of the code } ``` ```bash curl --location --request POST '/auth/signup' --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }, { id: "name", value: "John Doe" }, { id: "age", value: 27 }, { id: "country", value: "USA" }] }' ``` ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "name", label: "Full name", placeholder: "First name and last name", }, { id: "age", label: "Your age", placeholder: "How old are you?", }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true, }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "name", label: "Full name", placeholder: "First name and last name", }, { id: "age", label: "Your age", placeholder: "How old are you?", }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true, }, ], }, }, }), supertokensUISession.init(), ], }); ``` #### Create custom components By default, the new fields use `input` elements. To enable more complex fields you can create your own custom components. :::note You may need to disable the Shadow DOM if you're integrating with a different component library that requires you to import its own CSS. For instance, some component libraries, such as [react-international-phone](https://github.com/goveo/react-international-phone), might expect you to include their CSS alongside their components. For more information, refer to [Disable use of shadow DOM](/references/frontend-sdks/prebuilt-ui/shadow-dom). ::: Set the `inputComponent` property for each field that you want to customize. :::warning[This is not applicable for non React apps. You have to create your own custom UI instead.] ::: ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "select-dropdown", label: "Select Dropdown", inputComponent: ({ value, name, onChange }) => (
), optional: true, }, { id: "terms", label: "", optional: false, nonOptionalErrorMsg: "You must accept the terms and conditions", inputComponent: ({ name, onChange }) => (
onChange(e.target.checked.toString())}> I agree to the{" "} Terms and Conditions
), }, ], }, }, }), Session.init(), ], }); ```
### 2. Include the extra fields in the backend configuration Change the **Backend SDK** initialization call to ensure that the system processes the new fields when a new user registers. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [ { id: "name", }, { id: "age", }, { id: "country", optional: true, }, ], }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { countryOptional := true supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ { ID: "name", }, { ID: "age", }, { ID: "country", Optional: &countryOptional, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword import InputFormField init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( sign_up_feature=emailpassword.InputSignUpFeature( form_fields=[InputFormField(id='name'), InputFormField(id='age'), InputFormField(id='country', optional=True)] ) ), session.init() ] ) ``` ### 3. Save the values after a successful sign up Use the `signUpPOST` API function to process the field values and persist them. :::warning **SuperTokens** does not store custom form fields. You can either save them in your database or use the [User Metadata feature ](/post-authentication/user-management/user-metadata). ::: ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { if (originalImplementation.signUpPOST === undefined) { throw Error("Should never come here"); } // First we call the original implementation of signUpPOST. let response = await originalImplementation.signUpPOST(input); // Post sign up response, we check if it was successful if (response.status === "OK") { // These are the input form fields values that the user used while signing up let formFields = input.formFields; } return response; }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // First we copy the original implementation func originalSignUpPOST := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { resp, err := originalSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if resp.OK != nil { // sign up was successful // TODO: You can now read the formFields from the input params } return resp, err } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from typing import List, Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer def override_email_password_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], ): # First we call the original implementation of sign_up_post. response = await original_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) # Post sign up response, we check if it was successful if isinstance(response, SignUpPostOkResult): pass # TODO: use the input form fields values for custom logic return response original_implementation.sign_up_post = sign_up_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) ), session.init(), ], ) ``` --- ## Customize each form field :::warning[Not applicable] This section is not relevant for custom UI, as you create your own UI and already have control over the form fields. ::: ### Modify labels and placeholders To change the labels and placeholders of the fields, update the `formFields` property, in the recipe configuration. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "customFieldName", placeholder: "Custom value", }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "customFieldName", placeholder: "Custom value", }, ], }, }, }), supertokensUISession.init(), ], }); ``` ### Set default values Add a `getDefaultValue` option to the `formFields` configuration to set default values. Keep in mind that the function needs to return a string. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "Your Email", getDefaultValue: () => "john.doe@gmail.com", }, { id: "name", label: "Full name", getDefaultValue: () => "John Doe", }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "Your Email", getDefaultValue: () => "john.doe@gmail.com", }, { id: "name", label: "Full name", getDefaultValue: () => "John Doe", }, ], }, }, }), supertokensUISession.init(), ], }); ``` ### Change the optional error message When you try to submit the login form without filling in the required fields, the UI, by default, shows an error stating that the `Field is not optional`. To customize this message set the `nonOptionalErrorMsg` property to a custom string. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "Your Email", placeholder: "Email", nonOptionalErrorMsg: "Please add your email", }, { id: "name", label: "Full name", placeholder: "Name", nonOptionalErrorMsg: "Full name is required", }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "Your Email", placeholder: "Email", nonOptionalErrorMsg: "Please add your email", }, { id: "name", label: "Full name", placeholder: "Name", nonOptionalErrorMsg: "Full name is required", }, ], }, }, }), supertokensUISession.init(), ], }); ``` ### Change the field order To customize the order of fields in your sign up form, override the `EmailPasswordSignUpForm` component. Use the next example as a reference. :::warning[This is not applicable for non React apps. You have to create your own custom UI instead.] ::: ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword"; function App() { return ( { return ( id === "name")!, props.formFields.find(({ id }) => id === "email")!, props.formFields.find(({ id }) => id === "password")!, ]} /> ); }, }} > {/* Rest of the JSX */} ); } export default App; ``` ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword"; import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui"; function App() { if (canHandleRoute([EmailPasswordPreBuiltUI])) { return ( { return ( id === "name")!, props.formFields.find(({ id }) => id === "email")!, props.formFields.find(({ id }) => id === "password")!, ]} /> ); }, }} > {getRoutingComponent([EmailPasswordPreBuiltUI])} ); } return {/* Rest of the JSX */}; } export default App; ``` --- ## Change field validators ### 1. Update the frontend configuration :::warning[Not applicable] For your custom UI, you have to implement field validation checking yourself. Note that you need to also update the backend validation to ensure a complete flow. Check the next section for more details. ::: Add a `validate` method to any of your `formFields`. The following example shows how to add age verification to the form: ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "name", label: "Full name", placeholder: "First name and last name", }, { id: "age", label: "Your age", placeholder: "How old are you?", optional: true, /* Validation method to make sure that age is above 18 */ validate: async (value) => { if (parseInt(value) > 18) { return undefined; // means that there is no error } return "You must be over 18 to register"; }, }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true, }, ], }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "name", label: "Full name", placeholder: "First name and last name", }, { id: "age", label: "Your age", placeholder: "How old are you?", optional: true, /* Validation method to make sure that age is above 18 */ validate: async (value) => { if (parseInt(value) > 18) { return undefined; // means that there is no error } return "You must be over 18 to register"; }, }, { id: "country", label: "Your country", placeholder: "Where do you live?", optional: true, }, ], }, }, }), supertokensUISession.init(), ], }); ``` ### 2. Update the backend configuration Add `validate` functions to each of the form fields, in the backend SDK initialization call. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [ { id: "name", }, { id: "age", /* Validation method to make sure that age >= 18 */ validate: async (value, tenantId) => { if (parseInt(value) >= 18) { return undefined; // means that there is no error } return "You must be over 18 to register"; }, }, { id: "country", optional: true, }, ], }, }), Session.init({}), ], }); ``` ```go import ( "strconv" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { countryOptional := true supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ { ID: "name", }, { ID: "age", Validate: func(value interface{}, tenantId string) *string { age, _ := strconv.Atoi(value.(string)) if age >= 18 { // return nil to indicate success return nil } err := "You must be over 18 to register" return &err }, }, { ID: "country", Optional: &countryOptional, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword import InputFormField from typing import Any async def validate_age(value: Any, tenant_id: str): # Validation method to make sure that age >= 18 if int(value) >= 18: return None # means that there is no error return 'You must be over 18 to register' init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( sign_up_feature=emailpassword.InputSignUpFeature( form_fields=[ InputFormField(id='name'), InputFormField(id='age', validate=validate_age), InputFormField(id='country', optional=True) ] ) ) ] ) ``` :::info[Multi-tenancy] Notice the `tenantId` argument passed into the `validate` function. Using that, you can define custom logic per tenant. For example, you can define different password policies for different tenants. ::: ### Change the default email and password validators By default, SuperTokens adds an email and a password validator to the sign up form. - The default email validator makes sure that the provided email is in the correct email format. - The default password validator makes sure that the provided password: - has a minimum of 8 characters. - contains at least one lowercase character - contains at least one number To add your own validators follow the steps described initially in this section. :::note[- The email validator that you define for **sign up** is also applied automatically to **sign in**.] - The password validator that you define for **sign up** is also applied automatically to **reset password** forms. ::: Here is an example of what you need to change. ##### 1. Update the frontend configuration :::warning[Not applicable] For your custom UI, you have to implement field validation checking yourself. Note that you need to also update the backend validation to ensure a complete flow. Check the next section for more details. ::: ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; }, }, { id: "password", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; }, }, ], }, }, }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ signInAndUpFeature: { signUpForm: { formFields: [ { id: "email", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; }, }, { id: "password", label: "...", validate: async (value) => { // Your own validation returning a string or undefined if no errors. return "..."; }, }, ], }, }, }), ], }); ``` ##### 1. Update the backend configuration ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [ { id: "email", validate: async (value, tenantId) => { // Your own validation returning a string or undefined if no errors. return "..."; }, }, { id: "password", validate: async (value, tenantId) => { // Your own validation returning a string or undefined if no errors. return "..."; }, }, ], }, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ { ID: "email", Validate: func(value interface{}, tenantId string) *string { // Your own validation returning a string or nil if no errors. return nil }, }, { ID: "password", Validate: func(value interface{}, tenantId string) *string { // Your own validation returning a string or nil if no errors. return nil }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword import InputFormField from typing import Any async def validate_password(value: Any, tenant_id: str): pass # TODO async def validate_email(value: Any, tenant_id: str): pass # TODO init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( sign_up_feature=emailpassword.InputSignUpFeature( form_fields=[ InputFormField(id='password', validate=validate_password), InputFormField(id='email', validate=validate_email) ] ) ) ] ) ``` --- ## Add terms of service and privacy policy links To add "Terms of service" and "Privacy policy" links to your sign up page add the links in the frontend SDK initialization call. Based on the provided configuration the data renders in the following way: - Provided both links: "By signing up, you agree to the [Terms of Service](#add-terms-of-service-and-privacy-policy-links) and [Privacy Policy](#add-terms-of-service-and-privacy-policy-links)" - Provided only Terms of service link: "By signing up, you agree to the [Terms of Service](#add-terms-of-service-and-privacy-policy-links)" - Provided only Privacy policy link: "By signing up, you agree to the [Privacy Policy](#add-terms-of-service-and-privacy-policy-links)" ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, termsOfServiceLink: "https://example.com/terms-of-service", privacyPolicyLink: "https://example.com/privacy-policy", recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, termsOfServiceLink: "https://example.com/terms-of-service", privacyPolicyLink: "https://example.com/privacy-policy", recipeList: [ /* ... */ ], }); ``` :::warning[Not applicable since you do not use the pre-built UI.] ::: --- ## See also --- # Disable Sign Up Source: https://supertokens.com/docs/authentication/email-password/disable-signup Learn how to disable the sign up flow for the `EmailPassword` recipe. --- ## Overview In order to prevent users from signing up directly through the frontend, you can disable the sign up flow. This can be done in two steps: - Update the **UI** to get rid of any sign up information - Change the **Backend SDK** to prevent sign up attempts ## Before you start This guide assumes that you already have configured your application to use **SuperTokens** for authentication. If you have not, please check the [Quickstart Guide](/quickstart). ## Remove the sign up UI Remove the sign up UI by overriding the `AuthPageComponentList` component and setting the `showSuperTokensAuth` prop to `false`. Remove the sign up UI by customizing the `CSS` of the authentication page. ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { AuthRecipeComponentsOverrideContextProvider } from "supertokens-auth-react/ui"; import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword"; import { ThirdpartyComponentsOverrideProvider } from "supertokens-auth-react/recipe/thirdparty"; function App() { return ( { return ; }, }} > ); } export default App; ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=authPage] [data-supertokens~=headerSubtitle] { display: none; } `, recipeList: [ /* ... */ ], }); ``` If you have a custom UI, this step will depend on your implementation. Just make sure that the user will not be able to view any sign up elements on the authentication page. ## Disable the Backend SDK sign up endpoints Override the **Backend SDK** API functions to prevent sign up attempts. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: undefined, }; }, }, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { originalImplementation.SignUpPOST = nil return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import APIInterface def apis_override(original_impl: APIInterface): original_impl.disable_sign_up_post = True return original_impl init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=apis_override ), ) ] ) ``` ## See also Import accounts using the SuperTokens API. SDK functions that can be used to manage users. UI exposed by the SuperTokens SDK that allows you to view and manage users. --- # Hooks and overrides Source: https://supertokens.com/docs/authentication/email-password/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `emailpassword` recipe to your needs. Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Sign in ### Frontend hook This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up or signing in. With this method you can fire events immediately after a successful sign in. You can use it to send analytics events. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } }, }), supertokensUISession.init(), ], }); ``` :::warning[Not applicable] This section is not applicable for custom UI since you are calling the sign in API yourself anyway. You can perform anything you want to do post sign in based on the result of the API call. ::: ### Backend override Overriding the `signIn` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data or trigger actions. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signIn: async function (input) { // First we call the original implementation of signIn. let response = await originalImplementation.signIn(input); // Post sign up response, we check if it was successful if (response.status === "OK") { /** * * response.user contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ // TODO: post sign in logic } return response; }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // create a copy of the originalImplementation func originalSignIn := *originalImplementation.SignIn // override the sign in up function (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { // First we call the original implementation of SignIn. response, err := originalSignIn(email, password, tenantId, userContext) if err != nil { return epmodels.SignInResponse{}, err } if response.OK != nil { // sign in was successful // user object contains the ID and email user := response.OK.User // TODO: Post sign in logic. fmt.Println(user) } return response, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session, emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( RecipeInterface, SignInOkResult, ) from typing import Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer def override_emailpassword_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: original_sign_in = original_implementation.sign_in async def sign_in( email: str, password: str, tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], user_context: Dict[str, Any], ): result = await original_sign_in( email, password, tenant_id, session, should_try_linking_with_session_user, user_context, ) if isinstance(result, SignInOkResult): id = result.user.id emails = result.user.emails print(id) print(emails) # TODO: post sign in logic return result original_implementation.sign_in = sign_in return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( functions=override_emailpassword_functions ), ), session.init(), ], ) ``` --- ## Sign up ### Frontend hook This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } }, }), supertokensUISession.init(), ], }); ``` :::warning[Not applicable] This section is not applicable for custom UI since you are calling the sign up API yourself anyway. You can perform anything you want to do post sign up based on the result of the API call. ::: ### Backend override Overriding the `signUp` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data, synchronize users between **SuperTokens** and your systems or to trigger other types of actions. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; // backend SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signUp: async function (input) { // First we call the original implementation of signUp. let response = await originalImplementation.signUp(input); // Post sign up response, we check if it was successful if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { /** * * response.user contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ // TODO: post sign up logic } return response; }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // create a copy of the originalImplementation func originalSignUp := *originalImplementation.SignUp // override the sign in up function (*originalImplementation.SignUp) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignUpResponse, error) { // First we call the original implementation of SignUp. response, err := originalSignUp(email, password, tenantId, userContext) if err != nil { return epmodels.SignUpResponse{}, err } if response.OK != nil { // sign up was successful // user object contains the ID and email user := response.OK.User // TODO: Post sign up logic. fmt.Println(user) } return response, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session, emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( RecipeInterface, SignUpOkResult, ) from typing import Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer def override_emailpassword_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: original_sign_up = original_implementation.sign_up async def sign_up( email: str, password: str, tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], user_context: Dict[str, Any], ): result = await original_sign_up( email, password, tenant_id, session, should_try_linking_with_session_user, user_context, ) if isinstance(result, SignUpOkResult) and len(result.user.login_methods) == 1: id = result.user.id emails = result.user.emails print(id) print(emails) # TODO: post sign up logic return result original_implementation.sign_up = sign_up return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( functions=override_emailpassword_functions ), ), session.init(), ], ) ``` --- ## Password reset ### Frontend hook This method gets fired during the password reset flow with either the `PASSWORD_RESET_SUCCESSFUL` or `RESET_PASSWORD_EMAIL_SENT` action. Use it to fire analytics events or to add any additional logic. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ onHandleEvent: async (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { // Add you custom logic here } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { // Add you custom logic here } }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ onHandleEvent: async (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { // Add you custom logic here } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { // Add you custom logic here } }, }), supertokensUISession.init(), ], }); ``` :::warning[Not applicable] This section is not applicable for custom UI since you are calling the sign in API yourself anyway. You can perform anything you want to do during the password reset flow based on the result of the API call. ::: ### Backend override Overriding the `passwordResetPOST` function allows you to introduce your own logic for the password reset process. Use it to introduce your own logic for the flow. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, passwordResetPOST: async function (input) { if (originalImplementation.passwordResetPOST === undefined) { throw Error("Should never come here"); } // First we call the original implementation let response = await originalImplementation.passwordResetPOST(input); // Then we check if it was successfully completed if (response.status === "OK") { // TODO: post password reset logic } return response; }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // first we copy the original implementation originalPasswordResetPOST := *originalImplementation.PasswordResetPOST // override the password reset API (*originalImplementation.PasswordResetPOST) = func(formFields []epmodels.TypeFormField, token string, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.ResetPasswordPOSTResponse, error) { // First we call the original implementation resp, err := originalPasswordResetPOST(formFields, token, tenantId, options, userContext) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } // Then we check if it was successfully completed if resp.OK != nil { // TODO: post password reset logic } return resp, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, PasswordResetPostOkResult, ) from supertokens_python.recipe.emailpassword.interfaces import APIOptions from supertokens_python.recipe.emailpassword.types import FormField from typing import Dict, List, Any def override_apis(original_implementation: APIInterface): original_password_reset_post = original_implementation.password_reset_post async def password_reset_post( form_fields: List[FormField], token: str, tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): response = await original_password_reset_post( form_fields, token, tenant_id, api_options, user_context ) # Then we check if it was successfully completed if isinstance(response, PasswordResetPostOkResult): pass # TODO: post password reset logic return response original_implementation.password_reset_post = password_reset_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_apis) ) ], ) ``` --- ## See also --- # Implement username login Source: https://supertokens.com/docs/authentication/email-password/implement-username-login ## Overview This tutorial shows you how to customize the recipe to add username based login with an optional email field. A few variations exist on how username-based flows can work: | Login Type | Description | Password Reset Flow | |------------|-------------|-------------------| | Username only | User signs up and signs in with username and password | Contact support required | | Username with optional email | User signs up with username and password, email is optional. Can sign in with either username or email | Uses email if provided, otherwise contact support | | Username and email required | User must provide username, email and password during sign up. Can sign in with either username or email | Uses email | This guide implements the second flow: **Username and password login with optional email**. If you are using one of the other options, you can still follow this guide and make tweaks on parts of it to achieve your desired flow. The approach is to update the `email` form field. This way it gets displayed and validated as a username. Then the optional email value gets saved against the `userID` of the user and you use it during sign in and reset password flows. You need to handle the mapping of email to `userID` and store it in your own database. The code snippets below create placeholder functions for you to implement. ## Before you start This guide assumes that you have already implemented the [EmailPassword recipe](/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). ## Steps ### 1. Modify the default email validator function Update the backend validator function to check for your username format. The function runs during **sign up**, **sign in**, and **reset password**. Hence it needs to also match an email format since the user might enter it when signing in or resetting their password. Inside **SuperTokens**, the field is still called `email`. This ensures that the username is unique and that the authentication flows works. Use the next code snippet as a reference. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [ { id: "email", validate: async (value) => { if (typeof value !== "string") { return "Please provide a string input."; } // first we check for if it's an email if ( value.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ) !== null ) { return undefined; } // since it's not an email, we check for if it's a correct username if (value.length < 3) { return "Usernames must be at least 3 characters long."; } if (!value.match(/^[a-z0-9_-]+$/)) { return "Username must contain only alphanumeric, underscore or hyphen characters."; } }, }, ], }, }), ], }); ``` ```go import ( "regexp" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ { ID: "email", Validate: func(value interface{}, tenantId string) *string { // first we check if the input is an email emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(value.(string))) if err != nil { msg := "Email is invalid" return &msg } if emailCheck { return nil } // since it's not an email, we check for if it's a correct username if len(value.(string)) < 3 { msg := "Usernames must be at least 3 characters long." return &msg } userNameCheck, err := regexp.Match(`^[a-z0-9_-]+$`, []byte(value.(string))) if err != nil || !userNameCheck { msg := "Username must contain only alphanumeric, underscore or hyphen characters." return &msg } return nil }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.types import InputFormField from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature async def validate(value: str, tenant_id: str): # first we check for if it's an email if fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value ) is not None: return None # since it's not an email, we check for if it's a correct username if len(value) < 3: return "Usernames must be at least 3 characters long." if fullmatch(r'^[a-z0-9_-]+$', value) is None: return "Username must contain only alphanumeric, underscore or hyphen characters." return None init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ InputFormField(id="email", validate=validate) ]) ) ] ) ``` ### 2. Save the user email #### 2.1 Update the sign up form validation The sign up `API` takes in the username, password, and an optional email. Add a new form field for the email, along with a `validate` function that checks the uniqueness and syntax of the input email. :::warning[Custom Implementation] To check if the email is unique you need to persist values in your own database and then check against them. ::: ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ signUpFeature: { formFields: [ { id: "email", validate: async (value) => { // ...from previous code snippet... return undefined; }, }, { id: "actualEmail", validate: async (value) => { if (value === "") { // this means that the user did not provide an email return undefined; } if ( value.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ) === null ) { return "Email is invalid"; } if ((await getUserUsingEmail(value)) !== undefined) { return "Email already in use. Please sign in, or use another email"; } }, optional: true, }, ], }, }), ], }); ``` ```go import ( "regexp" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) var emailUserMap = map[string]string{} func getUserUsingEmail(email string) (*string, error) { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation userId, ok := emailUserMap[email] if !ok { return nil, nil } return &userId, nil } func main() { actualEmailOptional := true supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ { ID: "email", Validate: func(value interface{}, tenantId string) *string { // from previous implementation... return nil }, }, { ID: "actualEmail", Validate: func(value interface{}, tenantId string) *string { if value.(string) == "" { // user did not provide an email return nil } // first we check if the input is an email emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(value.(string))) if err != nil || !emailCheck { msg := "Email is invalid" return &msg } user, err := getUserUsingEmail(value.(string)) if err != nil || user != nil { msg := "Email already in use. Please sign in, or use another email" return &msg } return nil }, Optional: &actualEmailOptional, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Dict from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.types import InputFormField from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def validate(value: str, tenant_id: str): # from previous code snippet.. return None async def validate_actual_email(value: str, tenant_id: str): if fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value ) is None: return "Email is invalid" if (await get_user_using_email(value)) is not None: return "Email already in use. Please sign in, or use another email" init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ InputFormField(id="email", validate=validate), InputFormField(id="actualEmail", validate=validate_actual_email, optional=True) ]) ) ] ) ``` #### 2.2 Save the email field value Override the sign up API to save the custom email form field. Use a mapping of `userID` to `email` to keep track of the association. Save the email value in your own database. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId; } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (original) => { return { ...original, signUpPOST: async function (input) { let response = await original.signUpPOST!(input); if (response.status === "OK") { // sign up successful let actualEmail = input.formFields.find((i) => i.id === "actualEmail")!.value as string; if (actualEmail === "") { // User did not provide an email. // This is possible since we set optional: true // in the formField config } else { await saveEmailForUser(actualEmail, response.user.id); } } return response; }, }; }, }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */ ], }, }), ], }); ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) var emailUserMap = map[string]string{} func getUserUsingEmail(email string) (*string, error) { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation userId, ok := emailUserMap[email] if !ok || userId == "" { return nil, nil } return &userId, nil } func saveEmailForUser(email string, userId string) error { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId return nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /* ... from previous code... */}, }, 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) { resp, err := ogSignUpPOST(formFields, tenantId, options, userContext) if err != nil { return epmodels.SignUpPOSTResponse{}, err } if resp.OK != nil { // sign up successful actualEmail := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } actualEmail = valueAsString } } if actualEmail == "" { // User did not provide an email. // This is possible since we set optional: true // in the formField config } else { err := saveEmailForUser(actualEmail, resp.OK.User.ID) if err != nil { return epmodels.SignUpPOSTResponse{}, err } } } return resp, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) from supertokens_python.recipe.session.interfaces import SessionContainer email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def apis_override(original: APIInterface): og_sign_up_post = original.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], ): response = await og_sign_up_post( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) if isinstance(response, SignUpPostOkResult): # sign up successful actual_email = "" for field in form_fields: if field.id == "email": actual_email = field.value if actual_email == "": # User did not provide an email. # This is possible since we set optional: true # in the form field config pass else: await save_email_for_user(actual_email, response.user.id) return response original.sign_up_post = sign_up_post return original init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( form_fields=[ # from previous code snippets... ] ), override=InputOverrideConfig(apis=apis_override), ) ], ) ``` ### 3. Allow username or email during sign in The user should be able to sign in using their email or username along with their password. In the new logic, if a user enters their email, you need to fetch the username associated with that email and then perform the authentication flow. Override the sign in recipe function to allow this. Use the next code snippet as a reference. The example use the `email` to `userId` mapping, mentioned earlier, to figure out which username to use. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId; } function isInputEmail(input: string): boolean { return ( input.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ) !== null ); } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { functions: (original) => { return { ...original, signIn: async function (input) { if (isInputEmail(input.email)) { let userId = await getUserUsingEmail(input.email); if (userId !== undefined) { let superTokensUser = await SuperTokens.getUser(userId); if (superTokensUser !== undefined) { // we find the right login method for this user // based on the user ID. let loginMethod = superTokensUser.loginMethods.find( (lM) => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword", ); if (loginMethod !== undefined) { input.email = loginMethod.email!; } } } } return original.signIn(input); }, }; }, apis: (original) => { return { ...original, // override from previous code snippet }; }, }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */ ], }, }), ], }); ``` ```go import ( "regexp" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) var emailUserMap = map[string]string{} func getUserUsingEmail(email string) (*string, error) { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation userId, ok := emailUserMap[email] if !ok || userId == "" { return nil, nil } return &userId, nil } func saveEmailForUser(email string, userId string) error { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId return nil } func isInputEmail(email string) bool { emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(email)) if err != nil || !emailCheck { return false } return true } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /*...from previous code snippet...*/}, }, Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...from previous code snippet... return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { ogSignIn := *originalImplementation.SignIn (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { if isInputEmail(email) { userId, err := getUserUsingEmail(email) if err != nil { return epmodels.SignInResponse{}, err } if userId != nil { supertokensUser, err := emailpassword.GetUserByID(*userId) if err != nil { return epmodels.SignInResponse{}, err } if supertokensUser != nil { email = supertokensUser.Email } } } return ogSignIn(email, password, tenantId, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Any, Dict, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import get_user from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) from supertokens_python.recipe.session.interfaces import SessionContainer email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def is_input_email(email: str): return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', email, ) is not None ) def recipe_override(original: RecipeInterface): og_sign_in = original.sign_in async def sign_in( email: str, password: str, tenant_id: str, session: Union[SessionContainer, None], should_try_linking_with_session_user: Union[bool, None], user_context: Dict[str, Any], ): if is_input_email(email): user_id = await get_user_using_email(email) if user_id is not None: supertokens_user = await get_user(user_id) if supertokens_user is not None: login_method = next( ( lm for lm in supertokens_user.login_methods if lm.recipe_user_id.get_as_string() == user_id and lm.recipe_id == "emailpassword" ), None, ) if login_method is not None: assert login_method.email is not None email = login_method.email return await og_sign_in( email, password, tenant_id, session, should_try_linking_with_session_user, user_context, ) original.sign_in = sign_in return original init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( form_fields=[ # from previous code snippets... ] ), override=InputOverrideConfig( # apis=..., from previous code snippet functions=recipe_override ), ) ], ) ``` ### 4. Allow username or email during password reset The password reset flow requires the user to have added an email during sign up. If there is no email associated with the user, return an appropriate message. To update the functionality you have to first change how the password reset token gets generated and then update the email sending logic. This way both methods take into account the new fields. #### 4.1 Override the token generation API The user should enter either their username or their email when starting the password reset flow. Like the sign in customization, you must check if the input is an email and, if it is, retrieve the username associated with the email. If you can't find a username from an email you have to return an appropriate message to the frontend. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import supertokensTypes from "supertokens-node/types"; let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId; } function isInputEmail(input: string): boolean { return ( input.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ) !== null ); } async function getEmailUsingUserId(userId: string) { // TODO: check your database mapping.. // this is just a placeholder implementation let emails = Object.keys(emailUserMap); for (let i = 0; i < emails.length; i++) { if (emailUserMap[emails[i]] === userId) { return emails[i]; } } return undefined; } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { functions: (original) => { return { ...original, // ...override from previous code snippet... }; }, apis: (original) => { return { ...original, // ...override from previous code snippet... generatePasswordResetTokenPOST: async function (input) { let emailOrUsername = input.formFields.find((i) => i.id === "email")!.value as string; if (isInputEmail(emailOrUsername)) { let userId = await getUserUsingEmail(emailOrUsername); if (userId !== undefined) { let superTokensUser = await SuperTokens.getUser(userId); if (superTokensUser !== undefined) { // we find the right login method for this user // based on the user ID. let loginMethod = superTokensUser.loginMethods.find( (lM) => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword", ); if (loginMethod !== undefined) { // we replace the input form field's array item // to contain the username instead of the email. input.formFields = input.formFields.filter((i) => i.id !== "email"); input.formFields = [ ...input.formFields, { id: "email", value: loginMethod.email!, }, ]; } } } } let username = input.formFields.find((i) => i.id === "email")!.value as string; let superTokensUsers: supertokensTypes.User[] = await SuperTokens.listUsersByAccountInfo(input.tenantId, { email: username, }); // from the list of users that have this email, we now find the one // that has this email with the email password login method. let targetUser = superTokensUsers.find( (u) => u.loginMethods.find((lM) => lM.hasSameEmailAs(username) && lM.recipeId === "emailpassword") !== undefined, ); if (targetUser !== undefined) { if ((await getEmailUsingUserId(targetUser.id)) === undefined) { return { status: "GENERAL_ERROR", message: "You need to add an email to your account for resetting your password. Please contact support.", }; } } return original.generatePasswordResetTokenPOST!(input); }, }; }, }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */ ], }, }), ], }); ``` ```go import ( "regexp" "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) var emailUserMap = map[string]string{} func getUserUsingEmail(email string) (*string, error) { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation userId, ok := emailUserMap[email] if !ok || userId == "" { return nil, nil } return &userId, nil } func saveEmailForUser(email string, userId string) error { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId return nil } func isInputEmail(email string) bool { emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(email)) if err != nil || !emailCheck { return false } return true } func getEmailUsingUserId(userId string) (*string, error) { for email, mappedUserId := range emailUserMap { if mappedUserId == userId { return &email, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /*...from previous code snippet...*/ }, }, Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...override sign up API from previous code snippet... ogGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { emailOrUsername := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } emailOrUsername = valueAsString } } if isInputEmail(emailOrUsername) { userId, err := getUserUsingEmail(emailOrUsername) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if userId != nil { supertokensUser, err := emailpassword.GetUserByID(*userId) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if supertokensUser != nil { // we replace the input form field's array item // to contain the username instead of the email. newFormFields := []epmodels.TypeFormField{} for _, field := range formFields { if field.ID == "email" { newFormFields = append(newFormFields, epmodels.TypeFormField{ ID: "email", Value: supertokensUser.Email, }) } else { newFormFields = append(newFormFields, field) } } formFields = newFormFields } } } username := "" for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } username = valueAsString } } supertokensUser, err := emailpassword.GetUserByEmail(tenantId, username) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if supertokensUser != nil { email, err := getEmailUsingUserId(supertokensUser.ID) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if email == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "You need to add an email to your account for resetting your password. Please contact support.", }, }, nil } } return ogGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) } return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // ...override from previous code snippet... return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Any, Dict, List from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import get_user, list_users_by_account_info from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def is_input_email(email: str): return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', email, ) is not None ) async def get_email_using_user_id(user_id: str): for email in email_user_map: if email_user_map[email] == user_id: return email return None def apis_override(original: APIInterface): og_generate_password_reset_token_post = original.generate_password_reset_token_post async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): email_or_username = "" for field in form_fields: if field.id == "email": email_or_username = field.value if is_input_email(email_or_username): user_id = await get_user_using_email(email_or_username) if user_id is not None: supertokens_user = await get_user(user_id) if supertokens_user is not None: # we find the right login method for this user # based on the user ID. login_method = next( ( lm for lm in supertokens_user.login_methods if lm.recipe_user_id == user_id and lm.recipe_id == "emailpassword" ), None, ) if login_method is not None: # we replace the input form field's array item # to contain the username instead of the email. form_fields = [ field for field in form_fields if field.id != "email" ] form_fields.append( FormField(id="email", value=login_method.email) ) username = "" for field in form_fields: if field.id == "email": username = field.value supertokens_user = await list_users_by_account_info( tenant_id, AccountInfoInput(email=username) ) target_user = next( ( u for u in supertokens_user if any( lm.email == username and lm.recipe_id == "emailpassword" for lm in u.login_methods ) ), None, ) if target_user is not None: if (await get_email_using_user_id(target_user.id)) is None: return GeneralErrorResponse( "You need to add an email to your account for resetting your password. Please contact support." ) return await og_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) original.generate_password_reset_token_post = generate_password_reset_token_post return original init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( form_fields=[ # from previous code snippets... ] ), override=InputOverrideConfig( # functions=..., from previous code snippet apis=apis_override ), ) ], ) ``` #### 4.2 Override the email sending API Update the email sending API to retrieve the user email if the user used a username in the password reset flow. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId; } function isInputEmail(input: string): boolean { return ( input.match( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ) !== null ); } async function getEmailUsingUserId(userId: string) { // TODO: check your database mapping.. // this is just a placeholder implementation let emails = Object.keys(emailUserMap); for (let i = 0; i < emails.length; i++) { if (emailUserMap[emails[i]] === userId) { return emails[i]; } } return undefined; } SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { /* ...from previous code snippets... */ }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */ ], }, emailDelivery: { override: (original) => { return { ...original, sendEmail: async function (input) { input.user.email = (await getEmailUsingUserId(input.user.id))!; return original.sendEmail(input); }, }; }, }, }), ], }); ``` ```go import ( "regexp" "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) var emailUserMap = map[string]string{} func getUserUsingEmail(email string) (*string, error) { // TODO: Check your database for if the email is associated with a user // and return that user ID if it is. // this is just a placeholder implementation userId, ok := emailUserMap[email] if !ok || userId == "" { return nil, nil } return &userId, nil } func saveEmailForUser(email string, userId string) error { // TODO: Save email and userId mapping // this is just a placeholder implementation emailUserMap[email] = userId return nil } func isInputEmail(email string) bool { emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(email)) if err != nil || !emailCheck { return false } return true } func getEmailUsingUserId(userId string) (*string, error) { for email, mappedUserId := range emailUserMap { if mappedUserId == userId { return &email, nil } } return nil, nil } func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /*...from previous code snippet...*/ }, }, Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...override from previous code snippet... return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // ...override from previous code snippet... return originalImplementation }, }, EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { email, err := getEmailUsingUserId(input.PasswordReset.User.ID) if err != nil { return err } input.PasswordReset.User.Email = *email return ogSendEmail(input, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Any, Dict from supertokens_python import InputAppInfo, init from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.types import ( EmailDeliveryOverrideInput, EmailTemplateVars, ) from supertokens_python.recipe.emailpassword.utils import ( InputOverrideConfig, InputSignUpFeature, ) email_user_map: Dict[str, str] = {} async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. # this is just a placeholder implementation if email in email_user_map: return email_user_map[email] return None async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping # this is just a placeholder implementation email_user_map[email] = user_id def is_input_email(email: str): return fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', email ) is not None async def get_email_using_user_id(user_id: str): for email in email_user_map: if email_user_map[email] == user_id: return email return None def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: actual_email = await get_email_using_user_id(template_vars.user.id) if actual_email is None: raise Exception("Should never come here") template_vars.user.email = actual_email return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ # from previous code snippets... ]), override=InputOverrideConfig( # functions=..., from previous code snippet # apis=..., from previous code snippet ), email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ) ] ) ``` ### 5. Show the new fields in the user interface :::info[The following instructions are only relevant if you are using the pre-built UI.] If you created your own custom UI on the frontend, please make sure to pass the new email `formField` when you call the sign up function. Even if the user has not given an email, you must add it with an empty string. ::: Update the pre-built UI to reflect the new flow: - Skip frontend validation for the `email` field since username or email is permissible. The backend performs those checks. - Change the "Email" label in the sign up form to say "Username". - Add an extra field in the sign up form where the user can enter their email. - Change the "Email" label in the sign in form to say "Username or email". - Change the "Email" label in the password reset form to say "Username or email". - Update translations for the email field if necessary. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, languageTranslations: { translations: { en: { EMAIL_PASSWORD_EMAIL_LABEL: "Username or email", }, }, }, recipeList: [ EmailPassword.init({ signInAndUpFeature: { signInForm: { formFields: [ { id: "email", label: "Username or email", placeholder: "Username or email", }, ], }, signUpForm: { formFields: [ { id: "email", label: "Username", placeholder: "Username", validate: async (input) => { // the backend validates this anyway. So nothing required here return undefined; }, }, { id: "actualEmail", validate: async (input) => { // the backend validates this anyway. So nothing required here return undefined; }, label: "Email", optional: true, }, ], }, }, }), // other recipes initialisation.. ], }); ``` ## See also --- # Set Up Email/Password Authentication Source: https://supertokens.com/docs/authentication/email-password/initial-setup ## Overview Inspect this repository and integrate SuperTokens into the existing application. First discover the frontend stack and backend stack, including languages, frameworks, package managers, routers, SDK versions, existing authentication code, and environment configuration. If the frontend or backend stack cannot be determined reliably, ask the user to provide it before making changes. Also ask which authentication methods and UI approach they need if those choices cannot be inferred. Use the current SuperTokens documentation and SDK APIs, preserve the project's conventions, and do not commit secrets. Configure the frontend, backend, sessions, routes, middleware, cookies, CORS, and environment variables as required. Run the relevant typechecks, tests, and build, then summarize changed files, required environment variables, and validation results. This guide walks through adding Email/Password authentication with either the SuperTokens prebuilt UI or your own custom UI. Configure the frontend first, then connect your backend and prepare the integration for production. ## Steps ### 1. Integrate the frontend SDK #### Frontend integration summary - React uses `supertokens-auth-react`; Angular and Vue use `supertokens-web-js`. - Initialize the authentication and Session recipes. React applications also wrap their component tree with `SuperTokensWrapper`. - Render the prebuilt login UI on `/auth`. - The SDK intercepts `fetch` and XHR requests to manage session tokens automatically. Web sessions use HTTP-only cookies by default, with header-based authentication available as an alternative. Start the setup by configuring your frontend application to use **SuperTokens** for authentication. This guide uses the **SuperTokens pre-built UI** components. If you want to create your own interface please check the **Custom UI** tutorial. #### 1.1 Install the SDK Run the following command in your terminal to install the package. ```bash title="Reactjs" option="package-managers:npm" npm i -s supertokens-auth-react ``` ```bash title="Reactjs" option="package-managers:yarn" yarn add supertokens-auth-react supertokens-web-js ``` ```bash title="Reactjs" option="package-managers:pnpm" pnpm add supertokens-auth-react supertokens-web-js ``` ```bash title="Reactjs" option="package-managers:bun" bun add supertokens-auth-react supertokens-web-js ``` ```bash title="Angular" option="package-managers:npm" npm i -s supertokens-web-js ``` ```bash title="Angular" option="package-managers:yarn" yarn add supertokens-web-js ``` ```bash title="Angular" option="package-managers:pnpm" pnpm add supertokens-web-js ``` ```bash title="Angular" option="package-managers:bun" bun add supertokens-web-js ``` ```bash title="Vue" option="package-managers:npm" npm i -s supertokens-web-js ``` ```bash title="Vue" option="package-managers:yarn" yarn add supertokens-web-js ``` ```bash title="Vue" option="package-managers:pnpm" pnpm add supertokens-web-js ``` ```bash title="Vue" option="package-managers:bun" bun add supertokens-web-js ``` #### 1.2 Initialize the SDK In your main application file call the `SuperTokens.init` function to initialize the SDK. The `init` call includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. After that you have to wrap the application with the `SuperTokensWrapper` component. This provides authentication context for the rest of the UI tree. Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Angular app. **Architecture** - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Angular app, so that all pages in your app can use it. - You have to create a `/auth*` route in the Angular app which renders our pre-built UI. which also needs to be initialised, but only on that route. **Creating the `/auth` route** - Use the Angular CLI to generate a new route Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Vue app **Architecture** - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Vue app, so that all pages in your app can use it. - We create a `/auth*` route in the Vue app which renders our pre-built UI which also needs to be initialised, but only on that route. **Creating the `/auth` route** - Create a new file `AuthView.vue`, this Vue component is used to render the auth component: ```tsx title="Reactjs" import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [EmailPassword.init(), Session.init()], }); /* Your App */ class App extends React.Component { render() { return {/*Your app components*/}; } } ``` ```bash title="Angular" ng generate module auth --route auth --module app.module ``` ```tsx check=false reason="This is a Vue single-file component containing both TypeScript and template markup." title="Vue" ``` - Add the following code to your `auth` angular component - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. - Initialize the `supertokens-web-js` SDK in your Vue app's `main.ts` file. This provides session management across your entire application. ```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; import { DOCUMENT } from "@angular/common"; @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@v0.48.0/build/static/js/main.81589a39.js"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit("supertokensui", { appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [supertokensUIEmailPassword.init(), supertokensUISession.init()], }); }; this.renderer.appendChild(this.document.body, script); } } ``` ```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" import { createApp } from "vue"; import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import App from "./App.vue"; import router from "./router"; SuperTokens.init({ appInfo: { appName: "", apiDomain: "", apiBasePath: "/auth", }, recipeList: [Session.init()], }); const app = createApp(App); app.use(router); app.mount("#app"); ```
- In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. - Initialize the `supertokens-web-js` SDK in your angular app's root component. This provides session management across your entire application. ```tsx title="Angular" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { appName: "", apiDomain: "", apiBasePath: "/auth", }, recipeList: [Session.init()], }); ``` #### 1.3 Configure routing In order for the **pre-built UI** to be rendered inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. Call the `getSuperTokensRoutesForReactRouterDom` method from within any `react-router-dom` `Routes` component. Add the route handling shown below to your root-level `render` function. Update your angular router so that all auth related requests load the `auth` component Update your Vue router so that all auth related requests load the `AuthView` component ```tsx title="Reactjs" option="react-router:yes" import React from "react"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx title="Reactjs" option="react-router:no" import React from "react"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([EmailPasswordPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([EmailPasswordPreBuiltUI]); } return {/*Your app*/}; } } ``` ```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ { path: "auth", loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule), }, { path: "**", loadChildren: () => import("./home/home.module").then((m) => m.HomeModule), }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ``` ```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" import { createRouter, createWebHistory } from "vue-router"; import HomeView from "../views/HomeView.vue"; import AuthView from "../views/AuthView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "home", component: HomeView, }, { path: "/auth/:pathMatch(.*)*", name: "auth", component: AuthView, }, ], }); export default router; ``` :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ::: ```tsx title="Reactjs" option="react-router:yes" import React from "react"; import { BrowserRouter, useRoutes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ ]); const routes = useRoutes([ ...authRoutes.map((route) => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` #### 1.4 Handle session tokens This part is handled automatically by the **Frontend SDK**. You don't need to do anything. The step serves more as a way for us to tell you how is this handled under the hood. After you call the `init` function, the **SDK** adds interceptors to both `fetch` and `XHR`, XMLHTTPRequest. The latter is used by the `axios` library. The interceptors save the session tokens that are generated from the authentication flow. Those tokens are then added to requests initialized by your frontend app which target the backend API. By default, the tokens are stored through session cookies but you can also switch to [header based authentication](/post-authentication/session-management/switch-between-cookies-and-header-authentication). #### 1.5 Secure application routes In order to prevent unauthorized access to certain parts of your frontend application you can use our utilities. Follow the code samples below to understand how to do this. You can wrap your components with the `` react component. This ensures that your component renders only if the user is logged in. If they are not logged in, the user is redirected to the login page. You can use the `doesSessionExist` function to check if a session exists in all your routes. You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx check=false reason="Requires surrounding quickstart application context" title="Reactjs" import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import MyDashboardComponent from "./dashboard"; class App extends React.Component { render() { return ( {/*Components that require to be protected by authentication*/} } /> ); } } ``` ```tsx title="Angular" import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx title="Vue" import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ```
#### 1.1 Install the SDK Use the following command to install the required package. :::info If you want to implement a common authentication experience for both web and mobile, please look at our [**Unified Login guide**](/authentication/unified-login/introduction). ::: Add to your `settings.gradle`: ##### Using CocoaPods Add the CocoaPods dependency to your `Podfile` Add the dependency to your pubspec.yaml ```bash title="Web" option="install-method:npm" npm i -s supertokens-web-js ``` ```bash title="Mobile" option="mobile-frameworks:reactnative" npm i -s supertokens-react-native@5.1.5 @react-native-async-storage/async-storage@2.2.0 ``` ```bash title="Mobile" option="mobile-frameworks:android" dependencyResolutionManagement { ... repositories { ... maven { url 'https://jitpack.io' } } } ``` ```bash title="Mobile" option="mobile-frameworks:ios" pod 'SuperTokensIOS', '0.4.2' ``` ```bash title="Mobile" option="mobile-frameworks:flutter" supertokens_flutter: 0.6.5 ``` Add the following to you app level's `build.gradle`: ##### Using Swift Package Manager Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. When adding the dependency, select version `0.4.2` after you enter the SuperTokens iOS repository URL: You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-flutter/releases) (ignore the `v` prefix in the releases). ```bash title="Mobile" option="mobile-frameworks:android" implementation 'com.github.supertokens:supertokens-android:0.5.3' ``` ```bash title="Mobile" option="mobile-frameworks:ios" https://github.com/supertokens/supertokens-ios ``` You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-android/releases) (ignore the `v` prefix in the releases). #### 1.2 Initialize SuperTokens Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. Add the `SuperTokens.init` function call at the start of your application. ```tsx title="Web" option="install-method:npm" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import EmailPassword from "supertokens-web-js/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, recipeList: [Session.init(), EmailPassword.init()], }); ``` ```tsx title="Mobile" option="mobile-frameworks:reactnative" import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "", apiBasePath: "/auth", }); ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "") .apiBasePath("/auth") .build() } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "", apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "", apiBasePath: "/auth", ); } ``` #### 1.3 Add the login UI The **Email/Password** flow involves two types of user interfaces. One for registering and creating new users, the *Sign Up Form*. And one for the actual authentication attempt, the *Sign In Form*. If you are provisioning users from a different method you can skip over adding the sign up form. ##### 1.3.1 Add the sign-up form For the **Sign Up** flow you have to first add the UI elements which render your form. After that, call the following function when the user submits the form that you have previously created. For the **Sign Up** flow you have to first add the UI elements which render your form. After that, call the following API when the user submits the form that you have previously created. ```tsx title="Web" option="install-method:npm" import { signUp } from "supertokens-web-js/recipe/emailpassword"; async function signUpClicked(email: string, password: string) { try { let response = await signUp({ formFields: [ { id: "email", value: email, }, { id: "password", value: password, }, ], }); if (response.status === "FIELD_ERROR") { // one of the input formFields failed validation response.formFields.forEach((formField) => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax), // or the email is not unique. window.alert(formField.error); } else if (formField.id === "password") { // Password validation failed. // Maybe it didn't match the password strength window.alert(formField.error); } }); } else if (response.status === "SIGN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign up was not allowed. window.alert(response.reason); } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage"; } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash title="Mobile" curl --location --request POST '/auth/signup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }] }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User creation was successful. The response also contains more information about the user, for example their user ID. - `status: "FIELD_ERROR"`: One of the form field inputs failed validation. The response body contains information about which form field input based on the `id`: - The email could fail validation if it's syntactically not an email, of it it's not unique. - The password could fail validation if it's not string enough (as defined by the backend password validator). Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. The `formFields` input is a key-value array. You must provide it an `email` and a `password` value at a minimum. If you want to provide additional items, for example the user's name or age, you can append it to the array like so: ```json { "formFields": [ { "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }, { "id": "name", "value": "John Doe" } ] } ``` On the backend, the `formFields` array is available to you for consumption. On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. ###### How to check if an email is unique As a part of the sign up form, you may want to explicitly check that the entered email is unique. Whilst this is already done via the sign up API call, it may be a better UX to warn the user about a non unique email right after they finish typing it. ```tsx title="Web" option="install-method:npm" import { doesEmailExist } from "supertokens-web-js/recipe/emailpassword"; async function checkEmail(email: string) { try { let response = await doesEmailExist({ email, }); if (response.doesExist) { window.alert("Email already exists. Please sign in instead"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash title="Mobile" curl --location --request GET '/auth/emailpassword/email/exists?email=john@example.com' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: The response also contains a `exists` boolean which is `true` if the input email already belongs to an email password user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. ##### 1.3.2 Add the sign-in form For the **Sign In** flow you have to first add the UI elements which render your form. After that, call the following function when the user submits the form that you have previously created. For the **Sign In** flow you have to first add the UI elements which render your form. After that, call the following API when the user submits the form that you have previously created. ```tsx title="Web" option="install-method:npm" import { signIn } from "supertokens-web-js/recipe/emailpassword"; async function signInClicked(email: string, password: string) { try { let response = await signIn({ formFields: [ { id: "email", value: email, }, { id: "password", value: password, }, ], }); if (response.status === "FIELD_ERROR") { response.formFields.forEach((formField) => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax). window.alert(formField.error); } }); } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect."); } else if (response.status === "SIGN_IN_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in was not allowed. window.alert(response.reason); } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage"; } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash title="Mobile" curl --location --request POST '/auth/signin' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }] }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in was successful. The response also contains more information about the user, for example their user ID. - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. #### 1.4 Handle session tokens You can use sessions with SuperTokens in two modes: - Using `httpOnly` cookies - Authorization bearer token. Our frontend SDK uses `httpOnly` cookie based session for websites by default as it secures against tokens theft via XSS attacks. For other platforms, like mobile apps, we use a bearer token in the `Authorization` header by default. ##### With the Frontend SDK :::success[No action required.] ::: Our frontend SDK handles everything for you. You only need to make sure that you have called `supertokens.init` before making any network requests. Our SDK adds interceptors to `fetch` and `XHR` (used by `axios`) to save and add session tokens from and to the request. By default, our web SDKs use cookies to provide credentials. Our frontend SDK handles everything for you. You only need to make sure that you have added our network interceptors as shown below :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ###### Axios ###### Using a custom Axios instance ###### HttpURLConnection ###### `URLSession` ###### Using `URLSession.shared` ###### `http` You can make requests as you normally would with `http`, the only difference is that you import the client from the SuperTokens package instead. ```tsx title="Mobile" option="mobile-frameworks:reactnative" import axios from "axios"; import SuperTokens from "supertokens-react-native"; let axiosInstance = axios.create({ /*...*/ }); SuperTokens.addAxiosInterceptors(axiosInstance); async function callAPI() { // use axios as you normally do let response = await axiosInstance.get("https://yourapi.com"); } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens import com.supertokens.session.SuperTokensHttpURLConnection import com.supertokens.session.SuperTokensPersistentCookieStore import java.net.URL import java.net.HttpURLConnection class MainApplication: Application() { override fun onCreate() { super.onCreate() // TODO: Make sure to call SuperTokens.init } fun makeRequest() { val url = URL("") val connection = SuperTokensHttpURLConnection.newRequest(url, object: SuperTokensHttpURLConnection.PreConnectCallback { override fun doAction(con: HttpURLConnection?) { // TODO: Use `con` to set request method, headers etc } }) // Handle response using connection object, for example: if (connection.responseCode == 200) { // TODO: implement } } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import Foundation import SuperTokensIOS fileprivate class NetworkManager { func setupSuperTokensInterceptor() { URLProtocol.registerClass(SuperTokensURLProtocol.self) } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/http.dart' as http; // SuperTokens wraps the package:http API. Future makeRequest() async { Uri uri = Uri.parse("http://localhost:3001/api"); var response = await http.get(uri); // handle response } ``` ###### Using the global Axios instance :::note[You must call `addAxiosInterceptors` on all `axios` imports.] ::: :::note[When making network requests you do not need to call `HttpURLConnection.connect` because SuperTokens does this for you.] ::: ###### OkHttp or Retrofit ###### Using a custom `URLSession` instance ###### Using a custom HTTP client If you use a custom HTTP client and want to use SuperTokens, you can simply provide the SDK with your client. All requests continue to use your client along with the session logic that SuperTokens provides. ```tsx title="Mobile" option="mobile-frameworks:reactnative" import axios from "axios"; import SuperTokens from "supertokens-react-native"; SuperTokens.addAxiosInterceptors(axios); async function callAPI() { // use axios as you normally do let response = await axios.get("https://yourapi.com"); } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.content.Context import com.supertokens.session.SuperTokens import com.supertokens.session.SuperTokensInterceptor import okhttp3.OkHttpClient import retrofit2.Retrofit class NetworkManager { fun getClient(context: Context): OkHttpClient { val clientBuilder = OkHttpClient.Builder() clientBuilder.addInterceptor(SuperTokensInterceptor()) // TODO: Make sure to call SuperTokens.init val client = clientBuilder.build() // REQUIRED FOR RETROFIT ONLY val instance = Retrofit.Builder() .baseUrl("") .client(client) .build() return client } fun makeRequest(context: Context) { val client = getClient(context) // Use client to make requests normally } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import Foundation import SuperTokensIOS fileprivate class NetworkManager { func setupSuperTokensInterceptor() { let configuration = URLSessionConfiguration.default configuration.protocolClasses = [SuperTokensURLProtocol.self] let session = URLSession(configuration: configuration) // Use session when making network requests } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:http/http.dart' as base_http; import 'package:supertokens_flutter/http.dart' as supertokens_http; Future makeRequest() async { Uri uri = Uri.parse("http://localhost:3001/api"); var customClient = base_http.Client(); var httpClient = supertokens_http.Client(client: customClient); var response = await httpClient.get(uri); // handle response } ``` ###### Fetch :::success[When using `fetch`, network interceptors are added automatically when you call `supertokens.init`. So no action needed here.] ::: :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ###### Alamofire ###### Dio ###### Add the SuperTokens interceptor Use the extension method provided by the SuperTokens SDK to enable interception on your `Dio` client. This allows the SuperTokens SDK to handle session tokens for you. ```swift title="Mobile" option="mobile-frameworks:ios" import Foundation import SuperTokensIOS import Alamofire fileprivate class NetworkManager { func setupSuperTokensInterceptor() { let configuration = URLSessionConfiguration.af.default configuration.protocolClasses = [SuperTokensURLProtocol.self] + (configuration.protocolClasses ?? []) let session = Session(configuration: configuration) // Use session when making network requests } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/dio.dart'; import 'package:dio/dio.dart'; void setup() { Dio dio = Dio(); // Create a Dio instance. dio.addSupertokensInterceptor(); } ``` :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ###### Making network requests You can make requests as you normally would with `dio`. ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/dio.dart'; import 'package:dio/dio.dart'; void setup() { Dio dio = Dio( // Provide your config here ); dio.addSupertokensInterceptor(); var response = dio.get("http://localhost:3001/api"); // handle response } ``` :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ##### Without the Frontend SDK :::warning[We highly recommend using our frontend SDK to handle session token management. It saves you a lot of time.] ::: In this case, you need to manually handle the tokens and session refreshing, and decide if you are going to use header or cookie-based sessions. For browsers, we recommend cookies, while for mobile apps (or if you don't want to use the built-in cookie manager) you should use header-based sessions. **Cookie** ###### During the Login Action You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "cookie". The login API returns the following headers: - `Set-Cookie`: This contains the `sAccessToken`, `sRefreshToken` cookies which are `httpOnly` and are automatically managed by the browser. For mobile apps, you need to setup cookie handling yourself, use our SDK or use a header based authentication mode. - `front-token` header: This contains information about the access token: - The userID - The expiry time of the access token - The payload added by you in the access token. Here is the structure of the token: ```tsx let frontTokenFromRequestHeader = "..."; let frontTokenDecoded = JSON.parse(decodeURIComponent(escape(atob(frontTokenFromRequestHeader)))); console.log(frontTokenDecoded); /* { ate: 1665226412455, // time in milliseconds for when the access token expires, and then a refresh is required uid: "....", // user ID up: { sub: "..", iat: .., ... // other access token payload } } */ ``` This token is mainly used for cookie-based authentication because you don't have access to the actual access token on the frontend. You may still want to read its payload, for example to adjust the UI based on the user's role. The token is not signed and must not be used for authorization. If you cache it, treat its contents as untrusted and clear it when the session ends. - `anti-csrf` header (optional): By default it's not required, so it's not sent. But if this is sent, you should save this token as well for use when making requests. ###### When You Make Network Requests to Protected APIs The `sAccessToken` gets attached to the request automatically by the browser. Other than that, you need to add the following headers to the request: - `rid: "anti-csrf"` - this prevents against anti-CSRF requests. If your `apiDomain` and `websiteDomain` values are exactly the same, then this is not necessary. - `anti-csrf` header (optional): If this was provided to you during login, then you need to add that token as the value of this header. - For cross-origin browser requests, set the Fetch `credentials` request option to `"include"` (or the equivalent option in your HTTP library). `credentials` is not an HTTP header and does not accept `true` in Fetch. An API call can potentially update the `sAccessToken` and `front-token` tokens, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for: - `sAccessToken`: This is as a new `Set-Cookie` header and is managed by the browser automatically. - `front-token`: This should be read and saved by you in the same way as it's being done during login. ###### Handling session refreshing If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. You can call the refresh API as follows: ```bash curl --location --request POST '/auth/session/refresh' \ --header 'Cookie: sRefreshToken=...' ``` :::note[You may also need to add the `anti-csrf` header to the request if that was provided to you during sign in.] - The cURL command above shows the `sRefreshToken` cookie as well, but this is added by the web browser automatically, so you don't need to add it explicitly. ::: The result of a session refresh is either: - Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. - Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `front-token` that you saved on the frontend earlier. **Header (Authorization Bearer)** ###### During the Login Action You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "header". The login API returns the following headers: - `st-access-token`: This contains the current access token associated with the session. - `st-refresh-token`: This contains the current refresh token associated with the session. Do not persist these tokens in browser `localStorage`, because injected scripts can read them. Prefer the Web SDK's cookie-based mode for browsers. Native applications should use platform-provided secure storage. If you manually use header-based authentication in a browser, keep tokens in memory and account for the session ending when the page reloads. ###### When You Make Network Requests to Protected APIs You need to add the following headers to request: - `authorization: Bearer {access-token}` - Header-based requests do not require the Fetch API's `credentials` option unless the request also relies on cookies or HTTP authentication. An API call can potentially update the `access-token`, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for `st-access-token` These should be read and saved by you in the same way as it's being done during login. ###### Handling session refreshing If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. You can call the refresh API as follows: ```bash curl --location --request POST '/auth/session/refresh' \ --header 'authorization: Bearer {refresh-token}' ``` The result of a session refresh is either: - Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. - Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `st-refresh-token` and `st-access-token` that you saved on the frontend earlier. #### 1.5 Protect frontend routes You can use the `doesSessionExist` function to check if a session exists. ```tsx title="Web" option="install-method:npm" import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx title="Mobile" option="mobile-frameworks:reactnative" import SuperTokens from "supertokens-react-native"; async function doesSessionExist() { if (await SuperTokens.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun doesSessionExist() { if (SuperTokens.doesSessionExist(this.applicationContext)) { // user is logged in } else { // user has not logged in yet } } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func doesSessionExist() { if SuperTokens.doesSessionExist() { // User is logged in } else { // User is not logged in } } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/supertokens.dart'; Future doesSessionExist() async { return await SuperTokens.doesSessionExist(); } ``` #### 1.6 Add a sign-out action The `signOut` method revokes the session on the frontend and on the backend. Calling this function without a valid session also yields a successful response. ```tsx title="Web" option="install-method:npm" import Session from "supertokens-web-js/recipe/session"; async function logout() { await Session.signOut(); window.location.href = "/auth"; // or to wherever your logic page is } ``` ```tsx title="Mobile" option="mobile-frameworks:reactnative" import SuperTokens from "supertokens-react-native"; async function logout() { await SuperTokens.signOut(); // navigate to the login screen.. } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun logout() { SuperTokens.signOut(this); // navigate to the login screen.. } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func signOut() { SuperTokens.signOut(completionHandler: { error in if error != nil { // handle error } else { // Signed out successfully } }) } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/supertokens.dart'; Future signOut() async { await SuperTokens.signOut( completionHandler: (error) { // handle error if any } ); } ``` - On success, the `signOut` function does not redirect the user to another page, so you must redirect the user yourself. - The `signOut` function calls the sign out API exposed by the session recipe on the backend. - If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, our SDKs do an automatic session refresh before revoking the session. ### 2. Integrate the backend SDK Let's go through the changes required so that your backend can expose the **SuperTokens** authentication features. #### 2.1 Install the backend SDK Run the following command in your terminal to install the package. ```bash title="Node.js" option="package-managers:npm" npm i -s supertokens-node ``` ```bash title="Node.js" option="package-managers:yarn" yarn add supertokens-node ``` ```bash title="Node.js" option="package-managers:pnpm" pnpm add supertokens-node ``` ```bash title="Node.js" option="package-managers:bun" bun add supertokens-node ``` ```bash title="Go" go get github.com/supertokens/supertokens-golang ``` ```bash title="Python" pip install supertokens-python ``` :::info[Official backend SDKs are available for **Node.js**, **Python**, and **Go**.] For other languages, create a separate authentication service. Our [other frameworks guide](/references/backend-sdks/other-frameworks) explains this approach. ::: #### 2.2 Initialize the backend SDK You will have to initialize the **Backend SDK** alongside the code that starts your server. The init call will include [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app, how the backend will connect to the **SuperTokens Core**, as well as the **Recipes** that will be used in your setup. ```tsx title="Node.js" option="node-frameworks:express" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:hapi" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "hapi", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:fastify" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "fastify", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:koa" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "koa", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:loopback" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "loopback", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```go title="Go" import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. ConnectionURI: "https://try.supertokens.io", // APIKey: }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ emailpassword.Init(nil), session.Init(nil), }, }) if err != nil { panic(err.Error()) } } ``` ```python title="Python" option="python-frameworks:fastapi" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features emailpassword.init() ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```python title="Python" option="python-frameworks:flask" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='flask', recipe_list=[ session.init(), # initializes session features emailpassword.init() ] ) ``` ```python title="Python" option="python-frameworks:django" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='django', recipe_list=[ session.init(), # initializes session features emailpassword.init() ], mode='asgi' # use wsgi if you are running django server in sync mode ) ``` :::info[Multiple frontend domains] To handle clients from different domains with the same SuperTokens instance, use the `origin` property in the `appInfo` object instead of `websiteDomain`. The property accepts a function that receives the original request as an input and should return a valid domain. Make sure to whitelist all the domains during CORS configuration. Keep in mind that with this setup, each frontend application will not share authentication sessions. Users will have to authenticate separately for each domain. To configure a shared authentication experience between multiple services check the [Unified Login](/authentication/unified-login/introduction) documentation. ::: #### 2.3 Add the SuperTokens APIs and configure CORS Now that the SDK is initialized you need to expose the endpoints that will be used by the frontend SDKs. Besides this, your server's CORS, Cross-Origin Resource Sharing, settings should be updated to allow the use of the authentication headers required by **SuperTokens**. Register the `plugin`. Register the `plugin`. Also register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody) plugin. :::note[Add the `middleware` BEFORE all your routes.] ::: :::note[Add the `middleware` BEFORE all your routes.] ::: Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. Use the `Middleware` (**BEFORE all your routes**) and the `get_all_cors_headers()` functions as shown below. - Use the `Middleware` (**BEFORE all your routes and after calling init function**) and the `get_all_cors_headers()` functions as shown below. - Add a route to catch all paths and return a 404. This is needed because if we don't add this, then OPTIONS request for the APIs exposed by the `Middleware` will return a `404`. Use the `Middleware` and the `get_all_cors_headers()` functions as shown below in your `settings.py`. ```tsx title="Node.js" option="node-frameworks:express" import express from "express"; import cors from "cors"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/express"; let app = express(); app.use( cors({ origin: "", allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }), ); // IMPORTANT: CORS should be before the below line. app.use(middleware()); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:hapi" import Hapi from "@hapi/hapi"; import supertokens from "supertokens-node"; import { plugin } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000, routes: { cors: { origin: [""], additionalHeaders: [...supertokens.getAllCORSHeaders()], credentials: true, }, }, }); (async () => { await server.register(plugin); await server.start(); })(); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:fastify" import cors from "@fastify/cors"; import supertokens from "supertokens-node"; import { plugin } from "supertokens-node/framework/fastify"; import formDataPlugin from "@fastify/formbody"; import fastifyImport from "fastify"; let fastify = fastifyImport(); // ...other middlewares fastify.register(cors, { origin: "", allowedHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()], credentials: true, }); (async () => { await fastify.register(formDataPlugin); await fastify.register(plugin); await fastify.listen({ port: 8000 }); })(); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:koa" import Koa from "koa"; import cors from "@koa/cors"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/koa"; let app = new Koa(); app.use( cors({ origin: "", allowHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }), ); app.use(middleware()); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:loopback" import { RestApplication } from "@loopback/rest"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/loopback"; let app = new RestApplication({ rest: { cors: { origin: "", allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }, }, }); app.middleware(middleware); // ...your API routes ``` ```go title="Go" option="go-frameworks:http" import ( "net/http" "strings" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... http.ListenAndServe("SERVER ADDRESS", corsMiddleware( supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // TODO: Handle your APIs.. })))) } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) { response.Header().Set("Access-Control-Allow-Origin", "") response.Header().Set("Access-Control-Allow-Credentials", "true") if r.Method == "OPTIONS" { // we add content-type + other headers used by SuperTokens response.Header().Set("Access-Control-Allow-Headers", strings.Join(append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...), ",")) response.Header().Set("Access-Control-Allow-Methods", "*") response.Write([]byte("")) } else { next.ServeHTTP(response, r) } }) } ``` ```go title="Go" option="go-frameworks:gin" import ( "net/http" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... router := gin.New() // CORS router.Use(cors.New(cors.Config{ AllowOrigins: []string{""}, AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "OPTIONS"}, AllowHeaders: append([]string{"content-type"}, supertokens.GetAllCORSHeaders()...), AllowCredentials: true, })) // Adding the SuperTokens middleware router.Use(func(c *gin.Context) { supertokens.Middleware(http.HandlerFunc( func(rw http.ResponseWriter, r *http.Request) { c.Next() })).ServeHTTP(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() }) // Add APIs and start server } ``` ```go title="Go" option="go-frameworks:chi" import ( "github.com/go-chi/chi" "github.com/go-chi/cors" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... r := chi.NewRouter() // CORS r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{""}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...), AllowCredentials: true, })) // SuperTokens Middleware r.Use(supertokens.Middleware) // Add APIs and start server } ``` ```go title="Go" option="go-frameworks:mux" import ( "net/http" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... // TODO: Add APIs router := mux.NewRouter() // Adding handlers.CORS(options)(supertokens.Middleware(router))) http.ListenAndServe("SERVER ADDRESS", handlers.CORS( handlers.AllowedHeaders(append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...)), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), handlers.AllowedOrigins([]string{""}), handlers.AllowCredentials(), )(supertokens.Middleware(router))) } ``` ```python title="Python" option="python-frameworks:fastapi" from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from supertokens_python import get_all_cors_headers from supertokens_python.framework.fastapi import get_middleware app = FastAPI() app.add_middleware(get_middleware()) # TODO: Add APIs app.add_middleware( CORSMiddleware, allow_origins=[ "" ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) # TODO: start server ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" from supertokens_python import get_all_cors_headers from flask import Flask, abort from flask_cors import CORS from supertokens_python.framework.flask import Middleware app = Flask(__name__) Middleware(app) # TODO: Add APIs CORS( app=app, origins=[ "" ], supports_credentials=True, allow_headers=["Content-Type"] + get_all_cors_headers(), ) # This is required since if this is not there, then OPTIONS requests for # the APIs exposed by the supertokens' Middleware will return a 404 @app.route('/', defaults={'u_path': ''}) @app.route('/') def catch_all(u_path: str): abort(404) # TODO: start server ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" from typing import List from corsheaders.defaults import default_headers from supertokens_python import get_all_cors_headers CORS_ORIGIN_WHITELIST = [ "" ] CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ "" ] CORS_ALLOW_HEADERS: List[str] = list(default_headers) + [ "Content-Type" ] + get_all_cors_headers() INSTALLED_APPS = [ 'corsheaders', 'supertokens_python' ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ..., 'supertokens_python.framework.django.django_middleware.middleware', ] # TODO: start server ``` You can review all the endpoints that are added through the use of **SuperTokens** by visiting the [API Specs](https://app.swaggerhub.com/apis/supertokens/FDI). #### 2.4 Add the SuperTokens error handler Depending on the language and framework that you are using, you might need to add a custom error handler to your server. The handler will catch all the authentication related errors and return proper HTTP responses that can be parsed by the frontend SDKs. No additional `errorHandler` is required. Add the `errorHandler` **Before all your routes and plugin registration** No additional `errorHandler` is required. No additional `errorHandler` is required. :::info[You can skip this step] ::: :::info[You can skip this step] ::: ```tsx title="Node.js" option="node-frameworks:express" import express, { Request, Response, NextFunction } from "express"; import { errorHandler } from "supertokens-node/framework/express"; let app = express(); // ...your API routes // Add this AFTER all your routes app.use(errorHandler()); // your own error handler app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { /* ... */ }); ``` ```tsx title="Node.js" option="node-frameworks:fastify" import Fastify from "fastify"; import { errorHandler } from "supertokens-node/framework/fastify"; let fastify = Fastify(); fastify.setErrorHandler(errorHandler()); // ...your API routes ``` #### 2.5 Secure application routes Now that your server can authenticate users, the final step that you need to take care of is to prevent unauthorized access to certain parts of the application. For your APIs that require a user to be logged in, use the `verifySession` middleware. For your APIs that require a user to be logged in, use the `VerifySession` middleware. For your APIs that require a user to be logged in, use the `verify_session` middleware. ```tsx title="Node.js" option="node-frameworks:express" 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(), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }); ``` ```tsx title="Node.js" option="node-frameworks:hapi" 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(), }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //... }, }); ``` ```tsx title="Node.js" option="node-frameworks:fastify" 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(), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }, ); ``` ```tsx title="Node.js" option="node-frameworks:koa" 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(), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //.... }); ``` ```tsx title="Node.js" option="node-frameworks:loopback" 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()) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //.... } } ``` ```go title="Go" option="go-frameworks:http" import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) }) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go title="Go" option="go-frameworks:gin" import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(nil), likeCommentAPI) } // This is a function that wraps the supertokens verification function // to work the 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 likeCommentAPI(c *gin.Context) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go title="Go" option="go-frameworks:chi" import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go title="Go" option="go-frameworks:mux" import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:fastapi" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends(verify_session())): user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/update-jwt', methods=['POST']) @verify_session() def like_comment(): session: SessionContainer = g.supertokens user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def like_comment(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() print(user_id) ``` The middleware function returns a `401` to the frontend if a session doesn't exist, or if the access token has expired, in which case, our frontend SDK automatically refreshes the session. In case of successful session verification, you get access to a `session` object using which you can get the user's ID, or manipulate the session information. ### 3. Configure the Core Service If you have signed up and deployed a SuperTokens environment already, you can skip this step. Otherwise, please follow these instructions to use the correct **SuperTokens Core** instance in your application. The steps show you how to connect to a **SuperTokens Managed Service Environment**. If you want to self host the core instance please check the [following guide](/deployment/self-host-supertokens). #### 3.1 Sign up for a SuperTokens account Open this [page](https://supertokens.com/auth) in order to access the account creation page. Select the account that you want to use and wait for the action to complete. #### 3.2 Create a deployment After signing in, open the SuperTokens dashboard and select **Managed**. Enter a name for the deployment, select the region closest to your backend services, and click **Deploy Core**. Our internal service will deploy a separate environment based on your selection. After this process is complete, open the new deployment from the list. :::info[The initial setup flow only configures a development environment.] In order to use SuperTokens in production, you will have to create a separate deployment. ::: #### 3.3 Connect the backend SDK with SuperTokens In the SuperTokens dashboard, open the newly created deployment and select **Overview**. In **Connection Information**, copy the **Connection URI** and one of the **API Keys**, then use them as `connectionURI` and `apiKey` in your backend SDK configuration. If no suitable key exists, click **Generate Key** to create one. ```tsx title="Node.js" import supertokens from "supertokens-node"; supertokens.init({ supertokens: { connectionURI: "", apiKey: "", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [], }); ``` ```go title="Go" import "github.com/supertokens/supertokens-golang/supertokens" func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "", APIKey: "", }, }) } ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" from supertokens_python import init, InputAppInfo, SupertokensConfig init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='', api_key='' ), framework='...', recipe_list=[ #... ] ) ``` ## Next steps Review this repository's SuperTokens integration for production readiness. Inspect Core deployment configuration, API keys, environment separation, HTTPS, secret handling, session security, CORS, cookies, email or SMS delivery, rate limits, logging, and error handling. Check that frontend and backend recipes match and that protected routes are actually protected. Run the relevant tests, typechecks, and build. Report findings by severity with file references, then make only safe fixes that are clearly required. Now that you have completed the initial setup, continue configuring SuperTokens for your application's authentication and authorization requirements. Add passwordless, social, enterprise, or machine-to-machine authentication. Verify user email addresses during sign-up. Add more authentication factors to your sign-in process. Configure session security, storage, and advanced workflows. Manage users through the SuperTokens Dashboard. Run SuperTokens as a managed service or inside your infrastructure. --- # Email/Password Authentication Source: https://supertokens.com/docs/authentication/email-password/introduction ## Email/password integration summary - Configure the EmailPassword and Session recipes on both the frontend and backend. - Choose the prebuilt UI or a custom UI and configure the authentication routes. - Configure password reset and decide whether your application requires email verification. - Validate sign-up, sign-in, sign-out, session refresh, and password reset before deployment. Add SuperTokens email/password authentication to this existing application. First inspect the project stack and current SuperTokens configuration. Configure the frontend and backend EmailPassword and Session recipes, the chosen pre-built or custom UI, auth routes, password reset flow, and environment variables. Ask whether email verification is required if it is not clear. Preserve existing conventions, do not commit secrets, and validate the sign-up, sign-in, sign-out, session refresh, and password reset flows with the relevant tests and build. ## Overview The **Email/Password** `recipe` provides a way of authenticating users with basic credentials. You can use it out of the box, with the **Pre-Built UI**, or implement your own interface through the available SDKs. Sign in form UI for email password login ## Getting started You can either follow the quickstart tutorial or use the `CLI` tool to generate an example app that shows you how the recipe works. Go through a quick tutorial that shows you how to add the **Email/Password** recipe to your app. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Adapt the look and feel of the pre-built sign in form. Adapt the look and feel of the pre-built sign up form. Add custom logic after the user logs in or signs up. Read which hashing algorithms you can use and how to use them. Discover how you can implement an authentication flow that makes use of usernames instead of email addresses. --- # Password hashing Source: https://supertokens.com/docs/authentication/email-password/password-hashing ## Overview **SuperTokens** supports two password hashing algorithms: `BCrypt` and `Argon2`. Per current best practices, `Argon2` is the recommended algorithm. However, **SuperTokens** uses `BCrypt` by default since `Argon2` requires custom settings that are specific to the hardware in which the core is running on. ### Hashing time The key metric to aim for when hashing passwords is the amount of time each hash would take. By default, **SuperTokens** has configured these algorithms to take 300 milliseconds per hash on a machine with 1 GB of `RAM` and 2 virtual `CPU` cores. ## Change the hashing algorithm You can switch algorithms whenever you want. The change affects only the new users that sign up. Previous passwords undergo decryption using the original algorithm. For example, if you hash a password with `BCrypt`, it verifies using `BCrypt` even if the core configuration changes to `Argon2`. Instructions: 1. Go to the [SuperTokens SaaS dashboard](https://supertokens.com/dashboard) and select the relevant **Managed** deployment. 2. Open **Configuration** and find the **Password Hashing Algorithm** setting. 3. Change the algorithm. Configuration changes are saved automatically. ```bash docker run \ -p 3567:3567 \ -e PASSWORD_HASHING_ALG=BCRYPT \ -e BCRYPT_LOG_ROUNDS=11 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command password_hashing_alg: BCRYPT bcrypt_log_rounds: 11 ``` ## Hashing calibration This information is relevant only for self hosted core instances. The managed service instances have already calibrated the algorithms based on the hardware. :::note[When you change the hashing settings make sure to run the calibration CLI command to find the right balance for your hardware.] ::: #### Algorithm settings | Name | Default | Description | | --- | --- | --- | | `password_hashing_alg` | - | This setting chooses which password hashing algorithm to use. For using Argon2, set this to `ARGON2`. | | `argon2_iterations` | `1` | This controls how much `CPU` processing power the hashing process uses. The higher the value, the more processing power, and hence the more time each hash takes. | | `argon2_memory_kb` | `87795` (85 MB) | The amount of memory (`RAM`) that each hash takes. The higher this is, the harder it becomes to crack hashes offline, and the longer the algorithm takes. | | `argon2_parallelism` | `2` | This is the number of threads the algorithm uses during hashing. The higher this is, the harder it would be to crack passwords offline using multiple cores. Should be equal to the number of virtual cores (or twice the number of physical cores) available in the system. | | `argon2_hashing_pool_size` | `1` | This is the maximum number of concurrent hashes that the core performs. A value of `1` means that the core does only one hash at one point in time, other requests for hashing queue up and wait for their turn. | ##### Example If each hash takes 300 milliseconds, a value of `1` here would entail ~ a max of 3 hashes per second (1000 ms / 300 ms). A value of `2` here would entail a max of 6 hashes per second (1000 ms / 300 ms)*2. Password hashing occurs during sign in, sign up, and password reset flows. Therefore, you can set this value according to the target time per hash and how many sign ups/in you expect per second. #### Change the settings #### Algorithm settings | Name | Default | Description | |------|--------------|-------------| | `password_hashing_alg` | - | This setting chooses which password hashing algorithm to use. For using bcrypt, set this to `BCRYPT`. | | `bcrypt_log_rounds` | `11` | The number of rounds to use for hashing is `2^bcrypt_log_rounds`. The higher this value, the more time hashing takes. | #### Change settings ```bash docker run \ -p 3567:3567 \ -e PASSWORD_HASHING_ALG=ARGON2 \ -e ARGON2_ITERATIONS=1 \ -e ARGON2_MEMORY_KB=87795 \ -e ARGON2_PARALLELISM=2 \ -e ARGON2_HASHING_POOL_SIZE=1 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command password_hashing_alg: ARGON2 argon2_iterations: 1 argon2_memory_kb: 87795 argon2_parallelism: 2 argon2_hashing_pool_size: 1 ``` ```bash docker run \ -p 3567:3567 \ -e PASSWORD_HASHING_ALG=BCRYPT \ -e BCRYPT_LOG_ROUNDS=11 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command password_hashing_alg: BCRYPT bcrypt_log_rounds: 11 ``` #### Calibrate to your hardware To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: - `--with_alg`: - The value of this should be `argon2` - Compulsory parameter - `--with_time_per_hash_ms`: - This requires the target time per hash (in milliseconds). - The default value is `300`. - `--with_argon2_hashing_pool_size`: - This affects how much memory the hashing process uses per hash. - The default value is `1` - `--with_argon2_max_memory_mb`: - This is the maximum amount of memory (`RAM`) that the core should use for password hashing. The amount of memory per password hash is `with_argon2_max_memory_mb / with_argon2_hashing_pool_size`. - The default value is `1024`. - `--with_argon2_parallelism`: - This is the number of threads that argon2 should use. The higher this is, the harder it becomes to crack passwords offline. - The default value is `2*number of cores` Running the algorithm takes minutes. #### Calibrate to your hardware To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: - `--with_alg`: - The value of this should be `bcrypt`. - Compulsory parameter - `--with_time_per_hash_ms`: - This requires the target time per hash (in milliseconds). - The default value is `300` ```bash docker run supertokens/supertokens- supertokens hashingCalibrate --with_alg=argon2 ``` ```bash supertokens hashingCalibrate --with_alg=argon2 ``` ```bash docker run supertokens/supertokens- supertokens hashingCalibrate --with_alg=bcrypt ``` ```bash supertokens hashingCalibrate --with_alg=bcrypt ``` The above produces an output like: ```text ====Input Settings==== -> Target time per hash (--with_time_per_hash_ms): 300 MS -> Number of max concurrent hashes (--with_argon2_hashing_pool_size): 1 -> Max amount of memory to consume across 1 concurrent hashes (--with_argon2_max_memory_mb): 1024 MB -> Argon2 parallelism (--with_argon2_parallelism): 4 ====Running algorithm==== Current argon2 settings -> memory: 1024 MB -> iterations: 1 Calculating average hashing time.... ..................................................Took 574 MS per hash Adjusting memory to reach target time. Current argon2 settings -> memory: 972 MB -> iterations: 1 Calculating average hashing time.... ..................................................Took 529 MS per hash Adjusting memory to reach target time. Current argon2 settings -> memory: 924 MB -> iterations: 1 Calculating average hashing time.... ..................................................Took 494 MS per hash <....Truncated....> Adjusting memory to reach target time. Current argon2 settings -> memory: 367 MB -> iterations: 2 Calculating average hashing time.... ..................................................Took 319 MS per hash Adjusting memory to reach target time. Current argon2 settings -> memory: 348 MB -> iterations: 2 Calculating average hashing time.... ..................................................Took 303 MS per hash ====Final values==== Average time per hash is: 303 MS argon2_memory_kb: 357104 (348 MB) argon2_iterations: 2 argon2_parallelism: 4 argon2_hashing_pool_size: 1 ==================== You should use these as docker environment variables or put them in the config.yaml file in the SuperTokens installation directory. ``` ```text ====Input Settings==== -> Target time per hash (--with_time_per_hash_ms): 300 MS ====Running algorithm==== Current log rounds: 11 ..........Took 158 MS per hash Incrementing log rounds and trying again... Current log rounds: 12 ..........Took 310 MS per hash ====Final values==== Average time per hash is: 310 MS bcrypt_log_rounds: 12 ==================== You should use this as a docker environment variable or put this in the config.yaml file in the SuperTokens installation directory. ``` The contents of the `====Final values====` gives you the values of the parameters to provide to the core. The algorithm starts with the highest amount of memory per hash (= `with_argon2_max_memory_mb/with_argon2_hashing_pool_size`) and `1` iteration. It calculates the current average hashing time by simulating hashes concurrently (based on the value of `with_argon2_hashing_pool_size`). If the hashing time is greater than the target time, it reduces the memory by 5%. If it's less than the target time, it increases the number of iterations. The algorithm stops if the current time is within 10 milliseconds (higher or lower) of the target time. :::info[debug] If you see an output like: The contents of the `====Final values====` gives you the values of the parameters to provide to the core. The algorithm starts with the minimum recommended value (`11`), and increments it until the average time per hash is greater than the target time. The final value is then equal to the value that yields the closest time per hash as the target one. ```bash /usr/bin/supertokens: line 9: 15 Killed "${ST_INSTALL_LOC}"jre/bin/java -classpath "${ST_INSTALL_LOC}cli/*" io.supertokens.cli.Main false "${ST_INSTALL_LOC}" $@ ``` it means that the system doesn't have enough memory. Try to run the algorithm again with a lower memory value by passing `--with_argon2_max_memory_mb` ::: ## See also --- # Password managers Source: https://supertokens.com/docs/authentication/email-password/password-managers ## Overview Styling encapsulation relies on the ["shadow DOM" browser feature](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). Password managers such as Dashlane, LastPass, or OnePassword do not detect authentication forms fields inside shadow DOMs. Therefore, if you would like to make sure that your end users can use their password managers, you have to disable shadow DOM. :::info[These instructions are only relevant if you are using the pre-built UI components.] ::: ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, useShadowDom: false, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, useShadowDom: false, recipeList: [ /* ... */ ], }); ``` :::warning[- SuperTokens uses a special attribute to define its styling. Disabling shadow DOM should not impact the rest of your application's styles. Verify that your CSS does not impact how SuperTokens UI appears when disabling Shadow DOM.] - Shadow DOM is always disabled with Internet Explorer since it does not support it. Similarly, if you intend to support Internet Explorer for your application make sure to verify how SuperTokens UI appears. ::: ## See also --- # Password reset Source: https://supertokens.com/docs/authentication/email-password/password-reset ## Overview The password reset feature consists of two actions: one in which a user requests a reset password link over email and another where the user sets the new password. ### The password reset forms The following images show how the password reset forms render when you are using the pre-built UI. **Reset Password** You see this if you navigate to `/auth/reset-password`.
UI to send password reset email
**Change Password** You see this if you navigate to `/auth/reset-password?token=TOKEN`.
UI to change password
To implement your own interface create two different forms: - One where the user requests a password reset link. - Another one where the user changes their password. Use the pre-built UI components as a reference. ### The password reset email This is how the email that gets delivered to the learner looks like: Email UI for password reset email You can find the [source code of this template on GitHub](https://github.com/supertokens/email-sms-templates/blob/master/email-html/password-reset.html). To customize the template check the [email delivery](/platform-configuration/email-delivery) section for more information. --- ## Embed the reset form in a page To embed the reset form in a page you can use the next steps. ### 1. Disable the default implementation ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ resetPasswordUsingTokenFeature: { disableDefaultUI: true, }, }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ resetPasswordUsingTokenFeature: { disableDefaultUI: true, }, }), ], }); ``` If you navigate to `/auth/reset-password`, you should not see the widget anymore. ### 2. Render the component yourself Add the `ResetPasswordUsingToken` component in your app: :::warning[You have to build your own UI for this.] ::: ```tsx import React from "react"; import { ResetPasswordUsingToken } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; class ResetPasswordPage extends React.Component { render() { return (
); } } ```
:::warning[Not applicable since you do not use pre-built UI.] ::: ### 3. Change the website path for reset password UI This step is optional. The default path for this is component is `/auth/reset-password`. If you are displaying this at some custom path, then you need to add additional configuration on the backend and frontend: #### 3.1 On the backend ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { if (input.type === "PASSWORD_RESET") { return originalImplementation.sendEmail({ ...input, passwordResetLink: input.passwordResetLink.replace( // This is: `/auth/reset-password` "http://localhost:3000/auth/reset-password", "http://localhost:3000/your/path", ), }); } return originalImplementation.sendEmail(input); }, }; }, }, }), ], }); ``` ```go import ( "strings" "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // This is: `/auth/reset-password` input.PasswordReset.PasswordResetLink = strings.Replace( input.PasswordReset.PasswordResetLink, "http://localhost:3000/auth/reset-password", "http://localhost:3000/your/path", 1, ) return ogSendEmail(input, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import emailpassword from typing import Dict, Any from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: # This is: `/auth/reset-password` template_vars.password_reset_link = template_vars.password_reset_link.replace( "http://localhost:3000/auth/reset-password", "http://localhost:3000/your/path") return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ) ] ) ``` #### 3.2 On the frontend ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ // The user will be taken to the custom path when they click on forgot password. getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { return "/custom-reset-password-path"; } }, }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ // The user will be taken to the custom path when they click on forgot password. getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { return "/custom-reset-password-path"; } }, }), ], }); ``` :::warning[Not applicable since you do not use pre-built UI.] ::: ## Generate a reset link manually You can use the backend SDK to generate the reset password link as shown below: ```tsx import EmailPassword from "supertokens-node/recipe/emailpassword"; async function createResetPasswordLink(userId: string, email: string) { const linkResponse = await EmailPassword.createResetPasswordLink("public", userId, email); if (linkResponse.status === "OK") { console.log(linkResponse.link); } else { // user does not exist or is not an email password user } } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/emailpassword" ) func main() { userID := "..." linkRes, err := emailpassword.CreateResetPasswordLink("public", userID) if err != nil { // handle error } if linkRes.OK != nil { link := linkRes.OK.Link fmt.Println(link) } else { // user does not exist or is not an email password user } } ``` ```python from supertokens_python.recipe.emailpassword.asyncio import create_reset_password_link async def create_link(user_id: str, email: str): link = await create_reset_password_link("public", user_id, email) if isinstance(link, str): print(link) else: print("user does not exist or is not an email password user") ``` ```python from supertokens_python.recipe.emailpassword.syncio import create_reset_password_link def create_link(user_id: str, email: str): link = create_reset_password_link("public", user_id, email) if isinstance(link, str): print(link) else: print("user does not exist or is not an email password user") ``` :::info[Multy-tenancy] Notice that the first argument to the function call above is `"public"`. This refers to the default tenant ID used in SuperTokens. It means that the generated password reset link can only apply to users belonging to the `"public"` tenant. If you are using the multi-tenancy feature, you can pass in the `tenantId` that contains this user, which you can fetch by getting the user object for this `userId`. Finally, the generated link uses the configured `websiteDomain` from the `appInfo` object (in `supertokens.init`), however, you can change the domain of the generated link to match that of the tenant ID. ::: --- ## Change the reset's link lifetime By default, the password reset link's lifetime is 1 hour. You can change this via a core's configuration (time in milliseconds): ```bash # Here we set the lifetime to 2 hours. docker run \ -p 3567:3567 \ -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command email_verification_token_lifetime: 7200000 ``` :::info - For managed service, you can update these values in the **Configuration** page of the relevant deployment in the [SaaS Dashboard](https://supertokens.com/dashboard). - This requires that your SuperTokens core version >= `3.6.0` ::: --- ## See also --- # Implement common domain login Source: https://supertokens.com/docs/authentication/enterprise/common-domain-login ## Overview This guide shows you how to authenticate users through the same page, `https://example.com/auth`, and then redirect them to their subdomain after sign-in. The login page adjusts the authentication method based on the tenant's configuration. You can determine the tenant in several ways. A common approach is to ask the user for their organization name and use it as the `tenantId` configured in SuperTokens. :::info[Important] You can find an example app for this setup with the **pre-built UI** on [the GitHub example directory](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-one-login-many-subdomains). The app is setup to have three tenants: - `tenant1`: Login with `emailpassword` + Google sign in - `tenant2`: Login with `emailpassword` - `tenant3`: Login with passwordless + GitHub sign in You can also generate a demo app using the following command: ```bash npx create-supertokens-app@latest --recipe=multitenancy ``` ::: ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). You also need to create the tenants that your application requires. View the [previous tutorial](/authentication/enterprise/initial-setup) for more information on how to do this. ## Steps ### 1. Ask for the tenant ID on the login page If you have [followed the pre-built UI setup](/quickstart#1-integrate-the-frontend-sdk), when you visit the login screen, you see the login screen immediately. The flow needs to change to first ask the user to enter their tenant ID and then display the login UI based on the tenant ID. To do that, first obtain the tenant ID from the user. You can achieve this by building a UI that asks them to enter their tenant ID or organization name (which can serve as the tenant ID). This example implements the UI in a component called `AuthPage`. :::warning You have to [create tenants](/authentication/enterprise/initial-setup) before you can complete this step. ::: :::info[Caution] No code snippet provided here, however, if you visit the auth component, you see that the pre-built UI renders in the `"supertokensui"` `div` element on page load. The logic here needs to change to first check if the user has provided the `tenantId`. If they have, the SuperTokens UI renders as usual. If they have not, a simple UI renders which asks the user for their tenant id and saves that in `localstorage`. Switch to the React code tab here to see the implementation in React, and a similar logic applies here. ::: ```tsx import { useState } from "react"; import * as reactRouterDom from "react-router-dom"; import { Routes } from "react-router-dom"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; export const AuthPage = () => { const location = reactRouterDom.useLocation(); const [inputTenantId, setInputTenantId] = useState(""); const tenantId = localStorage.getItem("tenantId") ?? undefined; const session = useSessionContext(); if (session.loading) { return null; } if ( tenantId !== undefined || // if we have a tenantId stored session.doesSessionExist === true || // or an active session (it'll contain the tenantId) new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId ) { // Since this component (AuthPage) is rendered in the /auth route in the main Routes component, // and we are rendering this in a sub route as shown below, the third arg to getSuperTokensRoutesForReactRouterDom // tells SuperTokens to create Routes without /auth prefix to them, otherwise they would // render on /auth path. return {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI], "/auth")}; } else { return (
{ // this value will be read by SuperTokens as shown in the next steps. localStorage.setItem("tenantId", inputTenantId); }} >

Enter your organization's name:

setInputTenantId(e.target.value)} />
); } }; ```
```tsx import { useState } from "react"; import { getRoutingComponent } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; export const AuthPage = () => { const [inputTenantId, setInputTenantId] = useState(""); const tenantId = localStorage.getItem("tenantId") ?? undefined; const session = useSessionContext(); if (session.loading) { return null; } if ( tenantId !== undefined || // if we have a tenantId stored session.doesSessionExist === true || // or an active session (it'll contain the tenantId) new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId ) { return getRoutingComponent([EmailPasswordPreBuiltUI]); } else { return (
{ // this value will be read by SuperTokens as shown in the next steps. localStorage.setItem("tenantId", inputTenantId); }} >

Enter your organization's name:

setInputTenantId(e.target.value)} />
); } }; ```
The example creates a simple UI that asks the user for their organization's name. Their input serves as their tenant ID. When the user submits that form, the value is stored in local storage. :::info[Important] The `AuthPage` component should render to show on `/auth/*` paths of the website. The `AuthPage` replaces the call to `getSuperTokensRoutesForReactRouterDom` or `getRoutingComponent` that you may have added to your app from the quick setup section. :::
You need to build a UI that asks the user to enter their tenant ID or organization name (which can serve as the tenant ID). The input value is then used in function calls, as shown below. Once you have the user's tenant ID, you can fetch their list of configured providers and render the third party login buttons accordingly: ```tsx import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId, }); if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find((i) => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```tsx import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId, }); if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find((i) => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```bash curl --location --request GET '/auth/loginmethods' ``` - The code snippet fetches the login methods for the tenant ID. - It then renders the login UI buttons based on the configured `thirdPartyId` values in the response. The response body from the API call has a `status` property in it: - `status: "OK"`: The `recipes` field contains information about which login methods are active along with the list of third party providers configured for this tenant. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should appear on the frontend. ### 2. Include the tenant ID in authentication flow You need to tell SuperTokens how to resolve the tenant ID. To do this, set the `getTenantId` function in the `Multitenancy` recipe. In the current example, local storage provides the `tenantId`. ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", apiBasePath: "...", websiteBasePath: "...", }, usesDynamicLoginMethods: true, recipeList: [ Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; }, }; }, }, }), // other recipes... ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", apiBasePath: "...", websiteBasePath: "...", }, usesDynamicLoginMethods: true, recipeList: [ supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; }, }; }, }, }), // other recipes... ], }); ``` :::info[Important] Set the `usesDynamicLoginMethods` to `true` to tell SuperTokens that the login methods are dynamic (based on the `tenantId`). On page load (of the login page), SuperTokens first fetches the configured login methods for the `tenantId`. It then displays the login UI based on the result of the API call. ::: Initialize the multitenancy recipe with the following callback. You can get the tenant ID from wherever you stored it after asking the user for it. All the steps for mobile app login are similar to the [social login steps](/authentication/social/initial-setup#2-add-the-login-ui). However, when you are calling the sign in up API, you also need to pass in the `tenantId` in the request path. An example of this appears below: ```tsx import SuperTokens from "supertokens-web-js"; import Multitenancy from "supertokens-web-js/recipe/multitenancy"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; }, }; }, }, }), // other recipes... ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ supertokensMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: (input) => { let tid = localStorage.getItem("tenantId"); return tid === null ? undefined : tid; }, }; }, }, }), // other recipes... ], }); ``` ```bash curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "...", "clientType": "...", "oAuthTokens": { "access_token": "...", "id_token": "..." }, }' ``` ### 3. Redirect users based on tenant subdomain (optional) If each tenant has access to specific subdomains in your application, redirect users after sign-in. #### 3.1 Restrict subdomain access Before redirecting users, restrict which subdomains their sessions can be used on. To do this configure the SDK to know which domain each `tenantId` has access to. ```tsx import SuperTokens from "supertokens-node"; import Multitenancy from "supertokens-node/recipe/multitenancy"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Multitenancy.init({ getAllowedDomainsForTenantId: async (tenantId, userContext) => { // query your db to get the allowed domain for the input tenantId // or you can make the tenantId equal to the subdomain itself return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"]; }, }), // other recipes... ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ multitenancy.Init(&multitenancymodels.TypeInput{ GetAllowedDomainsForTenantId: func(tenantId string, userContext supertokens.UserContext) ([]string, error) { // query your db to get the allowed domain for the input tenantId // or you can make the tenantId equal to the subdomain itself return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil }, }), }, }) } ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multitenancy from typing import Dict, Any, List async def get_allowed_domains_for_tenant_id(tenant_id: str, user_context: Dict[str, Any]) -> List[str]: return [tenant_id + ".myapp.com", "myapp.com", "www.myapp.com"] init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="django", # Change this to "flask" or "fastapi" if you are using Flask or FastAPI recipe_list=[ multitenancy.init( get_allowed_domains_for_tenant_id=get_allowed_domains_for_tenant_id ) ], ) ``` The code sample tells SuperTokens to add the returned domains to the user's session claims when they sign in. The claim is available on the frontend and backend and can restrict where the session is used. :::warning[Domain checks are not tenant authorization] `AllowedDomainsClaim` and `hasAccessToCurrentDomain` restrict session use by hostname. They do not prove that the user belongs to an organization, enforce CORS or allowed browser origins, authorize access to business data, or provide complete tenant isolation. After authentication, verify the user's tenant membership. On every business-data access, the backend must derive the tenant from trusted session data and enforce application-level tenant authorization. Do not trust a tenant ID, hostname, or claim supplied by the browser as authorization. ::: #### 3.2 Redirect the user to their subdomain after sign-in After sign-in, the frontend SDK redirects the user to the `/` route by default. You can instead redirect them to their subdomain based on their tenant ID. ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { let claimValue: string[] | undefined = await Session.getClaimValue({ claim: Multitenancy.AllowedDomainsClaim, }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default subdomain } } return undefined; }, recipeList: [ /* Recipe init here... */ ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { let claimValue: string[] | undefined = await supertokensUISession.getClaimValue({ claim: supertokensUIMultitenancy.AllowedDomainsClaim, }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default subdomain } } return undefined; }, recipeList: [ /* Recipe init here... */ ], }); ``` On the frontend, after the user signs in, you can read the domain from their session and redirect them accordingly. ```tsx import Session from "supertokens-web-js/recipe/session"; import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function redirectToSubDomain() { if (await Session.doesSessionExist()) { let claimValue: string[] | undefined = await Session.getClaimValue({ claim: Multitenancy.AllowedDomainsClaim, }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default subdomain } } else { window.location.href = "/auth"; } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function redirectToSubDomain() { if (await supertokensSession.doesSessionExist()) { let claimValue: string[] | undefined = await supertokensSession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); if (claimValue !== undefined) { window.location.href = "https://" + claimValue[0]; } else { // there was no configured allowed domain for this user. Throw an error cause of // misconfig or redirect to a default subdomain } } else { window.location.href = "/auth"; } } ``` - The `AllowedDomainsClaim` claim is auto added to the session by the backend SDK if you provide the `GetAllowedDomainsForTenantId` configuration from the previous step. - This claim contains the domains configured for the session's tenant ID. It is not proof of user membership or authorization to business data. ### 6. Share sessions across subdomains (optional) If the user authenticates on your main website domain (`https://example.com/auth`) and is redirected to a subdomain, update the Session recipe to share sessions across subdomains. You can do this [by setting the `sessionTokenFrontendDomain` value in the Session recipe](/post-authentication/session-management/share-session-across-sub-domains). If the subdomains assigned to your tenants have their own backends on separate subdomains (one per tenant), you can also enable [sharing of sessions across API domains](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). ### 7. Limit session use to the tenant's subdomain (optional) The frontend uses session claim validators to restrict session use by subdomain. Before proceeding, make sure that you define the `GetAllowedDomainsForTenantId` function mentioned above. This adds the list of allowed domains into the user's access token payload. On the frontend, check whether the current subdomain is in the session's allowed domains. If it is not, redirect the user to the correct subdomain. You can achieve this by using the `hasAccessToCurrentDomain` session validator from the multitenancy recipe. You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import React from "react"; import Session from "supertokens-auth-react/recipe/session"; import { AllowedDomainsClaim } from "supertokens-auth-react/recipe/multitenancy"; Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensUISession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import Session from "supertokens-web-js/recipe/session"; import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. ```tsx import Session from "supertokens-web-js/recipe/session"; import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokensSession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensSession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes#check-the-claims-of-a-session), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. --- ## See also --- # Important concepts Source: https://supertokens.com/docs/authentication/enterprise/important-concepts ## Overview [Multitenancy](https://supertokens.com/features/multi-tenancy) organizes an application into groups of users that share access to it. These groups, or **tenants**, can have separate authentication configuration and user pools. Each tenant can also have different sign-in methods, configured by the tenant or by you, the application developer. For example, a SaaS application for a financial company may want to separate their users by the financial institution they represent. This might use a sign-in screen that asks for a username, password, and tenant name. The application would then route the user to their specific tenant, which could be a different database or a different collection of data within a database. ## References With SuperTokens, there are two levels of abstraction for multitenancy: **Tenant** and **Application**. :::warning[Authentication tenancy is not complete data isolation] SuperTokens tenants separate authentication configuration and can separate authentication user pools. A tenant ID in an authentication request or session does not prove organization membership or authorize access to application data. After authentication, verify the user's membership in the tenant. On every business-data access, derive the tenant from trusted session data and enforce application-level tenant authorization. Scope database queries and storage to that authorized tenant. Do not rely on tenant routing, hostnames, CORS, frontend checks, or session claims alone for tenant isolation. ::: ### Tenant A tenant is a group of users with shared access and specific privileges in an application. Key characteristics: - Each tenant can have its own sign-in method. For example, one tenant can use email-password sign-in, while another can use SSO. - Each tenant has its own user pool. One user can sign in with the same email across different tenants, and the system treats the identities as different users. You can also share a user across tenants. - You can isolate SuperTokens authentication data further by using a different database for each tenant. Business-data isolation remains the application's responsibility. - Roles and permissions exist at the application level, but their mapping to users is defined at the tenant level. This means that the same user shared across tenants can have different roles / permissions, depending on the tenant they log into. It also means that you can share the same role and permission set across tenants. - Sessions are per tenant (`appId` -> `tenantId` -> session handle) and cannot be shared across tenants. - For multiple tenants, you can run the same backend and frontend across all tenants of an app. Each request from the frontend contains a `tenantId` identifying that tenant to the backend, and once logged in, each session also contains that user's `tenantId`. ### Application The top-most level of abstraction in SuperTokens multitenancy. Key characteristics: - Each app can have its own set of tenants and users, which can't be shared with other apps. - Each app needs to have its own SuperTokens backend and SuperTokens frontend SDK setup. - User metadata is at the application level because users are also at the application level. - When you start the core for the first time, SuperTokens creates an app (`appId` is `"public"`) and one tenant in it (tenantId is `"public"`). When you create a new app, you also get a new tenant (`tenantId` is `"public"`) as part of that app created for you. - A user can be uniquely recognized by their `appId` -> `userId`. This allows the same user to be shared across tenants if required. - The identity of the user (their email for example) can be uniquely identified by `appId` -> `tenantId` -> email. This allows the same email to be used across tenants while still being treated as different users with different user IDs. The same applies to phone numbers and third-party sign-in profiles. - You can create multiple apps and tenants in the same database or in different databases. The only restriction is that for an app, you cannot share a user across `tenantA` and `tenantB` if the databases for `tenantA` and `tenantB` are different. In other words, a user can only be shared across tenants that use the same database. ## Types of setup Based on these abstractions, you can choose from four setup types when configuring authentication with **SuperTokens**. ### Single tenant, single app The default use case when you are not using the multitenancy feature. Single tenant single app architecture ### Single tenant, multi app This is where you have multiple applications running on the same SuperTokens core instance and each application has a single user pool. This could be two different apps in your organization, or two different development environments for the same app (or some combination of this). Single tenant multi app architecture ### Multi tenant, single app Different customers use the same application, but each customer has their own set of users and login methods (each customer is a unique tenant in SuperTokens). Multi tenant single app architecture ### Multi tenant, multi app Multiple applications run on the same **SuperTokens** core instance, and each application has its own set of tenants. This could be two different applications in your organization, or two different development environments for the same application (or some combination of this). Multi tenant multi app architecture In a multi app, multi tenant setup: A user can be uniquely recognized by their `appId` -> `userId`. This allows the same user to be shared across tenants if required. The identity of the user (their email for example) can be uniquely identified by `appId` -> `tenantId` -> email. This allows the same email to be used across tenants while still being treated as different users with different user IDs. The same applies to phone numbers and third-party sign-in profiles. Roles and permissions exist at the application level, but their mapping to users is defined at the tenant level. This means that the same user shared across tenants can have different roles / permissions, depending on the tenant they log into. It also means that you can share the same role and permission set across tenants. Sessions are per tenant (`appId` -> `tenantId` -> session handle) and cannot be shared across tenants. User metadata is at the application level because users are also at the application level. --- # Initial setup Source: https://supertokens.com/docs/authentication/enterprise/initial-setup Set up SuperTokens multi-tenancy for this application. Inspect the existing backend, frontend, authentication recipes, and tenant identification strategy. Determine whether the feature requires the managed service, configure tenant creation and enabled first factors, and add enterprise provider configuration with credentials stored in environment variables. Preserve existing conventions, avoid committing secrets, and validate tenant resolution, provider callbacks, login, and session behavior for more than one tenant. ## Before you start ## Steps ### 1. Create a tenant The first step in setting up a multi tenant login system is to create a tenant in the SuperTokens core. Each tenant has a unique `tenantId` mapped to that tenant's configuation. The `tenantId` could be that tenant's sub domain, or a workspace URL, or anything else that can help identify them. The configuration mapped to each tenant contains information about which login methods they enable. Create Tenant Create a new tenant by clicking on the **Add Tenant** button and specify the tenant ID. All Login Methods Enabled Once you create the tenant, turn on the Login Methods as required for the tenant. In the above example, you turn on all the Login Methods. ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" ) func main() { tenantId := "customer1" emailPasswordEnabled := true thirdPartyEnabled := true passwordlessEnabled := true resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ EmailPasswordEnabled: &emailPasswordEnabled, ThirdPartyEnabled: &thirdPartyEnabled, PasswordlessEnabled: &passwordlessEnabled, }) if err != nil { // handle error } if resp.OK.CreatedNew { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): response = await create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] )) if response.status != "OK": print("Handle error") elif response.created_new: print("New tenant was created") else: print("Existing tenant's config was updated") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate def some_func(): response = create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] )) if response.status != "OK": print("Handle error") elif response.created_new: print("New tenant was created") else: print("Existing tenant's config was updated") ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] }' ``` The snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by not including them in the `firstFactors` input. If `firstFactors` is not specified, by default, the system does not enable any of the login methods. If you set `firstFactors` to `null` the SDK uses any of the login methods. The built-in Factor IDs available for `firstFactors` include: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | The code snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by setting the corresponding field to `false`. The code snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by setting the corresponding field to `false`. The request includes the `appId` for which you need to create a new tenant. If you are using the default (`"public"`) app, you can omit the `/appid-` part of the URL. The snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by not including them in the `firstFactors` input. If `firstFactors` is not specified, by default, the system does not enable any of the login methods. The built-in Factor IDs available for `firstFactors` include: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | #### Configure third party providers If you are using the `thirdparty` recipe on a tenant, you also need to set the providers that you want to use with it. There's an extensive list of [built-in providers](/authentication/social/built-in-providers-config), but you can also configure [a custom provider](/authentication/enterprise/manage-tenants). The next code snippet shows how you can add an Active Directory login to your tenant. Update the `clientId`, `clientSecret`, and `directoryId` based on your tenant configuration. ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "active-directory", name: "Active Directory", clients: [ { clientId: "...", clientSecret: "...", }, ], oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "active-directory", Name: "Active Directory", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="active-directory", name="Active Directoy", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ### 2. Provide additional configuration per tenant You can also configure a tenant to use different settings. The next sample shows you how to customize the values. Custom tenant configuration In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. You can edit the values by clicking on the pencil icon and then specifying a new value. :::warning[You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them.] ::: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { coreConfig: { email_verification_token_lifetime: 7200000, password_reset_token_lifetime: 3600000, postgresql_connection_uri: "postgresql://localhost:5432/db2", }, }); if (resp.createdNew) { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ CoreConfig: map[string]interface{}{ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, }) if err != nil { // handle error } if resp.OK.CreatedNew { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): tenant_id = "customer1" result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate tenant_id = "customer1" result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" } }' ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. ### 3. View tenant details To view the configuration for a specific tenant you can use an SDK method or call the API directly. ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function getTenant(tenantId: string) { let resp = await Multitenancy.getTenant(tenantId); if (resp === undefined) { // tenant does not exist } else { let coreConfig = resp.coreConfig; let firstFactors = resp.firstFactors; let configuredThirdPartyProviders = resp.thirdParty.providers; } } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/multitenancy" ) func main() { tenantId := "customer1" tenant, err := multitenancy.GetTenant(tenantId) if err != nil { // handle error } if tenant == nil { // tenant does not exist } else { isEmailPasswordLoginEnabled := tenant.EmailPassword.Enabled; isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; if (isEmailPasswordLoginEnabled) { // Tenant support email password login } if (isThirdPartyLoginEnabled) { // Tenant support third party login configuredThirdPartyProviders := tenant.ThirdParty.Providers; fmt.Println(configuredThirdPartyProviders); } if (isPasswordlessLoginEnabled) { // Tenant support passwordless login } } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import get_tenant async def some_func(): tenant = await get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```python from supertokens_python.recipe.multitenancy.syncio import get_tenant tenant = get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```bash curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). If the input tenant does not exist, you get back a `200` status code with the following JSON: ```json { "status": "TENANT_NOT_FOUND_ERROR" } ``` Otherwise you get a `200` status code with the following JSON output: ```json check=false reason="The providers array is abbreviated in this example response." { "status": "OK", "thirdParty": { "providers": [...] }, "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" }, "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] } ``` The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. ### 4. Set up the user interface To allow users to authenticate using one of your previously created tenants you need to update your frontend application. You can do this in two ways: [through a common domain](/authentication/enterprise/common-domain-login), [through subdomains](/authentication/enterprise/subdomain-login). Explore the two guides for a full list of instructions on how to implement the flows. ## See also --- # Introduction Source: https://supertokens.com/docs/authentication/enterprise/introduction ## Overview **SuperTokens** covers enterprise authentication scenarios through the **multi-tenancy** feature. Tenants represent isolated user groups that can only access their specific data. Furthermore, each tenant can have different methods of logging in, configured by the tenant, or by you (the application developer). For example, a SaaS application for a financial company may want to separate their users by the financial institution they represent. This would require a login screen that asks for a username and password, as well as the name of the tenant. The application would then route the user to their specific tenant, which could be a different database or a different collection of data within a database. ### Features | Feature | Description | |---------|-------------| | Enterprise SSO/SAML login | Customers can log in with Workforce IdP or any other SSO provider | | Unique login methods per tenant | Each tenant can have its own login method (for example, email password login for one tenant, SSO login for another) | | Different user pools | Each tenant has its own isolated user pool. Users can use the same email across different tenants as separate accounts and you can share users across tenants. | | Data isolation | You can have separate databases per tenant | | Dynamic tenant creation | You can create tenants via API calls from your backend without manual onboarding | | Multiple development environments | Create multiple environments for development and CI/CD testing purposes | | Flexible tenant discovery | Authenticate users through different subdomains or based on a custom tenant selection | ## Getting started The initial setup guide shows you how to create a tenant and configure authentication for it. After that, you have to implement the tenant discovery flow. Choose between the common domain login and subdomain login methods, based on your use case. Before you explore the guides, read through the **Important concepts** page first. It explains how multi tenancy works in more detail. Go through a reference guide that explains all the multi-tenancy related concepts that are specific to SuperTokens. Create a new tenant and configure the login methods on it. Allow tenants to login using the same domain. Allow tenants to login using different subdomains. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Discover all the common actions that you can perform on tenants. Discover all the common actions that you can perform on applications. See how you can implement SAML login. --- # Legacy SAML Source: https://supertokens.com/docs/authentication/enterprise/legacy-saml ## Overview The following guide shows you how to configure SAML using the legacy setup with SuperTokens. :::warning[Since version `11.3` of the core service you can use SuperTokens as a SAML client.] We recommend that you use the latest version with [the simplified setup](/authentication/enterprise/saml). ::: :::danger[Archived, non-operational reference] Do not use the commands on this page for a new or production deployment. This legacy flow has no currently validated, pinned Jackson release. The versioned contract for the Polis replacement has not been established for `/api/v1/saml/config`, SuperTokens-managed hosting, provider-name aliases, or the returned client credentials. The examples below are retained only to explain the historical integration and must not be treated as current Polis APIs. ::: ### BoxyHQ [BoxyHQ](https://boxyhq.com/) is a commercial open-source company that has a product called "SAML Jackson" which helps integrate `SAML` providers into your application. `SAML` Jackson is a perfect fit with SuperTokens because: - It's an OAuth provider and a `SAML` client. This fits with the architecture. - For self hosted, it supports PostgreSQL and MySQL amongst [other databases](https://boxyhq.com/docs/jackson/deploy/service#database). Self hosted SuperTokens only supports PostgreSQL as a data source. `SAML` Jackson provides an HTTP service that you can host yourself or let BoxyHQ manage. The HTTP service uses NodeJS, and is embeddable within your NodeJS backend. However, because SuperTokens supports multiple backends, the focus is on deploying `SAML` Jackson as a microservice. Flowchart of integrating a `SAML` provider with SuperTokens using `SAML` Jackson ( BoxyHQ ) 1. The user clicks on the login button and redirects to `SAML` Jackson's microservice at `http://localhost:5225/api/oauth/authorize` 2. `SAML` Jackson redirects the user to the `SAML` provider's login page where the user needs to enter their credentials 3. After successfully authenticating the user, the `SAML` provider redirects the user to `SAML` Jackson. Step (2) and (3) follow the `SAML` protocol. 4. `SAML` Jackson redirects the user to the frontend app on the configured callback URL. The callback URL contains the one-time use auth code. 5. SuperTokens' frontend `SDK` passes the one-time use auth code to your app's backend. 6. SuperTokens' backend `SDK` verifies the auth code by querying `SAML` Jackson. On success, `SAML` Jackson returns the end user's information and access token. 7. SuperTokens' backend `SDK` creates a new user in the core associated with the end user's email. New session tokens are also created 8. A SuperTokens' session establishes between your app's backend and frontend - logging in the user. :::info[Example App] An [example app on GitHub](https://github.com/supertokens/jackson-supertokens-express) with SuperTokens + `SAML` Jackson, for React and NodeJS express apps, is available. This uses [mocksaml.com](https://mocksaml.com/) as a `SAML` provider ::: ## Before you start These instructions assume that you already are familiar with **SuperTokens** and you have configured a demo application. If you have skipped those steps, follow the main [quickstart guide](/quickstart). ## Using the SuperTokens dashboard ### 1. Generate the XML metadata file from your SAML provider Your SAML provider allows you to download a `.xml` file that you can upload to SAML Jackson. During this process, you need to provide it: - the SSO URL and; - the Entity ID. You can learn more about these in the [SAML Jackson docs](https://boxyhq.com/docs/jackson/configure-saml-idp). In the example app, [mocksaml.com](https://mocksaml.com/) serves as a free SAML provider for testing. When you navigate to the site, you see a "Download metadata" button which you should click on to get the `.xml` file. ### 2. Start the SAML Jackson service The former unpinned `boxyhq/jackson` deployment command has been removed because it does not identify a reproducible, validated release. Do not substitute `boxyhq/jackson:latest`. To recover this flow, first establish and document a known-compatible image digest, its required environment variables, and its versioned API contract. ### 3. Create a new tenant in SuperTokens (if not done already) Create Tenant ### 4. Configure the SAML provider for the tenant Create Tenant To configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party configuration. Make sure to specify provider name with one of the following: - Microsoft Entra ID
- Microsoft AD FS
- Okta
- Auth0
- Google
- OneLogin
- PingOne
- JumpCloud
- Rippling
- SAML
The historical dashboard flow assumed a separately hosted BoxyHQ server. SuperTokens-managed hosting for this legacy integration has not been validated against a current, versioned contract. :::success[You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows.] ::: ### 5. Adding multiple SAML connections to a single tenant If you have one SAML connection for a tenant, then the `Third Party Id` for that connection can be `boxy-saml`. This displays a single "SAML Login" button on the pre-built UI. If you want to add a second SAML connection for the same tenant, follow the same steps as above, but also use the `Add Suffix` option for the Third Party Id. For example, if a tenant has Active Directory and Okta login (both with SAML), you can create the Active Directory provider using `"boxy-saml"` as the `Third Party Id`. For Okta, you could use `okta` as a suffix to make the `Third Party Id` equal to `"boxy-saml-okta"`. You can also give them different names. Instead of "SAML Login" (that's shown above), you can use "Active Directory" and "Okta" to ensure that the button on the pre-built UI shows the right name. --- ## Using the BoxyHQ API ### 1. Generate the XML metadata file from your SAML provider Your SAML provider allows you to download a `.xml` file that you can upload to SAML Jackson. During this process, you need to provide it: - the SSO URL and; - the Entity ID. You can learn more about these in the [SAML Jackson docs](https://boxyhq.com/docs/jackson/configure-saml-idp). In the example app, [mocksaml.com](https://mocksaml.com/) serves as a free SAML provider for testing. When you navigate to the site, you see a "Download metadata" button which you should click on to get the `.xml` file. ### 2. Convert the `.xml` file to base64 You can use [an online base64 encoder](https://www.base64encode.org/) to do this. First copy the contents of the `.xml` file, and then put it in the encoder tool. The output string is the base64 version of the .xml file. For example, with an input `.xml` file (obtained from mocksaml.com): ```text MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV SzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4 MjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK DAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0 RuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd 4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V pwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b 2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ NfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF AAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW 5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4 khuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX UjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L r/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M m0eo2USlSRTVl7QHRTuiuSThHpLKQQ== urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress ``` The base64 output is: ```text PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PEVudGl0eURlc2NyaXB0b3IgeG1sbnM6bWQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDptZXRhZGF0YSIgZW50aXR5SUQ9Imh0dHBzOi8vc2FtbC5leGFtcGxlLmNvbS9lbnRpdHlpZCIgdmFsaWRVbnRpbD0iMjAyNi0wNi0yMlQxODozOTo1My4wMDBaIj48SURQU1NPRGVzY3JpcHRvciBXYW50QXV0aG5SZXF1ZXN0c1NpZ25lZD0iZmFsc2UiIHByb3RvY29sU3VwcG9ydEVudW1lcmF0aW9uPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxLZXlEZXNjcmlwdG9yIHVzZT0ic2lnbmluZyI+PEtleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSUM0akNDQWNvQ0NRQzMzd255YlQ1UVpEQU5CZ2txaGtpRzl3MEJBUXNGQURBeU1Rc3dDUVlEVlFRR0V3SlYKU3pFUE1BMEdBMVVFQ2d3R1FtOTRlVWhSTVJJd0VBWURWUVFEREFsTmIyTnJJRk5CVFV3d0lCY05Nakl3TWpJNApNakUwTmpNNFdoZ1BNekF5TVRBM01ERXlNVFEyTXpoYU1ESXhDekFKQmdOVkJBWVRBbFZMTVE4d0RRWURWUVFLCkRBWkNiM2g1U0ZFeEVqQVFCZ05WQkFNTUNVMXZZMnNnVTBGTlREQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dFUEFEQ0NBUW9DZ2dFQkFMR2ZZZXR0TXNjdDFUNnRWVXdUdWROSkg1UG5iOUdHbmtYaTlady9lNng0NUREMApSdVJPTmJGbEoyVDRSakFFL3VHK0FqWHhYUThvMlNaZmI5K0dnbUNIdVRKRk5nSG9aMW5GVlhDbWIvSGc4SHBkCjR2T0FHWG5kaXhhUmVPaXEzRUg1WHZwTWpNa0ozKzgrOVZZTXpNWk9qa2dRdEFxTzM2ZUFGRmZOS1g3ZFRqM1YKcHdMa3Z6Ni9LRkNxOE9Bd1krQVVpNGVabTVKNTdEMzFHempId2ZqSDlXVGVYME15bmRtbk5CMXFWNzVxUVIzYgoyL1c1c0dIUnYrOUFhcmdnSmtGK3B0VWtYb0x0VkE1MXdjZlltNmhJTHB0cGRlNUZRQzhSV1kxWXJzd0JXQUVaCk5meXJSNEplU3dlRWxOSGc0TlZPczRUd0dqT1B3V0dxelRmZ1RsRUNBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0YKQUFPQ0FRRUFBWVJsWWZsU1hBV29acEZmd05pQ1FWRTVkOXpaMERQek5kV2hBeWJYY1R5TWYwejVtRGY2RldCVwo1R3lvaTl1M0VNRURuekxjSk5rd0pBQWMzOUFwYTRJMi90bWwrSnkyOWRrOGJUeVg2bTkzbmdtQ2dkTGg1WmE0CmtodVUzQU0zTDYzZzdWZXhDdU83a3dramgvK0xxZGNJWHNWR082WERmdTJRT3MxWHBlOXpJekxwd20vUk5ZZVgKVWpiU2o1Y2UvamVrcEF3N3F5VlZMNHhPeWg4QXRVVzFlazN3SXcxTUp2RWdFUHQwZDE2b3NoV0pwb1MxT1Q4TApyLzIyU3ZZRW8zRW1TR2RUVkdnazN4M3MrQTBxV0FxVGN5anI3UTRzL0dLWVJGZm9tR3d6MFRaNEl3MVpOOTlNCm0wZW8yVVNsU1JUVmw3UUhSVHVpdVNUaEhwTEtRUT09CjwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvS2V5RGVzY3JpcHRvcj48TmFtZUlERm9ybWF0PnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzczwvTmFtZUlERm9ybWF0PjxTaW5nbGVTaWduT25TZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVJlZGlyZWN0IiBMb2NhdGlvbj0iaHR0cHM6Ly9tb2Nrc2FtbC5jb20vYXBpL3NhbWwvc3NvIi8+PFNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbW9ja3NhbWwuY29tL2FwaS9zYW1sL3NzbyIvPjwvSURQU1NPRGVzY3JpcHRvcj48L0VudGl0eURlc2NyaXB0b3I+ ``` ### 3. Start the SAML Jackson service The historical deployment command is intentionally omitted. A compatible Jackson image version or digest and its deployment contract have not been established. ### 4. Historical SAML Jackson configuration request The following request records the old integration shape only. Do not send it to Polis: no versioned authoritative contract has confirmed that Polis supports this route, fields, authentication scheme, or response credentials. ```bash curl --location --request POST 'http://localhost:5225/api/v1/saml/config' \ --header 'Authorization: Api-Key secret' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'encodedRawMetadata=' \ --data-urlencode 'defaultRedirectUrl=' \ --data-urlencode 'redirectUrl=[""]' \ --data-urlencode 'tenant=' \ --data-urlencode 'product=' \ --data-urlencode 'name=demo-config' \ --data-urlencode 'description=Demo SAML config' ``` You can learn more about the configuration values [in the SAML Jackson docs](https://boxyhq.com/docs/jackson/saml-flow#2-saml-config-api). For the example app, you can see [this command here](https://github.com/supertokens/jackson-supertokens-express/blob/main/addTenant.sh). This helper script, `addTenant.sh`, provides the command. You can run it like: ```bash ./addTenant.sh # example ./addTenant.sh customer1 ./addTenant.sh customer2 ``` The output of this command provides you the `client_id` and `client_secret` for this tenant. You need to provide these values to SuperTokens for this tenant when configuring this tenant's providers (see below). ### 5. Create a new tenant in SuperTokens (if not done already) ```tsx import Multiteancy from "supertokens-node/recipe/multitenancy"; async function createTenant() { let resp = await Multiteancy.createOrUpdateTenant("customer1", { firstFactors: ["thirdparty"], }); if (resp.createdNew) { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" ) func main() { tenantId := "customer1" thirdPartyEnabled := true resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ ThirdPartyEnabled: &thirdPartyEnabled, }) if err != nil { // handle error } if resp.OK.CreatedNew { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): result = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate(first_factors=["thirdparty"]) ) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant was created") else: print("existing tenant's config was modified.") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate def some_func(): result = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate(first_factors=["thirdparty"]) ) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant was created") else: print("existing tenant's config was modified.") ``` ### 6. Configure the SAML provider for the tenant ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyConfigToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "boxy-saml", name: "", clients: [ { clientId: "", clientSecret: "", additionalConfig: { boxyURL: "http://localhost:5225", }, }, ], }); if (resp.createdNew) { // SAML Login added to customer1 } else { // Existing SAML Login config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "boxy-saml", Name: "", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", ClientSecret: "", AdditionalConfig: map[string]interface{}{ "boxyURL": "http://localhost:5225", }, }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // SAML Login added to customer1 } else { // Existing SAML Login config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): result = await create_or_update_third_party_config("customer1", ProviderConfig( third_party_id="boxy-saml", name="", clients=[ ProviderClientConfig( client_id="", client_secret="", additional_config={ "boxyURL": "http://localhost:5225", } ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("SAML Login added to customer1") else: print("Existing SAML Login config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig result = create_or_update_third_party_config("customer1", ProviderConfig( third_party_id="boxy-saml", name="", clients=[ ProviderClientConfig( client_id="", client_secret="", additional_config={ "boxyURL": "http://localhost:5225", } ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("SAML Login added to customer1") else: print("Existing SAML Login config overwritten for customer1") ``` To configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party configuration. Make sure to replace `` in the code snippet above with one of the following - Microsoft Entra ID
- Microsoft AD FS
- Okta
- Auth0
- Google
- OneLogin
- PingOne
- JumpCloud
- Rippling
- SAML
The provider configuration above is historical. Current SuperTokens-managed hosting and the replacement service contract have not been validated. :::success[You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows.] ::: ### 7. Adding multiple SAML connections to a single tenant If you have one SAML connection for a tenant, then the `thirdPartyId` for that connection can be `boxy-saml`. This displays a single "SAML Login" button on the pre-built UI. If you want to add a second SAML connection for the same tenant, follow the same steps as above. Instead of using `"boxy-saml"` as the `thirdPartyId`, set it to another value that starts with `"boxy-saml"` (for step 6). For example, if a tenant has Active Directory and Okta login (both with SAML), you can create the Active Directory provider using `"boxy-saml"` as the `thirdPartyId`. For Okta, you could use `"boxy-saml-okta"`. It's important that the string starts with `"boxy-saml"`. You can also give them different names. Instead of "SAML Login" (that's shown above), you can use "Active Directory" and "Okta" to ensure that the button on the pre-built UI shows the right name. --- ## See also --- # Manage apps Source: https://supertokens.com/docs/authentication/enterprise/manage-apps ## Run multiple apps using the same SuperTokens core Like how you can create multiple tenants / user pools within one SuperTokens core, you can create multiple apps within one core as well: - Each app operates in isolation and can have multiple tenants. - Each app can have its own database or share a database with another app (and yet remain logically isolated). - Each app can have its own set of [core and db configurations](https://github.com/supertokens/supertokens-core/blob/master/config.yaml). If a specific configuration is not explicitly set for an app, it inherits from the base configuration.yaml / docker environment variables configuration. - The core and db configurations of each tenant within an app inherit from the configurations of that app. You can use this feature to deploy one SuperTokens core across multiple independent apps within your company. Additionally, you can create multiple development environments (`dev`, staging, prod, etc.) for one app without deploying individual SuperTokens core instances. ### 1. Create a new app in the core :::warning This is a paid feature, even if creating an additional `dev` `env` on the managed service, or if using the `dev` license keys in case of self-hosting. The pricing is $50 / month / additional app. Please reach out to [support@SuperTokens.com](mailto:support@SuperTokens.com) if you have any questions, or if you want to create multiple `environments` and want a bulk discount. ::: To create a new app in the SuperTokens core, you can use the following cURL command: ```bash curl --location --request PUT '/recipe/multitenancy/app/v2' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1", "coreConfig": {...} }' ``` - The above command creates (or updates) an app with the `appId` of `app1`. - It also creates a default tenant for this app with the tenant ID of `public` (that is, the default `tenantId`). - You can set core configurations for this app (see the configuration.yaml / docker environment variable options for your core). The core configurations for a new app inherit from the configurations provided in the configuration.yaml / docker environment (or the **Configuration** page for the managed deployment). - By default, all the login methods enable for a new app (specifically, the `public` tenant of the new app), but you can pass in `firstFactors` input to specifically enable selected login methods. The built-in Factor IDs that you can use for `firstFactors` are: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` ```bash curl --location --request PUT '/recipe/multitenancy/app' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1", "thirdPartyEnabled": true, "passwordlessEnabled": true, "emailPasswordEnabled": true, "coreConfig": {...} }' ``` - The above command creates (or updates) an app with the `appId` of `app1`. - It also creates a default tenant for this app with the tenant ID of `public` (that is, the default `tenantId`). - You can set core configurations for this app (see the configuration.yaml / docker environment variable options for your core). The core configurations for a new app inherit from the configurations provided in the configuration.yaml / docker environment (or the **Configuration** page for the managed deployment). - By default, all the login methods enable for a new app (specifically, the `public` tenant of the new app), but you can pass in `false` to any of the login methods specified above to disable them. :::note[Even if a login method enables for a tenant, you still require to initialize the right recipe on the backend for sign up / in to be possible with that login method. For example, if for a tenant, you have enabled the passwordless login method, but don't use the passwordless (or a combination recipe that has passwordless) on the backend, then end users cannot sign up / in using the passwordless APIs because those APIs are not exposed via the backend SDK's middleware.] ::: ### 2. Configure the `appId` during backend SDK init Whilst one core can have multiple apps, you must use a dedicated backend (integrated with the backend SDK) per app. For example, if you have two apps, and both use a NodeJS backend, then you need to configure one app's backend to have `appId` as `app1` (as an example). The other app's backend should have `appId` as `app2`. You can specify an `appId` on the backend SDK SuperTokens.init by appending the `appId` to the `connectionUri` as shown below: ```tsx import supertokens from "supertokens-node"; supertokens.init({ supertokens: { connectionURI: "http://localhost:3567/appid-app1", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [], }); ``` ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:3567/appid-app1", }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo, SupertokensConfig init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='http://localhost:3567/appid-app1', ), framework='...', recipe_list=[ #... ] ) ``` - In the above code snippet, the backend SDK receives information that the `appId` to use for this app is `app1`. You can pick your own app ID, but whatever it is, you need to add it as shown above. - It is important to prefix the app ID with `appid-` as that enables the SuperTokens core to reliably detect the app that the query is for. --- ## List all the apps in a SuperTokens core You can only perform this via a cURL command. No helper function exists for this in the backend SDKs since the backend SDKs are per app anyway. ```bash curl --location --request GET '/recipe/multitenancy/app/list/v2' \ --header 'api-key: ' \ --header 'Content-Type: application/json' ``` You get the following JSON output: ```json check=false reason="The nested tenant fields are abbreviated for readability." { "status": "OK", "apps": [{ "appId": "app1", "tenants": [{ "tenantId": "customer1", "thirdParty": { "providers": [...] }, "coreConfig": {...}, "firstFactors": [...] }] }] } ``` ```bash curl --location --request GET '/recipe/multitenancy/app/list' \ --header 'api-key: ' \ --header 'Content-Type: application/json' ``` You get the following JSON output: ```json check=false reason="The nested tenant fields are abbreviated for readability." { "status": "OK", "apps": [{ "appId": "app1", "tenants": [{ "tenantId": "customer1", "emailPassword": { "enabled": true }, "thirdParty": { "enabled": true, "providers": [...] }, "passwordless": { "enabled": true }, "coreConfig": {...} }] }] } ``` --- ## Delete an app from SuperTokens core The following snippet shows you how to delete an app from a SuperTokens Core instance. This operation is irreversible and deletes all user data associated with the app. :::note[Before you delete an app, ensure that you satisfy the following requirements:] - The request must originate from the public app and tenant - The app must not have any tenants other than the public tenant. You need to delete other tenants first. After deleting an app, make sure to update any backend services configured to use this app ID to prevent unexpected errors. ::: ```bash curl --location --request POST '/recipe/multitenancy/app/remove' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1" }' ``` - The above command deletes the app with the `appId` of `app1` and all its associated tenants. - All user data, configuration, and tenant information associated with this app are permanently deleted. - The API key used must have the necessary permissions to delete apps. :::danger[This operation cannot be undone. Make sure you have backed up any important data before proceeding.] ::: --- # Tenant actions Source: https://supertokens.com/docs/authentication/enterprise/manage-tenants ## Create a new tenant Create Tenant Create a new tenant by clicking on the **Add Tenant** button and specify the tenant ID. All Login Methods Enabled Once you create the tenant, turn on the Login Methods as required for the tenant. In the above example, you turn on all the Login Methods. ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"], }); if (resp.createdNew) { // Tenant created successfully } else { // Existing tenant's config was modified. } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" ) func main() { tenantId := "customer1" emailPasswordEnabled := true thirdPartyEnabled := true passwordlessEnabled := true resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ EmailPasswordEnabled: &emailPasswordEnabled, ThirdPartyEnabled: &thirdPartyEnabled, PasswordlessEnabled: &passwordlessEnabled, }) if err != nil { // handle error } if resp.OK.CreatedNew { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): response = await create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] )) if response.status != "OK": print("Handle error") elif response.created_new: print("New tenant was created") else: print("Existing tenant's config was updated") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate def some_func(): response = create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] )) if response.status != "OK": print("Handle error") elif response.created_new: print("New tenant was created") else: print("Existing tenant's config was updated") ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] }' ``` The snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by not including them in the `firstFactors` input. If `firstFactors` is not specified, by default, the system does not enable any of the login methods. If you set `firstFactors` to `null` the SDK uses any of the login methods. The built-in Factor IDs available for `firstFactors` include: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | The code snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by setting the corresponding field to `false`. The code snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by setting the corresponding field to `false`. The request includes the `appId` for which you need to create a new tenant. If you are using the default (`"public"`) app, you can omit the `/appid-` part of the URL. The snippet creates a new tenant with the id `"customer1"`. It enables the email password, third party and passwordless login methods for this tenant. You can also disable any of these by not including them in the `firstFactors` input. If `firstFactors` is not specified, by default, the system does not enable any of the login methods. The built-in Factor IDs available for `firstFactors` include: | Authentication Type | Factor ID | |-------------------|-----------| | Email password auth | `emailpassword` | | Social login / enterprise SSO auth | `thirdparty` | | Passwordless - Email OTP | `otp-email` | | Passwordless - SMS OTP | `otp-phone` | | Passwordless - Email magic link | `link-email` | | Passwordless - SMS magic link | `link-phone` | --- ## Update a tenant You can also configure a tenant to have different configurations per the core's `config.yaml` or docker environment variables. Below is how you can specify the configuration, when creating or modifying a tenant: Custom tenant configuration In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. You can edit the values by clicking on the pencil icon and then specifying a new value. :::warning[You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them.] ::: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function createNewTenant() { let resp = await Multitenancy.createOrUpdateTenant("customer1", { coreConfig: { email_verification_token_lifetime: 7200000, password_reset_token_lifetime: 3600000, postgresql_connection_uri: "postgresql://localhost:5432/db2", }, }); if (resp.createdNew) { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ CoreConfig: map[string]interface{}{ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, }) if err != nil { // handle error } if resp.OK.CreatedNew { // new tenant was created } else { // existing tenant's config was modified. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): tenant_id = "customer1" result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate tenant_id = "customer1" result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( core_config={ "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2", }, )) if result.status != "OK": print("handle error") elif result.created_new: print("new tenant created") else: print("existing tenant's config was modified.") ``` ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data-raw '{ "tenantId": "customer1", "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" } }' ``` In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. In the above example, the system assigns different values for certain configurations for `customer1` tenant. All other configurations inherit from the base configuration. Notice the `postgresql_connection_uri`. This allows you to achieve **data isolation on a tenant level**. This configuration is not required. If not provided, the database stores the tenant's information as specified in the core's configuration. It is still a different user pool though. --- ## Get tenant details Once you have set the configs for a specific tenant, you can fetch the tenant info as shown below: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function getTenant(tenantId: string) { let resp = await Multitenancy.getTenant(tenantId); if (resp === undefined) { // tenant does not exist } else { let coreConfig = resp.coreConfig; let firstFactors = resp.firstFactors; let configuredThirdPartyProviders = resp.thirdParty.providers; } } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/multitenancy" ) func main() { tenantId := "customer1" tenant, err := multitenancy.GetTenant(tenantId) if err != nil { // handle error } if tenant == nil { // tenant does not exist } else { isEmailPasswordLoginEnabled := tenant.EmailPassword.Enabled; isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; if (isEmailPasswordLoginEnabled) { // Tenant support email password login } if (isThirdPartyLoginEnabled) { // Tenant support third party login configuredThirdPartyProviders := tenant.ThirdParty.Providers; fmt.Println(configuredThirdPartyProviders); } if (isPasswordlessLoginEnabled) { // Tenant support passwordless login } } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import get_tenant async def some_func(): tenant = await get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```python from supertokens_python.recipe.multitenancy.syncio import get_tenant tenant = get_tenant("customer1") if tenant is None: print("tenant does not exist") else: core_config = tenant.core_config first_factors = tenant.first_factors providers = tenant.third_party_providers print(core_config) print(first_factors) print(providers) ``` ```bash curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). If the input tenant does not exist, you get back a `200` status code with the following JSON: ```json { "status": "TENANT_NOT_FOUND_ERROR" } ``` Otherwise you get a `200` status code with the following JSON output: ```json check=false reason="The tenant response fields are abbreviated for readability." { "status": "OK", "thirdParty": { "providers": [...] }, "coreConfig": { "email_verification_token_lifetime": 7200000, "password_reset_token_lifetime": 3600000, "postgresql_connection_uri": "postgresql://localhost:5432/db2" }, "tenantId": "customer1", "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] } ``` The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. --- ## List all the tenants of an app ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function listAllTenants() { let resp = await Multitenancy.listAllTenants(); let tenants = resp.tenants; tenants.forEach((tenant) => { let coreConfig = tenant.coreConfig; let firstFactors = tenant.firstFactors; let configuredThirdPartyProviders = tenant.thirdParty.providers; }); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/multitenancy" ) func main() { resp, err := multitenancy.ListAllTenants() if err != nil { // handle error } for i := 0; i < len(resp.OK.Tenants); i++ { currTenant := resp.OK.Tenants[i] coreConfig := currTenant.CoreConfig; fmt.Println(coreConfig) isEmailPasswordLoginEnabled := currTenant.EmailPassword.Enabled; isThirdPartyLoginEnabled := currTenant.ThirdParty.Enabled; isPasswordlessLoginEnabled := currTenant.Passwordless.Enabled; configuredThirdPartyProviders := currTenant.ThirdParty.Providers; if isEmailPasswordLoginEnabled { // Tenant has email password login enabled } if isThirdPartyLoginEnabled { // Tenant has third party login enabled fmt.Println(configuredThirdPartyProviders) } if isPasswordlessLoginEnabled { // Tenant has passwordless login enabled } } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import list_all_tenants async def some_func(): response = await list_all_tenants() if response.status != "OK": print("Handle error") return for tenant in response.tenants: core_configuration = tenant.core_config first_factors = tenant.first_factors configured_third_party_providers = tenant.third_party_providers print(core_configuration) print(f"First factors: {first_factors}") print(f"Configured third party providers: {configured_third_party_providers}") ``` ```python from supertokens_python.recipe.multitenancy.syncio import list_all_tenants def some_func(): response = list_all_tenants() if response.status != "OK": print("Handle error") return for tenant in response.tenants: core_config = tenant.core_config first_factors = tenant.first_factors configured_third_party_providers = tenant.third_party_providers print(core_config) print(f"First factors: {first_factors}") print(f"Configured third party providers: {configured_third_party_providers}") ``` ```bash curl --location --request GET '/recipe/multitenancy/tenant/list/v2' \ --header 'api-key: ' \ --header 'Content-Type: application/json' ``` The value of `firstFactors` can be as follows: - `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. - `[]` (empty array): The tenant does not enable any login methods. - a non-empty array: The tenant enables only the login methods in the array. You get the following JSON output: ```json check=false reason="The tenant response fields are abbreviated for readability." { "status": "OK", "tenants": [{ "tenantId": "customer1", "thirdParty": { "providers": [...] }, "coreConfig": {...}, "firstFactors": [...] }] } ``` The value of `firstFactors` can be as follows: - `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. - `[]` (empty array): The tenant does not enable any login methods. - a non-empty array: The tenant enables only the login methods in the array. --- ## Add a custom third-party provider to a tenant If you can't find a provider in [the built-in list](/authentication/social/built-in-providers-config), you can add your own custom implementation. This page shows you how to do that on a per tenant basis. :::info[Note] If you think that SuperTokens should support this provider by default, make sure to let the team know [on GitHub](https://github.com/supertokens/supertokens-node/issues/88). ::: Once you have created a tenant, you want to call the API / function to create a new provider for the tenant as shown below. ### Using OAuth endpoints Click on **Add new provider** in the Social/Enterprise Providers section Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider ```tsx import Multiteancy from "supertokens-node/recipe/multitenancy"; async function createTenant() { let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "custom", name: "Custom Provider", clients: [ { clientId: "...", clientSecret: "...", scope: ["email", "profile"], }, ], authorizationEndpoint: "https://example.com/oauth/authorize", authorizationEndpointQueryParams: { // optional someKey1: "value1", someKey2: null, }, tokenEndpoint: "https://example.com/oauth/token", tokenEndpointBodyParams: { someKey1: "value1", }, userInfoEndpoint: "https://example.com/oauth/userinfo", userInfoMap: { fromUserInfoAPI: { userId: "id", email: "email", emailVerified: "email_verified", }, }, }); if (resp.createdNew) { // custom provider added to tenant } else { // existing custom provider config overwritten for tenant } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "..." resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "custom", Name: "Custom Provider", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", Scope: []string{"email", "profile"}, }, }, AuthorizationEndpoint: "https://example.com/oauth/authorize", AuthorizationEndpointQueryParams: map[string]interface{}{ // optional "someKey1": "value1", "someKey2": nil, }, TokenEndpoint: "https://example.com/oauth/token", TokenEndpointBodyParams: map[string]interface{}{ // optional "someKey1": "value1", }, UserInfoEndpoint: "https://example.com/oauth/userinfo", UserInfoMap: tpmodels.TypeUserInfoMap{ FromUserInfoAPI: struct{UserId string "json:\"userId,omitempty\""; Email string "json:\"email,omitempty\""; EmailVerified string "json:\"emailVerified,omitempty\""} { UserId: "id", Email: "email", EmailVerified: "email_verified", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Custom provider added to tenant } else { // Existing custom provider config overwritten for tenant } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields async def some_func(): tenant_id = "..." result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], authorization_endpoint="https://example.com/oauth/authorize", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, token_endpoint="https://example.com/oauth/token", token_endpoint_body_params={ "someKey1": "value1", }, user_info_endpoint="https://example.com/oauth/userinfo", user_info_map=UserInfoMap( from_user_info_api=UserFields( user_id="id", email="email", email_verified="email_verified", ), from_id_token_payload=UserFields(), ), )) if result.status != "OK": print("handle error") elif result.created_new: print("Custom provider added to tenant") else: print("Existing custom provider config overwritten for tenant") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields tenant_id = "..." result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], authorization_endpoint="https://example.com/oauth/authorize", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, token_endpoint="https://example.com/oauth/token", token_endpoint_body_params={ "someKey1": "value1", }, user_info_endpoint="https://example.com/oauth/userinfo", user_info_map=UserInfoMap( from_user_info_api=UserFields( user_id="id", email="email", email_verified="email_verified", ), from_id_token_payload=UserFields(), ), )) if result.status != "OK": print("handle error") elif result.created_new: print("Custom provider added to tenant") else: print("Existing custom provider config overwritten for tenant") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "custom", "name": "Custom provider", "clients": [{ "clientId": "...", "clientSecret": "...", "scope": ["email", "profile"] }], "authorizationEndpoint": "https://example.com/oauth/authorize", "authorizationEndpointQueryParams": { "someKey1": "value1", "someKey2": "value2" }, "tokenEndpoint": "https://example.com/oauth/token", "tokenEndpointBodyParams": { "someKey1": "value1" }, "userInfoEndpoint": "https://example.com/oauth/userinfo", "userInfoMap": { "fromUserInfoAPI": { "userId": "id", "email": "email", "emailVerified": "email_verified" } } } }' ``` You can see all the options in the [CDI documentation](https://supertokens.com/docs/references/cdi). | Field | Description | Example | |-------|-------------|---------| | `tenantId` | Unique ID that identifies the tenant. If not specified, defaults to `"public"` | `"customer1"` | | `thirdPartyId` | Unique ID for identifying the provider | `"google"` | | `name` | Display name used for the login button UI | `"XYZ"` → displays "Login using XYZ" | | `clients` | Array of client credentials/settings. Can contain multiple items for different client types (web/mobile) | Contains `clientId`, `clientSecret`, and optional `clientType` | | `authorizationEndpoint` | URL for user login | `"https://accounts.google.com/o/oauth2/v2/auth"` | | `authorizationEndpointQueryParams` | Optional configuration to modify query params | | | `tokenEndpoint` | API endpoint for exchanging Authorization Code | `"https://oauth2.googleapis.com/token"` | | `tokenEndpointBodyParams` | Optional configuration to modify request body | | | `userInfoEndpoint` | API endpoint that provides user information | `"https://www.googleapis.com/oauth2/v1/userinfo"` | | `userInfoMap` | Maps provider's JSON response to user info fields. Use dot notation to map nested fields: `user.id` | ```{ userId: "id", email: "email", emailVerified: "email_verified" }``` | ### Using OpenID Connect endpoints If the provider is Open ID Connect (OIDC) compatible, you can provide a URL for the `OIDCDiscoverEndpoint` configuration. The backend SDK automatically discovers authorization endpoint, token endpoint and the user info endpoint by querying the `/.well-known/openid-configuration`. Click on **Add new provider** in the Social/Enterprise Providers section Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider ```tsx import Multiteancy from "supertokens-node/recipe/multitenancy"; async function createTenant() { let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "custom", name: "Custom Provider", clients: [ { clientId: "...", clientSecret: "...", scope: ["email", "profile"], }, ], oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", authorizationEndpointQueryParams: { // optional someKey1: "value1", someKey2: null, }, userInfoMap: { fromIdTokenPayload: { userId: "id", email: "email", emailVerified: "email_verified", }, }, }); if (resp.createdNew) { // custom provider added to tenant } else { // existing custom provider config overwritten for tenant } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "..." resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "custom", Name: "Custom provider", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", Scope: []string{"profile", "email"}, }, }, OIDCDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", AuthorizationEndpointQueryParams: map[string]interface{}{ // optional "someKey1": "value1", "someKey2": nil, }, UserInfoMap: tpmodels.TypeUserInfoMap{ FromIdTokenPayload: struct{UserId string "json:\"userId,omitempty\""; Email string "json:\"email,omitempty\""; EmailVerified string "json:\"emailVerified,omitempty\""} { UserId: "id", Email: "email", EmailVerified: "email_verified", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Custom provider added to tenant } else { // Existing custom provider config overwritten for tenant } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields async def some_func(): tenant_id = "..." result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], oidc_discovery_endpoint="https://example.com/.well-known/openid-configuration", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, user_info_map=UserInfoMap( from_user_info_api=UserFields(), from_id_token_payload=UserFields( user_id="id", email="email", email_verified="email_verified", ), ), )) if result.status != "OK": print("handle error") elif result.created_new: print("Custom provider added to tenant") else: print("Existing custom provider config overwritten for tenant") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "custom", "name": "Custom provider", "clients": [{ "clientId": "...", "clientSecret": "...", "scope": ["email", "profile"] }], "oidcDiscoveryEndpoint": "https://example.com/.well-known/openid-configuration", "authorizationEndpointQueryParams": { "someKey1": "value1", "someKey2": "value2" }, "userInfoMap": { "fromIdTokenPayload": { "userId": "id", "email": "email", "emailVerified": "email_verified" } } } }' ``` You can see all the options in the [CDI documentation](https://supertokens.com/docs/references/cdi). | Field | Description | |-------|-------------| | `tenantId` | Unique ID that identifies the tenant. If not specified, defaults to `"public"` | | `thirdPartyId`, `name`, `clients` | Configuration values similar to OAuth endpoints method | | `userInfoMap.fromIdTokenPayload` | Maps user info from the ID token payload | | `userInfoMap.fromUserInfoAPI` | Optional mapping from user info API. You can combine it with ID token payload mapping | --- ## Add a user to a tenant When a user creates an account, they receive a `tenantId` to sign up. This means that the user can only log in to that tenant. SuperTokens allows you to assign a user ID to multiple tenants. This is possible as long as that user's email or phone number is unique for that login method, for each of the new tenants. Once associated with multiple tenants, that user can log in to each of the tenants they have access to. For example, if a user signs up with email password login in the `public` tenant with email `user@example.com`, they can join another tenant (`t1` for example). This is possible as long as `t1` does not already have an email password user with the same email (that is `user@example.com`). To associate a user with a tenant, you can call the following API: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import { RecipeUserId } from "supertokens-node"; async function addUserToTenant(recipeUserId: RecipeUserId, tenantId: string) { let resp = await Multitenancy.associateUserToTenant(tenantId, recipeUserId); if (resp.status === "OK") { // User is now associated with tenant } else if (resp.status === "UNKNOWN_USER_ID_ERROR") { // The provided user ID was not one that signed up using one of SuperTokens' auth recipes. } else if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. } else if (resp.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. } else if (resp.status === "ASSOCIATION_NOT_ALLOWED_ERROR") { // This can happen if using account linking along with multi tenancy. One example of when this // happens if if the target tenant has a primary user with the same email / phone numbers // as the current user. } else { // status is THIRD_PARTY_USER_ALREADY_EXISTS_ERROR // This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant. } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" ) func main() { tenantId := "customer1" userID := "user1" resp, err := multitenancy.AssociateUserToTenant(tenantId, userID) if err != nil { // handle error } if resp.OK != nil { // User is now associated with tenant } else if resp.UnknownUserIdError != nil { // The provided user ID was not one that signed up using one of SuperTokens' auth recipes. } else if resp.EmailAlreadyExistsError != nil { // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. } else if resp.PhoneNumberAlreadyExistsError != nil { // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. } else { // status is ThirdPartyUserAlreadyExistsError // This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant. } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import associate_user_to_tenant from supertokens_python.recipe.multitenancy.interfaces import AssociateUserToTenantUnknownUserIdError, AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantNotAllowedError, AssociateUserToTenantOkResult from supertokens_python.types import RecipeUserId async def some_func(): response = await associate_user_to_tenant("customer1", RecipeUserId("user1")) if isinstance(response, AssociateUserToTenantOkResult): print("User is now associated with tenant") elif isinstance(response, AssociateUserToTenantUnknownUserIdError): print("The provided user ID was not one that signed up using one of SuperTokens' auth recipes.") elif isinstance(response, AssociateUserToTenantEmailAlreadyExistsError): print("This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method.") elif isinstance(response, AssociateUserToTenantPhoneNumberAlreadyExistsError): print("This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login.") elif isinstance(response, AssociateUserToTenantNotAllowedError): # This can happen if using account linking along with multi tenancy. One example of when this # happens if if the target tenant has a primary user with the same email / phone numbers # as the current user. print("The new tenant does not allow associating users to it.") else: print("status is ThirdPartyUserAlreadyExistsError") print("This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant.") ``` ```python from supertokens_python.recipe.multitenancy.syncio import associate_user_to_tenant from supertokens_python.recipe.multitenancy.interfaces import AssociateUserToTenantUnknownUserIdError, AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantNotAllowedError, AssociateUserToTenantOkResult from supertokens_python.types import RecipeUserId response = associate_user_to_tenant("customer1", RecipeUserId("user1")) if isinstance(response, AssociateUserToTenantOkResult): print("User is now associated with tenant") elif isinstance(response, AssociateUserToTenantUnknownUserIdError): print("The provided user ID was not one that signed up using one of SuperTokens' auth recipes.") elif isinstance(response, AssociateUserToTenantEmailAlreadyExistsError): print("This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method.") elif isinstance(response, AssociateUserToTenantPhoneNumberAlreadyExistsError): print("This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login.") elif isinstance(response, AssociateUserToTenantNotAllowedError): # This can happen if using account linking along with multi tenancy. One example of when this # happens if if the target tenant has a primary user with the same email / phone numbers # as the current user. print("The new tenant does not allow associating users to it.") else: print("status is ThirdPartyUserAlreadyExistsError") print("This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant.") ``` ```bash curl --location --request POST '//recipe/multitenancy/tenant/user \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "recipeUserId": "..." }' ``` In the above code, `recipeUserId` is associated with the tenant with ID `TENANT_ID`. The output of the above API has the following `status` response: - `"OK"`: User association with tenant was successful - `"UNKNOWN_USER_ID_ERROR"`: The provided user ID was not one that signed up using one of SuperTokens' auth recipes. - `"EMAIL_ALREADY_EXISTS_ERROR"`: This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. - `"PHONE_NUMBER_ALREADY_EXISTS_ERROR"`: This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. - `"THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"`: This means that the input user had already previously signed in with the same third-party provider (for example, Google) for the new tenant. --- ## Remove a user from a tenant You can even remove a user's access from a tenant using the API call shown below. In fact, you can remove a user from all tenants that they have access to, and the user and their metadata remain in the system. However, they cannot log in to any tenant. To remove a user from a tenant, call the following API: ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; import { RecipeUserId } from "supertokens-node"; async function removeUserFromTeannt(recipeUserId: RecipeUserId, tenantId: string) { let resp = await Multitenancy.disassociateUserFromTenant(tenantId, recipeUserId); if (resp.wasAssociated) { // User was removed from tenant } else { // User was never a part of the tenant anyway } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" ) func main() { tenantId := "customer1" userID := "user1" resp, err := multitenancy.DisassociateUserFromTenant(tenantId, userID) if err != nil { // handle error } if resp.OK.WasAssociated { // User was removed from tenant } else { // User was never a part of the tenant anyway } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import disassociate_user_from_tenant from supertokens_python.types import RecipeUserId async def some_func(): response = await disassociate_user_from_tenant("customer1", RecipeUserId("user1")) if response.was_associated: print("User was removed from tenant") else: print("User was never a part of the tenant anyway") ``` ```python from supertokens_python.recipe.multitenancy.syncio import disassociate_user_from_tenant from supertokens_python.types import RecipeUserId def some_func(): response = disassociate_user_from_tenant("customer1", RecipeUserId("user1")) if response.was_associated: print("User was removed from tenant") else: print("User was never a part of the tenant anyway") ``` ```bash curl --location --request POST '//recipe/multitenancy/tenant/user/remove \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "recipeUserId": "..." }' ``` :::note[- Users can only share access across tenants and not across apps.] - If your app has two tenants, that are in different database locations, then you cannot share users between them. ::: ## See also --- # SAML Source: https://supertokens.com/docs/authentication/enterprise/saml ## Overview The following guide shows you how to configure SAML with your SuperTokens integration. `SAML`, or Security Assertion Markup Language, is an open protocol that exchanges information between the authentication server and the client application. ### How does it work? Your SAML identity provider (IdP) has a metadata file (`.xml`) that you or your end users need to upload to the SAML service provider (SP). The `.xml` metadata file contains (amongst other things): - A unique entity ID that identifies the IdP. It is an identifier, not a secret, and may be shared as part of SAML metadata. - A public certificate that verifies the signature attached to the incoming `SAML` response. This ensures the response is coming from the expected Identity Provider. - Information about where to redirect the end user to when they click on the login button in your application. This URL is to a website controlled by the `SAML` provider and asks the end user for their credentials. ## Before you start :::warning[Use a current, patched SuperTokens Core] SAML requires Core `12.0` or later. The feature is available in the Node.js and Python SDKs while Go is not currently supported. ::: ## Steps ### 1. Get the SAML metadata from your identity provider Before configuring SuperTokens, you need to obtain the SAML metadata XML from your identity provider (IdP). This is typically available in your IdP's admin console as a downloadable XML file or a metadata URL. Common locations for metadata: - **Azure AD**: Enterprise Applications > Your App > Single sign-on > Federation Metadata XML - **Okta**: Applications > Your App > Sign On > SAML Metadata - **Google Workspace**: Apps > Web and mobile apps > Your App > Download metadata ### 2. Initialize the SAML recipe in the backend SDK ```typescript import SuperTokens from "supertokens-node"; import Saml from "supertokens-node/recipe/saml"; SuperTokens.init({ supertokens: { connectionURI: "", apiKey: "", }, appInfo: { appName: "App name", apiDomain: "", websiteDomain: "", }, recipeList: [ // other recipes Saml.init(), ], }); ``` ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import saml init( supertokens_config=SupertokensConfig( connection_uri="", api_key="", ), app_info=InputAppInfo( app_name="App name", api_domain="", website_domain="", ), framework="fastapi", recipe_list=[ # Other recipes saml.init(), ], ) ``` ### 3. Create a new SAML client Use the metadata XML obtained in step 1 to create a SAML client. The `redirectURIs` should point to your application's callback URL where users will be redirected after authentication. :::info[This step assumes that you previously have created a SuperTokens tenant.] If you have not, please follow the [initial setup guide](/authentication/enterprise/initial-setup). ::: ```typescript import Saml from "supertokens-node/recipe/saml"; async function createSamlClient() { const result = await Saml.createOrUpdateClient({ tenantId: "", clientId: "", clientSecret: "", redirectURIs: ["https://your-app.com/auth/callback"], defaultRedirectURI: "https://your-app.com/auth/callback", metadataXML: "", allowIDPInitiatedLogin: true, enableRequestSigning: true, }); if (result.status === "OK") { // Save the clientId for use in the ThirdParty provider configuration console.log("Client ID:", result.clientId); } } ``` ```python from supertokens_python.recipe.saml.asyncio import create_or_update_client async def create_saml_client(): result = await create_or_update_client( tenant_id="", client_id="", client_secret="", redirect_uris=["https://your-app.com/auth/callback"], default_redirect_uri="https://your-app.com/auth/callback", metadata_xml="", allow_idp_initiated_login=True, enable_request_signing=True, ) if result.status == "OK": # Save the client ID for use in the ThirdParty provider configuration print("Client ID:", result.client.client_id) ``` | Name | Type | Description | Required | | ------------------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | | `tenantId` | `string` | The unique identifier of the tenant for which the SAML client is being created or updated. | Yes | | `clientId` | `string` | The unique identifier for the SAML client. If provided, updates the existing client; if omitted, creates a new client. | No | | `clientSecret` | `string` | The secret key associated with the SAML client for authentication purposes. | No | | `redirectURIs` | `string[]` | An array of URIs where the user agent should be redirected after successful authentication. | Yes | | `defaultRedirectURI` | `string` | The default URI to redirect to if no specific redirect URI is specified. | Yes | | `metadataXML` | `string` | The SAML metadata XML string containing configuration details for the Identity Provider. | Yes | | `allowIDPInitiatedLogin` | `boolean` | A flag indicating whether login requests initiated by the Identity Provider are allowed. | No | | `enableRequestSigning` | `boolean` | A flag indicating whether SAML requests should be digitally signed for security. | No | | `userContext` | `Record` | An optional object containing additional context or metadata for the operation. | No | ### 4. Configure your identity provider After creating the SAML client, you need to configure your identity provider with your application's service provider (SP) details. On the IdP side, configure the following properties: - **Entity ID**: Should match the `saml_sp_entity_id` value used in your [tenant configuration](/authentication/enterprise/manage-tenants#update-a-tenant). The default value is `https://saml.supertokens.com`. - **ACS URL** (Assertion Consumer Service URL): `/auth/`<TENANT_ID>`/saml/callback` ### 5. Add the ThirdParty provider Update your SuperTokens initialization to include the ThirdParty recipe with your SAML provider. The `thirdPartyId` must start with `saml-` followed by your custom identifier. ```typescript import SuperTokens from "supertokens-node"; import Saml from "supertokens-node/recipe/saml"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ supertokens: { connectionURI: "", apiKey: "", }, appInfo: { appName: "App name", apiDomain: "", websiteDomain: "", }, recipeList: [ Saml.init(), ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { // Name that will be shown on the login page name: "Azure SAML", // Must start with "saml-" thirdPartyId: "saml-azure", clients: [ { // The clientId from step 3 clientId: "", }, ], }, }, ], }, }), ], }); ``` ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import saml, thirdparty from supertokens_python.recipe.thirdparty import ( ProviderClientConfig, ProviderConfig, ProviderInput, SignInAndUpFeature, ) init( supertokens_config=SupertokensConfig( connection_uri="", api_key="", ), app_info=InputAppInfo( app_name="App name", api_domain="", website_domain="", ), framework="fastapi", recipe_list=[ saml.init(), thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( # Must start with "saml-" third_party_id="saml-azure", # Name shown on the login page name="Azure SAML", clients=[ ProviderClientConfig( # The client ID from step 3 client_id="", ) ], ) ) ] ) ), ], ) ``` ### 6. Test rejection paths before production Use an isolated test tenant and test IdP to verify that invalid SAML responses fail closed. At minimum, confirm that authentication is rejected for: - A response that has neither a valid response signature nor a valid signature on every assertion, including an assertion modified after signing and a response containing a duplicate or wrapping assertion. - An assertion with a future `NotBefore` value or an expired `NotOnOrAfter` value. Core `12.1.1` is not expected to reject a mismatched `Response.Destination`, and its audience validation is insufficient for responses containing multiple assertions. If a trusted upstream validator supplies the missing controls, test destination and per-assertion audience rejection at that layer before production. Do not disable signature or time-condition validation to make negative tests pass. Keep failure details in server-side logs and return a generic authentication error to the browser. ## See also --- # Implement subdomain login Source: https://supertokens.com/docs/authentication/enterprise/subdomain-login ## Overview This guide shows you how to authenticate users through different subdomains. The authentication method displayed on each page varies based on the tenant configuration. :::note[Throughout this page, assume that a tenant's ID matches its subdomain. If the subdomain assigned to a tenant is `customer1.example.com`, then its `tenantId` is `customer1`.] An example app for this setup with the **pre-built UI** is available on [the GitHub example directory](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-one-login-per-subdomain). The app is setup to have three tenants: - `tenant1.example.com`: Login with `emailpassword` + Google sign in - `tenant2.example.com`: Login with `emailPassword` - `tenant3.example.com`: Login with passwordless + GitHub sign in ::: ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). Your application also needs you to create the tenants it requires. View the [previous tutorial](/authentication/enterprise/initial-setup) for more information on how to do this. ## Steps ### 1. Change the CORS settings and `websiteDomain` :::warning You have to [create tenants](/authentication/enterprise/initial-setup) before you can complete this step. ::: #### 1.1 CORS setup For browsers to make requests to the backend, configure backend CORS with the exact allowed origins. For example, if the frontend uses `https://customer1.example.com`, allow that full origin. If tenants are dynamic, validate the request's full `Origin` value against an anchored pattern that permits only your intended HTTPS subdomains. #### 1.2 `websiteDomain` setup Set the `websiteDomain` to `window.location.origin` in the frontend SDK initialization step. On the backend, update `websiteDomain` to the main domain (`example.com` if your subdomains are `sub.example.com`). Then override the `sendEmail` functions to change the domain of the link dynamically based on the tenant ID supplied to the `sendEmail` function. See the Email Delivery section in the docs for how to override the `sendEmail` function. ### 2. Load login methods dynamically on the frontend based on the `tenantId` Modify `SuperTokens.init` as follows: 1. Set `usesDynamicLoginMethods` to `true`. This tells the frontend SDK that the login page relies on the tenant ID and must fetch the tenant configuration from the backend before showing any login UI. 2. Initialize the `Multitenancy` recipe and provide the `getTenantId` configuration function. ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import Session from "supertokens-auth-react/recipe/session"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; SuperTokens.init({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, usesDynamicLoginMethods: true, recipeList: [ // Other recipes.. Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the subdomain as the tenant ID return window.location.host.split(".")[0]; }, }; }, }, }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, usesDynamicLoginMethods: true, recipeList: [ // Other recipes... supertokensUISession.init(), supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the subdomain as the tenant ID return window.location.host.split(".")[0]; }, }; }, }, }), ], }); ``` You can fetch the user's login methods based on their tenant ID, which you can derive from the current subdomain, as shown below. ```tsx import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId, }); if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find((i) => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```tsx import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { const loginMethods = await Multitenancy.getLoginMethods({ tenantId, }); if (loginMethods.firstFactors.includes("thirdparty")) { const providers = loginMethods.thirdParty.providers; if (providers.find((i) => i.id === "active-directory")) { // render sign in with Active Directory button } else { // more checks for other providers } } else { // thirdparty login is disabled for the tenant } } ``` ```bash curl --location --request GET '/auth/loginmethods' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: The `recipes` field contains information about which login methods are active along with the list of third party providers configured for this tenant. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. You also need to initialize the multitenancy recipe with the following callback. You can get the tenant ID from the subdomain as shown below. After you have shown the login methods and the user tries to sign in, follow all the steps for mobile app login similar to the [social login steps](/authentication/social/initial-setup#2-add-the-login-ui). When calling the sign in up API, also pass in the `tenantId` in the request path. An example of this appears below: ```tsx import SuperTokens from "supertokens-web-js"; import Multitenancy from "supertokens-web-js/recipe/multitenancy"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ Multitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the subdomain as the tenant ID return window.location.host.split(".")[0]; }, }; }, }, }), // other recipes... ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokens.init({ appInfo: { appName: "...", apiDomain: "...", }, recipeList: [ supertokensMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { // We treat the subdomain as the tenant ID return window.location.host.split(".")[0]; }, }; }, }, }), // other recipes... ], }); ``` ```bash curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "...", "clientType": "...", "oAuthTokens": { "access_token": "...", "id_token": "..." }, }' ``` ### 3. Restrict session use by subdomain Restrict the subdomains on which a tenant's sessions can be used. To do this, configure the SDK with the domains for each tenant ID. ```tsx import SuperTokens from "supertokens-node"; import Multitenancy from "supertokens-node/recipe/multitenancy"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Multitenancy.init({ getAllowedDomainsForTenantId: async (tenantId, userContext) => { // query your db to get the allowed domain for the input tenantId // or you can make the tenantId equal to the subdomain itself return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"]; }, }), // other recipes... ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ multitenancy.Init(&multitenancymodels.TypeInput{ GetAllowedDomainsForTenantId: func(tenantId string, userContext supertokens.UserContext) ([]string, error) { // query your db to get the allowed domain for the input tenantId // or you can make the tenantId equal to the subdomain itself return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil }, }), }, }) } ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multitenancy from typing import Dict, Any, List async def get_allowed_domains_for_tenant_id(tenant_id: str, user_context: Dict[str, Any]) -> List[str]: return [tenant_id + ".myapp.com", "myapp.com", "www.myapp.com"] init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="django", # Change this to "flask" or "fastapi" if you are using Flask or FastAPI recipe_list=[ multitenancy.init( get_allowed_domains_for_tenant_id=get_allowed_domains_for_tenant_id ) ], ) ``` The configuration above tells SuperTokens to add the returned domains to the user's session claims when they sign in. The SDK can access the claim on the frontend and backend to restrict where the session is used. :::warning[Domain checks are not tenant authorization] `AllowedDomainsClaim` and `hasAccessToCurrentDomain` restrict session use by hostname. They do not prove that the user belongs to an organization, enforce CORS or allowed browser origins, authorize access to business data, or provide complete tenant isolation. After authentication, verify the user's tenant membership. On every business-data access, the backend must derive the tenant from trusted session data and enforce application-level tenant authorization. Do not trust a tenant ID, hostname, or claim supplied by the browser as authorization. ::: ### 4. Share sessions across subdomains (optional) If users need the same session across multiple subdomains, update the configuration. Set the [`sessionTokenFrontendDomain` value ](/post-authentication/session-management/share-session-across-sub-domains) in the `Session` recipe to enable this behavior. If the subdomain and main website domain have different backends on different subdomains, you can also enable [sharing of sessions across API domains](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). :::note[Even if they visit the main domain (logged in via `a.example.com`, and visit `example.com`), the frontend app there can detect if the user has a session or not.] This only shows that a session exists. The domain validator below restricts where that session is used; application-level tenant authorization is still required. ::: ### 5. Limit session use to the tenant's subdomain Use [session claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) on the frontend to restrict session use by subdomain. Before proceeding, ensure that you have defined the `GetAllowedDomainsForTenantId` function mentioned above. This adds the list of allowed domains into the user's access token payload. On the frontend, check whether the current subdomain is in the session's allowed domains. If it is not, redirect the user to the correct subdomain. Use the `hasAccessToCurrentDomain` session validator from the multitenancy recipe. Make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import React from "react"; import Session from "supertokens-auth-react/recipe/session"; import { AllowedDomainsClaim } from "supertokens-auth-react/recipe/multitenancy"; Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensUISession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import Session from "supertokens-web-js/recipe/session"; import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. ```tsx import Session from "supertokens-web-js/recipe/session"; import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await Session.getClaimValue({ claim: AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokensSession.init({ override: { functions: (oI) => ({ ...oI, getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ ...claimValidatorsAddedByOtherRecipes, { ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), onFailureRedirection: async () => { let claimValue = await supertokensSession.getClaimValue({ claim: supertokensMultitenancy.AllowedDomainsClaim, }); return "https://" + claimValue![0]; }, }, ], }), }, }); ``` Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes#check-the-claims-of-a-session), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. --- ## See also --- # Tenant discovery Source: https://supertokens.com/docs/authentication/enterprise/tenant-discovery ## Overview This tutorial shows you how to add tenant discovery functionality to your SuperTokens authentication flows. The guide makes use of the plugins functionality which automatically discovers and routes users to the appropriate tenants based on their email domains. ## How it works The plugin extracts the domain from user email addresses to infer the tenant ID. For example, `user@company.com` would be routed to the `company` tenant. The system includes built-in protection against popular email providers and falls back to the `public` tenant when appropriate. ## Before you start The tenant discovery plugin supports only the React and Node.js SDKs. Support for other platforms is under active development. Besides initializing the plugin, you also have to configure multi-tenancy in your SuperTokens setup. ## Steps ### 1. Initialize the backend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/tenant-discovery-nodejs ``` #### 1.2 Update your backend SDK configuration ```typescript import SuperTokens from "supertokens-node"; import TenantDiscoveryPlugin from "@supertokens-plugins/tenant-discovery-nodejs"; SuperTokens.init({ appInfo: { appName: "My app", apiDomain: "https://api.example.com", }, recipeList: [ // your other recipes ], experimental: { plugins: [ TenantDiscoveryPlugin.init({ enableTenantListAPI: false, // Optional: defaults to false }), ], }, }); ``` ### 2. Initialize the frontend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/tenant-discovery-react ``` #### 2.2 Update your frontend SDK configuration ```typescript import SuperTokens from "supertokens-auth-react"; import TenantDiscoveryPlugin from "@supertokens-plugins/tenant-discovery-react"; SuperTokens.init({ appInfo: { appName: "My app", apiDomain: "https://api.example.com", websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ TenantDiscoveryPlugin.init({ showTenantSelector: true, // Optional: defaults to true extractTenantIdFromDomain: true, // Optional: defaults to true }), ], }, }); ``` ### 3. Use tenant discovery With this configuration, when a user tries to sign in, the system automatically determines their tenant based on the email domain. Hence, you don't need to change anything else to make it work. :::warning[Tenant discovery is not tenant authorization] Tenant discovery routes an authentication attempt to an inferred tenant. An email domain does not prove that the user owns or belongs to an organization. Discovery does not enforce CORS or allowed browser origins, authorize access to business data, or provide complete tenant isolation. After authentication, verify the user's membership in the discovered tenant. On every business-data access, the backend must derive the tenant from trusted session data and enforce application-level tenant authorization. Do not use the submitted email domain or a browser-supplied tenant ID as authorization. ::: Email authentication form If you want to customize the user interface experience the plugin also provides other options. #### Tenant selection interface You can use the tenant selection interface accessible at `/tenant-discovery/select`. This page displays all available tenants and allows users to choose their organization before proceeding with authentication. :::info[Keep in mind that you also need to enable the `tenant list` endpoint in your backend plugin configuration.] ::: ## Customization ### Block emails from specific tenants You can override the default tenant assignment logic to prevent certain emails from accessing specific tenants: ```typescript import TenantDiscoveryPlugin from "@supertokens-plugins/tenant-discovery-nodejs"; TenantDiscoveryPlugin.init({ enableTenantListAPI: false, override: (originalImplementation) => ({ ...originalImplementation, isTenantAllowedForEmail: (email: string, tenantId: string) => { // Prevent routing to public tenant return tenantId !== "public"; }, }), }); ``` ### Add custom domain restrictions Extend the list of restricted domains that should always use the `public` tenant: ```typescript check=false reason="This example omits surrounding application and SuperTokens configuration." TenantDiscoveryPlugin.init({ override: (originalImplementation) => ({ ...originalImplementation, isRestrictedEmailDomain: (emailDomain: string) => { return originalImplementation.isRestrictedEmailDomain(emailDomain) || emailDomain === "example.com"; }, }), }); ``` ### Implement a custom user interface To create a custom tenant discovery interface, use the `usePluginContext` hook: ```tsx import { useState } from "react"; import { usePluginContext } from "@supertokens-plugins/tenant-discovery-react"; function CustomTenantDiscovery() { const { api, functions } = usePluginContext(); const [email, setEmail] = useState(""); const [tenants, setTenants] = useState>([]); const handleEmailSubmit = async () => { const result = await api.tenantIdFromEmail(email); if (result.status === "OK") { functions.setEmailId(email); functions.setTenantId(result.tenant); } }; const loadTenants = async () => { const response = await api.fetchTenants(); if (response.status === "OK") { setTenants(response.tenants); } }; return (

Enter your email to find your organization

setEmail(e.target.value)} placeholder="user@company.com" />

Or choose from available organizations:

{tenants.map((tenant) => ( ))}
); } ``` ## Next steps Besides tenant discovery, you can also explore other enterprise authentication features: --- # Tenant management Source: https://supertokens.com/docs/authentication/enterprise/tenant-management-plugin ## Overview This tutorial shows you how to add comprehensive tenant management functionality to your SuperTokens authentication flows. The guide makes use of the plugins functionality which provides complete multi-tenancy management including tenant creation, user roles, invitations, join requests, and seamless tenant switching. ## Before you start The tenant management plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. ## Steps ### 1. Initialize the backend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/tenants-nodejs ``` #### 1.2 Update your backend SDK configuration ```typescript import SuperTokens from "supertokens-node"; import TenantsPlugin from "@supertokens-plugins/tenants-nodejs"; SuperTokens.init({ appInfo: { appName: "My app", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [TenantsPlugin.init()], }, }); ``` ##### Configuration options The plugin supports the following configuration options: | Option | Type | Default | Description | |--------|------|---------|-------------| | `requireNonPublicTenantAssociation` | `boolean` | `false` | Require users to associate with at least one non-public tenant | | `requireTenantCreationRequestApproval` | `boolean` | `true` | Whether tenant creation requires admin approval | | `enableTenantListAPI` | `boolean` | `false` | Enable API to list all tenants | | `createRolesOnInit` | `boolean` | `true` | Auto-create required roles on init | ### 2. Initialize the frontend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/tenants-react ``` #### 2.2 Update your frontend SDK configuration ```typescript import SuperTokens from "supertokens-auth-react"; import ProfileBasePlugin from "@supertokens-plugins/profile-base-react"; import TenantsPlugin from "@supertokens-plugins/tenants-react"; SuperTokens.init({ appInfo: { appName: "My app", apiDomain: "https://api.example.com", websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ProfileBasePlugin.init(), TenantsPlugin.init()], }, }); ``` ##### Configuration options The plugin supports the following configuration options: | Option | Type | Default | Description | |--------|------|---------|-------------| | `requireTenantCreation` | `boolean` | `false` | Whether users must create a tenant before accessing the app | | `redirectToUrlOnJoiningTenant` | `string` | `/` | The path to which users are redirected after joining a tenant | ### 3. Test the implementation With this configuration, users can access comprehensive tenant management features through the profile interface. Make sure that you have a user with the required permissions and then access`/user/tenants/create` to create a new tenant. ## Customization ### Roles and permissions The plugin automatically creates the following roles and permissions: #### Default roles | Role | Description | Permissions | |------|-------------|-------------| | `tenant-admin` | Full administrative access within the tenant | All tenant permissions | | `tenant-member` | Basic member access within the tenant | `tenant-access` | | `app-admin` | Global application administrator | All permissions across all tenants | #### Available permissions | Permission | Description | |------------|-------------| | `tenant-access` | Basic access to tenant | | `list-users` | View list of users in tenant | | `manage-invitations` | Create and manage tenant invitations | | `manage-join-requests` | Approve or reject join requests | | `change-user-roles` | Modify user roles within tenant | | `remove-users` | Remove users from tenant | ### Email delivery configuration Configure custom email delivery for tenant-related notifications: ```typescript check=false reason="This example omits the surrounding SuperTokens application configuration." import SuperTokens from "supertokens-node"; import { PluginSMTPService } from "@supertokens-plugins/tenants-nodejs"; import TenantsPlugin from "@supertokens-plugins/tenants-nodejs"; SuperTokens.init({ // ... other config experimental: { plugins: [ TenantsPlugin.init({ emailDelivery: { service: new PluginSMTPService({ smtpSettings: { host: "smtp.example.com", port: 587, from: { name: "Your App", email: "noreply@example.com", }, secure: false, authUsername: "username", password: "password", }, }), }, }), ], }, }); ``` ### Custom implementation override You can override default behaviors by providing custom implementations: ```typescript check=false reason="This example omits surrounding application and SuperTokens configuration." TenantsPlugin.init({ override: { functions: (originalImplementation) => ({ ...originalImplementation, isAllowedToCreateTenant: async (session) => { // Custom logic to determine if user can create tenant const userId = session.getUserId(); // Add your custom logic here return true; }, canApproveJoinRequest: async (targetUser, tenantId, session) => { // Custom logic for approving join requests return true; }, }), }, }); ``` ### Custom user interface To create your own UI you can use the `usePluginContext` hook. It exposes an interface which you can use to interface with the endpoints exposed by the backend plugin. ```tsx import { useState } from "react"; import { usePluginContext } from "@supertokens-plugins/tenants-react/dist/plugin"; function CustomTenantComponent() { const { api, t } = usePluginContext(); const [tenants, setTenants] = useState>([]); const handleFetchTenants = async () => { const result = await api.fetchTenants(); if (result.status === "OK") { setTenants(result.tenants); } }; const handleCreateTenant = async (name: string) => { const result = await api.createTenant({ name }); if (result.status === "OK") { console.log("Tenant created successfully"); } }; const handleSwitchTenant = async (tenantId: string) => { const result = await api.switchTenant(tenantId); if (result.status === "OK") { console.log("Switched to tenant successfully"); } }; return (

{t("PL_TB_CREATE_TENANT_LABEL")}

{tenants.map((tenant) => (
{tenant.name} ({tenant.role})
))}
); } ``` ### Custom page components You can customize the default pages by providing your own components: ```typescript check=false reason="This example depends on local application modules." import TenantsPlugin from "@supertokens-plugins/tenants-react"; import { CustomSelectTenant, CustomTenantManagement } from "./your-custom-components"; SuperTokens.init({ // ... other config experimental: { plugins: [ TenantsPlugin.init({ override: (oI) => ({ ...oI, pages: (originalPages) => ({ ...originalPages, selectTenant: CustomSelectTenant, tenantManagement: CustomTenantManagement, }), }), }), ], }, }); ``` ## Next steps Besides tenant management, you can also explore other enterprise authentication features: --- # Client Credentials Flow Source: https://supertokens.com/docs/authentication/m2m/client-credentials ## Overview In the **Client Credentials Flow** the authentication sequence works in the following way: 1. **Service A uses credentials to get an OAuth2 Access Token** 2. **Authorization Service(/authentication/unified-login/oauth2-basics#authorization-server) returns the OAuth2 Access Token** 3. **Service A uses the OAuth2 Access Token to communicate with Service B** 4. **Service B validates the OAuth2 Access Token** 5. **If the token is valid Service B returns the requested resource** Machine to Machine Authentication Before going into the actual instructions, start by imagining a real life example that you can reference along the way. This makes it easier to understand what is happening. We are going to configure authentication for the following setup: - A **Calendar Service** that exposes these actions: `event.view`, `event.create`, `event.update` and `event.delete` - A **File Service** that exposes these actions: `file.view`, `file.create`, `file.update` and `file.delete` - A **Task Service** that interacts with the **Calendar Service** and the **File Service** in the process of scheduling a task The aim is to allow the **Task Service** to perform an authenticated action on the **Calendar Service**. Proceed to the actual steps. ## Before you start ## Steps ### 1. Enable the OAuth2 features from the Dashboard You first have to enable **M2M Authentication** from the [**SuperTokens.com Dashboard**](https://supertokens.com/dashboard). Select the relevant **Managed** deployment, open **Features**, and enable **M2M Authentication**. Changes are saved automatically. You should be able to use the OAuth2 recipes in your applications. ### 2. Create the OAuth2 Clients For each of your **`microservices`** you need to create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). This can occur by directly calling the **SuperTokens Core** API. For manual curl testing, provision a config through your secret-management or deployment system, restrict it to the service account with mode `0600`, and do not commit it: ```text header = "api-key: " ``` The cURL example refers to this file as ``. This keeps the API key out of shell history and process arguments. Disable shell tracing and curl verbose or trace output, and ensure HTTP, process, and error logs do not record request headers, config contents, or the API key. See the [Create OAuth2 client API reference](/references/cdi/oauth2provider-recipe/createoauth2client) for the complete request schema and response details. ", clientName: "", grantTypes: ["client_credentials"], scope: " ", audience: [""], }} /> :::info[Custom Example] To create a client for the **Task Service**, use the following attributes: ```json { "clientId": "task-service", "clientName": "Task Service", "grantTypes": ["client_credentials"], "scope": "event.view event.create event.update event.delete file.view file.create file.update file.delete", "audience": ["event", "file"] } ``` This allows the **Task Service** to perform all types of actions against both of the other services as long as it has a valid **OAuth2 Access Token**. ::: :::note[Retry client provisioning safely] Client creation has no documented duplicate-request key. Use a stable `clientId`, serialize provisioning for that client, and after a timeout query the client by ID before retrying. Do not blindly retry an uncertain `POST` response. ::: :::warning[Protect the client credentials] Store the client ID and secret in a secret manager. The Core persists the secret encrypted at rest, and callers with the Core API key can retrieve it. Treat both the API key and client secret as sensitive credentials. ::: ### 3. Set Up your Authorization Service The Node.js and Python SDKs automatically initialize the **OAuth2Provider** recipe when it is absent. Add it explicitly to your [**Authorization Server**](/authentication/unified-login/oauth2-basics#authorization-server) configuration when you need recipe overrides or want to make the dependency visible. ```tsx import supertokens from "supertokens-node"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [OAuth2Provider.init()], }); ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init() ], ) ``` ### 4. Generate access tokens You can directly call the [**Authorization Server**](/authentication/unified-login/oauth2-basics#authorization-server) to generate Access Tokens. See the [Exchange OAuth grant API reference](/references/fdi/oauth2provider-recipe/oauthtokenpost) for response schemas and error details. The cURL example remains authoritative for this request because the current FDI specification does not model the form-encoded request body or HTTP Basic client authentication. Keep the client secret out of command arguments and shell history. For manual testing, provision a curl config through your secret-management or deployment system, restrict it to the service account with mode `0600`, and do not commit it: ```text user = ":" ``` Then reference the protected config by path: ```bash curl -X POST '/auth/oauth/token' \ --config '' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'scope=' \ --data-urlencode 'audience=' ``` For production, load the secret from a secret manager in your application client. Disable shell tracing and ensure HTTP, process, and error logs do not record authorization headers, curl configuration contents, or client secrets. You should limit the scopes that you are requesting to the ones necessary to perform the desired action. :::info[Custom Example] If the **Task Service** wants to create an event on the **Calendar Service**, a token with the following attributes needs generation: ```bash curl -X POST '/auth/oauth/token' \ --config '' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'scope=event.create' \ --data-urlencode 'audience=event' ``` ::: The **Authorization Server** returns a response that looks like this: ```json { "access_token": "", "expires_in": 3600, "token_type": "bearer", "scope": "event.create" } ``` Save the `access_token` in memory for use in the next step. The `expires_in` field indicates how long the token is valid for. Each service that you communicate with needs its own token. With an **OAuth2 Access Token**, it can facilitate communication with the other services. Keep in mind to generate a new one when it expires. ### 5. Verify an OAuth2 Access Token Use the released SuperTokens backend SDK validator instead of implementing JWT validation yourself. It validates the signature, expiration, and `stt=1` token type. Pass requirements for the intended audience, client, and every scope needed by the operation. Also compare the token issuer with your Authorization Server's issuer. ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; async function validateClientCredentialsToken(token: string): Promise { try { const result = await OAuth2Provider.validateOAuth2AccessToken(token, { audience: "", clientId: "", scopes: [""], }); return result.payload.iss === "/auth"; } catch { return false; } } ``` ```python from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token def validate_client_credentials_token(token: str) -> bool: try: result = validate_oauth2_access_token( token=token, requirements=OAuth2TokenValidationRequirements( audience="", client_id="", scopes=[""], ), ) return result.payload.get("iss") == "/auth" except Exception: return False ``` :::warning[Bearer tokens do not prevent request replay] Token validation authenticates and authorizes a request; it does not make a state-changing operation replay-safe. For create or update APIs, require an application-level unique request key, atomically bind it to the authenticated client, operation, and request-body digest, and return the stored result for an exact retry. Reject reuse with a different payload and use business uniqueness or conditional updates where appropriate. ::: :::info[Custom Example] If the **Task Service** uses the previously generated token to create a calendar event, the **Calendar Service** must require `stt=1`, the `event.create` scope, the `event` audience, the expected Task Service client ID, and the expected Authorization Server issuer. ::: #### Handle both SuperTokens session tokens and OAuth2 access tokens If your Authorization Server is also a Resource Server, a protected route may accept either a SuperTokens session or an OAuth2 access token. Parse the `Authorization` header strictly. Never accept a malformed bearer value, and never ignore a validator's failure or false result. ```tsx import express, { type NextFunction, type Request, type Response } from "express"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import Session from "supertokens-node/recipe/session"; async function verifySessionOrOAuthToken(req: Request, res: Response, next: NextFunction) { const authorization = req.headers.authorization; if (authorization !== undefined) { const separator = authorization.indexOf(" "); const scheme = authorization.slice(0, separator); const token = authorization.slice(separator + 1); if (separator < 1 || scheme.toLowerCase() !== "bearer" || !token) { return res.status(401).json({ message: "Unauthorized" }); } try { const result = await OAuth2Provider.validateOAuth2AccessToken(token, { audience: "", clientId: "", scopes: [""], }); if (result.payload.iss === "/auth") { return next(); } } catch { // The bearer token may be a SuperTokens session access token. } } try { await Session.getSession(req, res); return next(); } catch { return res.status(401).json({ message: "Unauthorized" }); } } const app = express(); app.get("/protected", verifySessionOrOAuthToken, async (_req, res) => { res.json({ message: "Authorized" }); }); ``` ```python from fastapi import HTTPException from fastapi.requests import Request from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token from supertokens_python.recipe.session.syncio import get_session def verify_session_or_oauth_token(request: Request) -> bool: authorization = request.headers.get("authorization") if authorization is not None: scheme, separator, token = authorization.partition(" ") if scheme.lower() != "bearer" or not separator or not token: raise HTTPException(status_code=401, detail="Unauthorized") try: result = validate_oauth2_access_token( token=token, requirements=OAuth2TokenValidationRequirements( audience="", client_id="", scopes=[""], ), ) if result.payload.get("iss") == "/auth": return True except Exception: # The bearer token may be a SuperTokens session access token. pass try: get_session(request) return True except Exception as error: raise HTTPException(status_code=401, detail="Unauthorized") from error ``` --- # Introduction Source: https://supertokens.com/docs/authentication/m2m/introduction Implement SuperTokens machine-to-machine authentication for this repository. Inspect the services, backend SDK, deployment model, and existing secrets configuration first. Prefer the managed-service OAuth2 client-credentials flow when supported; otherwise explain the legacy-flow tradeoff. Configure the OAuth2 provider, clients, token acquisition, service authentication, scopes, and secret storage without committing credentials. Validate token issuance, expiry, invalid credentials, and protected service-to-service requests. ## Overview The recommended way to authenticate microservices with **SuperTokens** is by using the **OAuth2** specification. You have to create an **OAuth2 Provider** and use the **OAuth2 Client Credentials Flow** for authorization. ## Prerequisites Before you can dive deeper in the functionality there are a few things to keep in mind: - The feature is available with the **SuperTokens Managed Service**. It is not included in the **Self-Hosted** version. - You can use it with the `Node.js` or the `Python` backend SDKs. If you do not meet the previous requirements you can use the [legacy flow](/authentication/m2m/legacy-flow). ## Getting started Two separate quickstart guides are available for you to follow. The recommendation is to use the **Client Credentials Flow**. The **Legacy Flow** guide is here for backwards compatibility and it is going to be deprecated in the future. Before you explore a guide, read through the **OAuth2 Basics** page first. It explains concepts used in each tutorial. Go through a quick summary of the OAuth2 specifications to get accustomed with the language used in the quickstart guides. Implement a common authentication service that all your microservices can use. Legacy info --- # Legacy Flow Source: https://supertokens.com/docs/authentication/m2m/legacy-flow ## Overview Use the [OAuth2 Client Credentials Flow](/authentication/m2m/client-credentials) when it is available. It gives each client an identity and uses the standard OAuth2 token and scope model. This legacy flow is a custom bearer-token scheme for deployments that cannot use client credentials. A service with access to the SuperTokens Core JWT API can mint a token containing arbitrary claims. Consequently, a `source` or `sub` claim proves only that a caller with signing access asserted that value; it does not independently prove which service made the request. The flow is: 1. **Service M1 requests a short-lived JWT from SuperTokens Core** 2. **M1 sends the JWT to M2 in the Authorization header** 3. **M2 verifies the signature and every required claim before authorizing the request** :::warning[Security boundary] Anyone who can call the Core JWT API can mint any service identity or permission accepted by this scheme. Restrict Core access with network controls and an API key, store the API key in a secrets manager, and monitor issuance. Multiple Core API keys simplify secret rotation and may help attribute Core requests, but the issued JWT does not identify which API key was used. Multiple keys therefore do not create cryptographic service identities or authorization boundaries. ::: For stronger isolation, use client credentials or separate trust domains. Deploying separate Cores can create separate signing domains, but it adds operational cost and does not turn a shared Core API key into service identity. ## Token policy For every token: - Use a short validity appropriate to the request path. The examples below use five minutes. - Use dynamic signing keys. Dynamic keys rotate every 168 hours (one week) by default unless the Core configuration changes `access_token_dynamic_signing_key_update_interval`. - Require an exact issuer (`iss`), audience (`aud`), subject/service identity (`sub`), source, token type, permissions, and expiration (`exp`) at the receiving service. - Grant only the permissions needed by the target API. Do not treat successful signature verification as authorization. - Fetch keys from JWKS and support key rotation. Do not embed a public key in the application. The JWT recipe defaults to a 100-year validity and a static signing key when those arguments are omitted. Those defaults are unsuitable for bearer credentials. Static keys do not rotate. Always pass a short validity and explicitly select the dynamic signing key as shown below. ## 1. Initialize the JWT recipe ```tsx import supertokens from "supertokens-node"; import jwt from "supertokens-node/recipe/jwt"; supertokens.init({ appInfo: { apiDomain: "https://auth.example.com", appName: "service-auth", websiteDomain: "https://example.com", }, supertokens: { connectionURI: "...", apiKey: "...", }, recipeList: [jwt.init()], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/jwt" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ AppInfo: supertokens.AppInfo{ AppName: "service-auth", WebsiteDomain: "https://example.com", APIDomain: "https://auth.example.com", }, Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "...", APIKey: "...", }, RecipeList: []supertokens.Recipe{ jwt.Init(nil), }, }) } ``` ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import jwt init( app_info=InputAppInfo( app_name="service-auth", api_domain="https://auth.example.com", website_domain="https://example.com", ), supertokens_config=SupertokensConfig( connection_uri="...", api_key="...", ), framework="django", recipe_list=[jwt.init()], ) ``` The `apiDomain`/`api_domain`/`APIDomain` value becomes the JWT issuer domain and must be the domain that serves the JWKS endpoint. If this process initializes no other recipe, `appName` and `websiteDomain` do not affect this flow. ## 2. Create a short-lived JWT Use a fixed schema rather than accepting arbitrary claims from request input. This example identifies `M1`, limits the token to `M2`, and grants one permission. ```tsx import jwt from "supertokens-node/recipe/jwt"; async function createServiceAccessToken(): Promise { const response = await jwt.createJWT( { iss: "https://auth.example.com", aud: "service-m2", sub: "service-m1", source: "microservice", token_type: "service_access", permissions: ["comments:write"], }, 300, false, ); if (response.status !== "OK") { throw new Error("JWT creation failed"); } return response.jwt; } ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/jwt" ) func createServiceAccessToken() (string, error) { validitySeconds := uint64(300) useStaticSigningKey := false response, err := jwt.CreateJWT(map[string]interface{}{ "iss": "https://auth.example.com", "aud": "service-m2", "sub": "service-m1", "source": "microservice", "token_type": "service_access", "permissions": []string{"comments:write"}, }, &validitySeconds, &useStaticSigningKey) if err != nil { return "", err } if response.OK == nil { return "", errors.New("JWT creation failed") } return response.OK.Jwt, nil } ``` ```python from supertokens_python.recipe.jwt import asyncio from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult async def create_service_access_token() -> str: response = await asyncio.create_jwt( { "iss": "https://auth.example.com", "aud": "service-m2", "sub": "service-m1", "source": "microservice", "token_type": "service_access", "permissions": ["comments:write"], }, validity_seconds=300, use_static_signing_key=False, ) if not isinstance(response, CreateJwtOkResult): raise RuntimeError("JWT creation failed") return response.jwt ``` ```python from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult from supertokens_python.recipe.jwt.syncio import create_jwt response = create_jwt( { "iss": "https://auth.example.com", "aud": "service-m2", "sub": "service-m1", "source": "microservice", "token_type": "service_access", "permissions": ["comments:write"], }, validity_seconds=300, use_static_signing_key=False, ) if not isinstance(response, CreateJwtOkResult): raise RuntimeError("JWT creation failed") access_token = response.jwt ``` Prefer the backend SDK. It avoids manually constructing the Core request and keeps the API key out of command-line arguments. If operational tooling must call the released Core API directly, provide the URL and headers through an owner-readable curl config file (`0600`) populated by your secret tooling. Provide the request body over standard input: ```bash curl --config /run/secrets/supertokens-curl.conf --data-binary @- <<'JSON' { "payload": { "iss": "https://auth.example.com", "aud": "service-m2", "sub": "service-m1", "source": "microservice", "token_type": "service_access", "permissions": ["comments:write"] }, "useStaticSigningKey": false, "algorithm": "RS256", "jwksDomain": "https://auth.example.com", "validity": 300 } JSON ``` Configure that file with the `/recipe/jwt` URL, `POST` method, `rid: jwt`, `Content-Type: application/json`, and `api-key` header. Do not put the API key in command-line arguments, shell history, environment dumps, or generated logs. Disable shell tracing such as `set -x` around secret handling, restrict access to the config file, and remove temporary copies immediately after use. Keep the token in memory only as long as needed. Send it as `Authorization: Bearer ` over TLS. Never log the token or place it in a URL, source file, or long-lived configuration value. ## 3. Verify and authorize the JWT The JWKS endpoint is: ```text /jwt/jwks.json ``` With the default API base path, this is `https://auth.example.com/auth/jwt/jwks.json`. Configure a maintained JWT library to fetch and cache this JWKS, honor its cache behavior, and refetch when it encounters an unknown `kid`. Dynamic keys rotate every week by default. Static keys may also appear in JWKS, but they do not rotate and must not be selected or hardcoded for this flow. Do not trust decoded data until the verification library reports success. Verification must: 1. Allow only `RS256`; reject an unexpected or missing `alg` or `kid`. 2. Verify the signature with the JWKS key selected by `kid`. 3. Reject every library error before reading claims. This includes an invalid signature, expired token, malformed token, unknown key, and issuer or audience mismatch. 4. Require `exp` and reject expired tokens. Do not disable expiry verification or add an unbounded clock tolerance. 5. Require exact expected values for `iss`, `aud`, `source`, and `token_type`. 6. Require an approved `sub` service identity and every permission needed by the endpoint. For the example above, `M2` must require: | Claim | Required value | | --- | --- | | `iss` | `https://auth.example.com` | | `aud` | `service-m2` | | `sub` | An approved calling service, such as `service-m1` | | `source` | `microservice` | | `token_type` | `service_access` | | `permissions` | Includes the endpoint's required permission | | `exp` | Present and in the future | Return `401 Unauthorized` when authentication fails. Return `403 Forbidden` when the token is valid but its service or permissions do not authorize the operation. Do not reveal signature, key, or claim-validation details to the caller. ### Idempotent writes and replay A valid bearer token can be replayed until it expires. For non-idempotent writes, require a caller-generated `Idempotency-Key` scoped to the authenticated service and operation. Atomically reserve the key in shared durable storage before the side effect, and return the stored result for an identical retry. Reject reuse with different request data, and retain the record for a bounded period covering the retry window. If a token must be accepted only once, add a server-generated, unpredictable, unique `jti` claim when creating it. Before the side effect, atomically insert `(iss, sub, jti)` into replay storage shared by every service instance; reject the request if it already exists. Keep the entry until at least `exp` plus the permitted clock skew. Couple replay reservation and the write transaction, or use a transactional outbox, so a crash cannot consume the token without a defined result. An `Idempotency-Key` or `jti` is not a substitute for signature, claim, identity, and permission verification. ### APIs that accept frontend sessions and service tokens Prefer separate endpoints or an explicit authentication policy for frontend sessions and service tokens. If one endpoint must accept both, verify each credential only with its intended verifier and apply a separate authorization policy. Never fall back to trusting decoded JWT claims after either verifier returns an error. A malformed, expired, or invalid token must not be downgraded into another authentication path. Use the backend SDK's `getSession` function for frontend session verification. Use the bounded JWKS procedure above for legacy service tokens. Accept the request only after one verifier succeeds and the corresponding identity and permission checks pass. ## Compromise response - **Core API key compromised:** revoke and replace it, stop token issuance while investigating, and wait at least the maximum token lifetime before considering previously minted tokens expired. Review issuance and service logs. - **Dynamic signing key compromised:** rotate the signing material, prevent further issuance, and reject affected keys. Network restrictions can reduce exposure but do not make forged tokens safe. - **Bearer token compromised:** revoke or disable the caller where possible and let the short expiry bound exposure. If immediate revocation is required, use an introspected or stateful design rather than this self-contained legacy flow. --- # Overview Source: https://supertokens.com/docs/authentication/overview Discover all the ways in which you can authenticate your users with **SuperTokens**. ## Authentication Methods Basic authentication using email and password. Authentication through magic links or one-time codes. Login flow that uses third-party providers for authentication. Passwordless authentication using biometrics, security keys, or device-based credentials. Instructions on how to configure your application to support multiple tenants and enterprise authentication methods. Details on how to create a common authentication experience for all your products. Guides on authenticating microservices using SuperTokens. ## Additional Resources For information on other features exposed by SuperTokens, please refer to the following resources: Set up additional verification layers in your sign-in process. Enable additional security features that shield your app. Deploy SuperTokens in your own infrastructure. Learn how to migrate from an existing authentication provider. --- # Customization Source: https://supertokens.com/docs/authentication/passkeys/customization ## Overview Like the other **SuperTokens** authentication recipes, you can customize the `WebAuthn` flow through different configuration options and overrides. The following page describes the options that you can change and the different scenarios enabled through customization. --- ## Backend recipe configuration ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthn from "supertokens-node/recipe/webauthn"; supertokens.init({ framework: "express", supertokens: { // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connectionURI: "https://try.supertokens.com", // apiKey: , }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ WebAuthn.init({ getOrigin: async () => { return "https://example.com"; }, getRelyingPartyId: async () => { return "example.com"; }, getRelyingPartyName: async () => { return "example"; }, }), Session.init(), // initializes session features ], }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/webauthn" "github.com/supertokens/supertokens-golang/recipe/webauthn/webauthnmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ConnectionURI: "https://try.supertokens.com"}, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ webauthn.Init(&webauthnmodels.TypeInput{ GetOrigin: func(tenantID string, req *http.Request, userContext supertokens.UserContext) (string, error) { return "https://example.com", nil }, GetRelyingPartyId: func(tenantID string, req *http.Request, userContext supertokens.UserContext) (string, error) { return "example.com", nil }, GetRelyingPartyName: func(tenantID string, userContext supertokens.UserContext) (string, error) { return "example", nil }, }), session.Init(nil), }, }) if err != nil { panic(err) } } ``` ```python from typing import Optional from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.framework import BaseRequest from supertokens_python.recipe import session, webauthn from supertokens_python.recipe.webauthn import WebauthnConfig from supertokens_python.types.base import UserContext async def get_origin(*, tenant_id: str, request: Optional[BaseRequest], user_context: UserContext): return "https://example.com" async def get_relying_party_id(*, tenant_id: str, request: Optional[BaseRequest], user_context: UserContext): return "example.com" async def get_relying_party_name(*, tenant_id: str, user_context: UserContext): return "example" init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connection_uri="https://try.supertokens.com", # api_key="" ), framework='flask', # Replace this with the framework you are using recipe_list=[ webauthn.init( config=WebauthnConfig( get_origin=get_origin, get_relying_party_id=get_relying_party_id, get_relying_party_name=get_relying_party_name, ) ), session.init() # initializes session features ] ) ``` The backend recipe accepts the following properties during initialization: | Option | Description | Default | |--------|-------------|---------| | `getRelyingPartyId` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | Hostname of `appInfo.apiDomain` | | `getRelyingPartyName` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `appName` value that you have set in `appConfig` | | `getOrigin` | Configures the frontend origin that WebAuthn credentials bind to. | `appInfo.getOrigin(...)`, normally the configured website origin | | `emailDelivery` | Configures how the system builds and sends account-recovery emails. Read the [email delivery page](/platform-configuration/email-delivery) for more information. | Default email service | | `validateEmailAddress` | Adds custom validation logic for email addresses. | Basic email format validation | All the properties are optional. The backend recipe accepts the following properties during initialization: | Option | Description | Default | |--------|-------------|---------| | `get_relying_party_id` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | Hostname of `app_info.api_domain` | | `get_relying_party_name` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `app_name` value that you have set in `app_config` | | `get_origin` | Configures the frontend origin that WebAuthn credentials bind to. | `app_info.get_origin(...)`, normally the configured website origin | | `email_delivery` | Configures how the system builds and sends account-recovery emails. Read the [email delivery page](/platform-configuration/email-delivery) for more information. | Default email service | | `validate_email_address` | Adds custom validation logic for email addresses. | Basic email format validation | All the properties are optional. The backend recipe accepts the following optional properties in `webauthnmodels.TypeInput`: | Option | Description | Default | |--------|-------------|---------| | `GetRelyingPartyId` | Sets the domain name associated with the WebAuthn credentials. | Hostname of `AppInfo.APIDomain` | | `GetRelyingPartyName` | Sets a human-readable name for your application. | `AppInfo.AppName` | | `GetOrigin` | Configures the frontend origin that WebAuthn credentials bind to. | `AppInfo.GetOrigin(...)`, normally the configured website origin | | `EmailDelivery` | Configures how the system builds and sends account-recovery emails. | Default email service | | `ValidateEmailAddress` | Adds custom validation logic for email addresses. | Basic email format validation | The RP ID must equal the frontend origin's host or be a registrable domain suffix of it. It must not include a scheme, port, or path. The origin must include the exact scheme and host, plus the port when it is non-default. For example, RP ID `example.com` is valid for origin `https://login.example.com`, but `api.example.net` is not. If your API and website use unrelated hosts, the default RP ID derived from the API domain is invalid for the website origin; configure both values explicitly. --- ## Credential generation The client generates the credentials based on the options provided by the backend SDK. The frontend SDK uses [`navigator.credentials.create()`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create) to start the registration ceremony. To change the options used to generate credentials, you need to override the `registerOptions` function. ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthn from "supertokens-node/recipe/webauthn"; supertokens.init({ framework: "express", supertokens: { // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connectionURI: "https://try.supertokens.com", // apiKey: , }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ WebAuthn.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, registerOptions: (input) => { return originalImplementation.registerOptions({ ...input, attestation: "direct", residentKey: "required", timeout: 10 * 1000, userVerification: "required", userPresence: true, displayName: "John Doe", supportedAlgorithmIds: [-257], relyingPartyId: "example.com", relyingPartyName: "example", origin: "https://example.com", }); }, }; }, }, }), Session.init(), // initializes session features ], }); ``` ```go check=false reason="Override excerpt; pass config to webauthn.Init in your recipe list." config := &webauthnmodels.TypeInput{ Override: &webauthnmodels.OverrideStruct{ Functions: func(original webauthnmodels.RecipeInterface) webauthnmodels.RecipeInterface { originalRegisterOptions := *original.RegisterOptions registerOptions := func( email, recoverAccountToken, displayName *string, relyingPartyID, relyingPartyName, origin string, timeout *int, attestation *webauthnmodels.Attestation, residentKey *webauthnmodels.ResidentKey, userVerification *webauthnmodels.UserVerification, userPresence *bool, supportedAlgorithmIDs []webauthnmodels.COSEAlgorithmIdentifier, tenantID string, userContext supertokens.UserContext, ) (webauthnmodels.RegisterOptionsResponse, error) { customTimeout := 10 * 1000 customAttestation := webauthnmodels.AttestationDirect customResidentKey := webauthnmodels.ResidentKeyRequired customUserVerification := webauthnmodels.UserVerificationRequired customUserPresence := true return originalRegisterOptions( email, recoverAccountToken, displayName, "example.com", "example", "https://example.com", &customTimeout, &customAttestation, &customResidentKey, &customUserVerification, &customUserPresence, []webauthnmodels.COSEAlgorithmIdentifier{-257}, tenantID, userContext, ) } original.RegisterOptions = ®isterOptions return original }, }, } ``` ```python from typing import List, Optional, cast from typing_extensions import Unpack from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn from supertokens_python.recipe.webauthn import ( RecipeInterface, WebauthnConfig, WebauthnOverrideConfig, ) from supertokens_python.recipe.webauthn.interfaces.recipe import ( Attestation, RegisterOptionsKwargsInput, ResidentKey, UserVerification, ) from supertokens_python.types.base import UserContext def override_webauthn_functions(original_implementation: RecipeInterface): original_register_options = original_implementation.register_options async def register_options( *, relying_party_id: str, relying_party_name: str, origin: str, resident_key: Optional[ResidentKey] = None, user_verification: Optional[UserVerification] = None, user_presence: Optional[bool] = None, attestation: Optional[Attestation] = None, supported_algorithm_ids: Optional[List[int]] = None, timeout: Optional[int] = None, tenant_id: str, user_context: UserContext, **kwargs: Unpack[RegisterOptionsKwargsInput], ): return await original_register_options( relying_party_id="example.com", relying_party_name="example", origin="https://example.com", resident_key="required", user_verification="required", user_presence=True, attestation="direct", supported_algorithm_ids=[-257], timeout=10 * 1000, tenant_id=tenant_id, user_context=user_context, email=cast(str, kwargs.get("email")), recover_account_token=cast(str, kwargs.get("recover_account_token")), display_name="John Doe", ) original_implementation.register_options = register_options return original_implementation init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth", ), supertokens_config=SupertokensConfig( # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connection_uri="https://try.supertokens.com", # api_key="" ), framework="flask", # Replace this with the framework you are using recipe_list=[ webauthn.init( config=WebauthnConfig( override=WebauthnOverrideConfig(functions=override_webauthn_functions) ) ), session.init(), # initializes session features ], ) ```
#### Input properties | Name | Type | Description | Default Value | |----------|----------|-------------|---------------| | `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration, which defaults to the hostname of `appInfo.apiDomain` | | `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration, which defaults to `appName` | | `origin` | `string` | The frontend origin where the credential is created. | Uses `getOrigin` from the recipe configuration, which normally defaults to the configured website origin | | `timeout` | `number` | The time in milliseconds that the user has to complete the credential generation process. | `60000` | | `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The attestation conveyance preference requested from the authenticator. | `none` | | `supportedAlgorithmIds` | `number[]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | | `residentKey` | `"discouraged" \| "preferred" \| "required"` | Whether the authenticator creates a discoverable credential. A discoverable credential may be synced or device-bound. | `required` | | `userVerification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | | `userPresence` | `boolean` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `true` | | `displayName` | `string` | The display name of the user. | The user's `email` property |

#### Input properties | Name | Type | Description | Default Value | |----------|----------|-------------|---------------| | `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration, which defaults to the hostname of `app_info.api_domain` | | `relying_party_name` | `str` | The human-readable name of your application. | Uses `get_relying_party_name` from the recipe configuration which defaults to the `app_name` | | `origin` | `str` | The frontend origin where the credential is created. | Uses `get_origin` from the recipe configuration, which normally defaults to the configured website origin | | `timeout` | `int` | The time in milliseconds that the user has to complete the credential generation process. | `60000` | | `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The attestation conveyance preference requested from the authenticator. | `none` | | `supported_algorithm_ids` | `List[int]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | | `resident_key` | `"discouraged" \| "preferred" \| "required"` | Whether the authenticator creates a discoverable credential. A discoverable credential may be synced or device-bound. | `required` | | `user_verification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | | `user_presence` | `bool` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `True` | | `display_name` | `str` | The display name of the user. | The user's `email` property |

#### Input properties | Name | Type | Description | Default Value | |----------|----------|-------------|---------------| | `relyingPartyId` | `string` | The domain name used to validate the credential. | Uses `GetRelyingPartyId`, which defaults to the hostname of `AppInfo.APIDomain` | | `relyingPartyName` | `string` | The human-readable name of your application. | Uses `GetRelyingPartyName`, which defaults to `AppInfo.AppName` | | `origin` | `string` | The frontend origin where the credential is created. | Uses `GetOrigin`, which normally defaults to the configured website origin | | `timeout` | `*int` | The time in milliseconds available to complete credential creation. | `60000` | | `attestation` | `*webauthnmodels.Attestation` | The attestation conveyance preference requested from the authenticator. | `AttestationNone` | | `supportedAlgorithmIds` | `[]webauthnmodels.COSEAlgorithmIdentifier` | The allowed credential algorithms. | `[-8, -7, -257]` | | `residentKey` | `*webauthnmodels.ResidentKey` | Whether the authenticator creates a discoverable credential. | `ResidentKeyRequired` | | `userVerification` | `*webauthnmodels.UserVerification` | Whether user verification, such as a PIN or biometrics, is necessary. | `UserVerificationPreferred` | | `userPresence` | `*bool` | Whether the ceremony requires evidence of user interaction. | `true` | | `displayName` | `*string` | The display name of the user. | The user's email |
Keep the default `attestation: "none"` unless your relying party has a specific attestation policy. `"direct"` can expose identifying authenticator information and still requires you to validate the attestation statement and its certificate chain against trust anchors you maintain. Requesting direct attestation does not by itself make an authenticator trusted. --- ## Credential validation When a user attempts to sign in, the authenticator uses their credential to sign an assertion on the client. The frontend SDK uses [`navigator.credentials.get()`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get) to start the authentication ceremony. The server generates the options for signing the challenge through the backend SDK, and then sends them to the client. To change those, you need to override the `signInOptions` function. ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthn from "supertokens-node/recipe/webauthn"; supertokens.init({ framework: "express", supertokens: { // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connectionURI: "https://try.supertokens.com", // apiKey: , }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ WebAuthn.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signInOptions: (input) => { return originalImplementation.signInOptions({ ...input, timeout: 10 * 1000, userVerification: "required", userPresence: true, relyingPartyId: "example.com", origin: "https://example.com", }); }, }; }, }, }), Session.init(), // initializes session features ], }); ``` ```go check=false reason="Override excerpt; pass config to webauthn.Init in your recipe list." config := &webauthnmodels.TypeInput{ Override: &webauthnmodels.OverrideStruct{ Functions: func(original webauthnmodels.RecipeInterface) webauthnmodels.RecipeInterface { originalSignInOptions := *original.SignInOptions signInOptions := func( relyingPartyID, relyingPartyName, origin string, timeout *int, userVerification *webauthnmodels.UserVerification, userPresence *bool, tenantID string, userContext supertokens.UserContext, ) (webauthnmodels.SignInOptionsResponse, error) { customTimeout := 10 * 1000 customUserVerification := webauthnmodels.UserVerificationRequired customUserPresence := true return originalSignInOptions( "example.com", "example", "https://example.com", &customTimeout, &customUserVerification, &customUserPresence, tenantID, userContext, ) } original.SignInOptions = &signInOptions return original }, }, } ``` ```python from typing import Any from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn from supertokens_python.recipe.webauthn import ( RecipeInterface, WebauthnConfig, WebauthnOverrideConfig, ) from supertokens_python.types.base import UserContext def override_webauthn_functions(original_implementation: RecipeInterface): original_sign_in_options = original_implementation.sign_in_options async def sign_in_options( *, tenant_id: str, user_context: UserContext, **kwargs: Any ): return await original_sign_in_options( tenant_id=tenant_id, user_context=user_context, timeout=10 * 1000, user_verification="required", user_presence=True, relying_party_id='example.com', relying_party_name='Example', origin='https://example.com', ) original_implementation.sign_in_options = sign_in_options return original_implementation init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. connection_uri="https://try.supertokens.com", # api_key="" ), framework='flask', # Replace this with the framework you are using recipe_list=[ webauthn.init( config=WebauthnConfig( override=WebauthnOverrideConfig( functions=override_webauthn_functions ) ) ), session.init() # initializes session features ] ) ``` #### Input properties | Name | Type | Description | Default | |----------|----------|-------------|---------| | `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration, which defaults to the hostname of `appInfo.apiDomain` | | `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration, which defaults to `appName` | | `origin` | `string` | The expected frontend origin for the authentication response. | Uses `getOrigin` from the recipe configuration, which normally defaults to the configured website origin | | `timeout` | `number` | The time in milliseconds that the user has to complete the credential validation process. | `60000` | | `userVerification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | | `userPresence` | `boolean` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `true` | #### Input properties | Name | Type | Description | Default | |----------|----------|-------------|---------| | `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration, which defaults to the hostname of `app_info.api_domain` | | `relying_party_name` | `str` | The human-readable name of your application. | Uses `get_relying_party_name` from the recipe configuration which defaults to the `app_name` | | `origin` | `str` | The expected frontend origin for the authentication response. | Uses `get_origin` from the recipe configuration, which normally defaults to the configured website origin | | `timeout` | `int` | The time in milliseconds that the user has to complete the credential validation process. | `60000` | | `user_verification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | | `user_presence` | `bool` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `True` | #### Input properties | Name | Type | Description | Default | |----------|----------|-------------|---------| | `relyingPartyId` | `string` | The domain name used to validate the credential. | Uses `GetRelyingPartyId`, which defaults to the hostname of `AppInfo.APIDomain` | | `relyingPartyName` | `string` | The human-readable name of your application. | Uses `GetRelyingPartyName`, which defaults to `AppInfo.AppName` | | `origin` | `string` | The expected frontend origin for the authentication response. | Uses `GetOrigin`, which normally defaults to the configured website origin | | `timeout` | `*int` | The time in milliseconds available to complete authentication. | `60000` | | `userVerification` | `*webauthnmodels.UserVerification` | Whether user verification, such as a PIN or biometrics, is necessary. | `UserVerificationPreferred` | | `userPresence` | `*bool` | Whether the ceremony requires evidence of user interaction. | `true` | --- # Important concepts Source: https://supertokens.com/docs/authentication/passkeys/important-concepts ## Overview Use this page to get a high-level overview of the key concepts involved in the WebAuthn documentation. The reference goes over each term and describes how the **WebAuthn** flows work within **SuperTokens**. ## Terminology ### WebAuthn Web Authentication, **WebAuthn**, is an open web standard that enables secure, passwordless authentication for web applications. **WebAuthn** allows users to log in using biometrics, security keys, or device-based credentials, replacing traditional username and password combinations. Under the hood, the standard relies on [asymmetric (public-key) cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) to confirm the identity of a user. For a more detailed explanation of WebAuthn, you can refer to the [actual specification](https://www.w3.org/TR/webauthn/). ### Passkeys A **passkey** is a discoverable WebAuthn credential. A passkey may be synced across a user's devices by a credential provider, or it may remain bound to one authenticator. Synced passkeys can make recovery from device loss easier; device-bound passkeys require another recovery path. Passkeys are integrated into operating systems and browsers and support a wide range of devices. ### Additional terms A **WebAuthn credential** is an RP-scoped public-key credential created by an authenticator. Credentials can be discoverable or non-discoverable. A passkey is a discoverable credential and can be either synced or device-bound. :::info[Note] This documentation uses **credential** for the WebAuthn object and **passkey** only when referring to a discoverable credential. ::: A device or software that implements the **WebAuthn** authentication. This can be: - **Platform Authenticator**: Built-in biometric sensors like TouchID, FaceID, or Windows Hello. - **Roaming Authenticator**: External security devices like YubiKeys or Google Titan keys. The process where a user registers their **authenticator** with your application. During this process: 1. The server generates registration options containing a challenge and RP information. 2. The browser calls `navigator.credentials.create()`, and the authenticator creates a credential and returns an attestation response bound to the challenge. 3. The server validates the response and stores the public key and required metadata for future authentication. The process where a user proves their identity, using their **authenticator**, by responding to a **server challenge**. Using their private key, they sign the **challenge** and then send the result to the server. The server then verifies the signature with the stored public key.
**Attestation** can provide evidence about an authenticator's provenance and capabilities. A relying party can evaluate that evidence against its policy and trusted attestation roots; attestation alone does not establish a device's general trustworthiness or security level.
**User presence** confirms that a person interacted with the authenticator, for example by touching a security key. It does not identify or verify that person. **User verification** confirms that the person is authorized to use the authenticator. This can use: - Biometric verification (fingerprint, face scan). - `PIN` entry.
## Authentication flows This section explains how each component communicates during different authentication flows. ### Login 1. **The frontend SDK requests authentication options from the backend.** The options are then returned based on the response from the **SuperTokens** core service. 2. **The browser calls navigator.credentials.get(), and the authenticator signs an assertion with an existing credential.** 3. **The backend SDK sends the assertion for validation by SuperTokens Core and creates a session after successful sign-in.** 4. **The authentication UI updates, based on the result of the validation process.** Sign in form UI for passkeys login ### Sign up 1. **The user enters their email address in the frontend authentication UI** 2. **The frontend SDK uses the email to request registration options from the backend.** The options are then returned based on the response from the **SuperTokens** core service. 3. **The browser calls navigator.credentials.create(), and the authenticator creates a credential and returns an attestation response.** 4. **The backend SDK sends the registration response for validation by SuperTokens Core and creates the account and session after successful sign-up.** 5. **The authentication UI updates, based on the result of the validation process.** Passkeys sign up flow ### Account recovery SuperTokens account recovery uses an email containing a link to a page where the user can register a new credential. 1. **The frontend initiates the recovery flow by communicating with the backend SDK** 2. **The backend checks if the email exists and then sends a recovery email.** The email includes a security token obtained from the **SuperTokens** core. 3. **When the user accesses the recovery link, they get directed to the frontend application. ** The security token gets validated by the backend SDK. If successful, the SDK begins the process of registering a new credential. From here, the flow matches the one described in the previous sections. Passkey account recovery flow --- # Set Up Passkey Authentication Source: https://supertokens.com/docs/authentication/passkeys/initial-setup ## Passkey integration summary - This guide configures standalone passkey authentication, not passkeys as an MFA factor. - WebAuthn is supported by the Node.js, Python, and Go backend SDKs. - Configure the WebAuthn and Session recipes on both the frontend and backend, then expose and render the authentication routes. - The backend recipe exposes the endpoints used by the frontend and communicates with SuperTokens Core to complete registration and authentication. Add SuperTokens passkey authentication to this application. Inspect the existing stack and authentication setup, confirm that the backend SDK supports WebAuthn, and determine the deployment origin and relying-party configuration. Configure the frontend and backend WebAuthn and Session recipes, auth routes, HTTPS requirements, and fallback authentication where appropriate. Preserve existing conventions, do not commit secrets, and validate registration, authentication, cancellation, and unsupported-browser behavior. ## Overview This page shows you how to add the **Passkeys** authentication method to your project. The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ## Before you start Passkeys may be unavailable because of browser, device, or authenticator support. Keep another authentication method or an account-recovery path available. A user can also cancel the browser or platform prompt; treat cancellation as an interrupted attempt, let the user retry, and do not report it as a successful sign-in or sign-up. WebAuthn is available only in a [secure context](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API), so serve the frontend over HTTPS in production. Browsers also allow `http://localhost` for local development. The relying party (RP) ID must equal the frontend hostname or be a registrable suffix of it. The expected origin must exactly match the frontend origin, including its scheme and non-default port. If the frontend and API use different hostnames, [configure these values explicitly](/authentication/passkeys/customization#backend-recipe-configuration) instead of relying on values derived from the ## Steps ### 1. Initialize the frontend SDK #### 1.1 Add the `WebAuthn` recipe in your main configuration file. ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import WebAuthn from "supertokens-auth-react/recipe/webauthn"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", websiteDomain: "...", appName: "...", }, recipeList: [WebAuthn.init(), Session.init()], }); ``` #### 1.2 Include the pre-built UI components in your application. In order for the **pre-built UI** to render inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. ```tsx import React from "react"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([WebauthnPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([WebauthnPreBuiltUI]); } return {/*Your app*/}; } } ``` :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx import React from "react"; import { BrowserRouter, useRoutes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI]); const routes = useRoutes([ ...authRoutes.map((route) => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import WebAuthn from "supertokens-web-js/recipe/webauthn"; SuperTokens.init({ appInfo: { apiDomain: "...", apiBasePath: "...", appName: "...", }, recipeList: [Session.init(), WebAuthn.init()], }); ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "", apiBasePath: "/auth", }); ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "") .apiBasePath("/auth") .build() } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "", apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "", apiBasePath: "/auth", ); } ``` ### 2. Add the passkeys UI #### 2.1 Add the sign up form Create a form in which the user can input their email address. When the user submits the form, call the `registerCredentialWithSignUp` method like in the next code snippet. Under the hood, the method communicates with the backend SDK to fetch the registration options. Once the backend responds, it uses the browser's APIs to begin the registration process. For a more detailed overview of the sign-up flow check the [Important Concepts page](/authentication/passkeys/important-concepts#sign-up). ```ts import { registerCredentialWithSignUp } from "supertokens-web-js/recipe/webauthn"; async function signUp(email: string) { try { let response = await registerCredentialWithSignUp({ email, userContext: {}, }); if (response.status === "SIGN_UP_NOT_ALLOWED" || response.status === "INVALID_AUTHENTICATOR_ERROR") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else if (response.status === "INVALID_EMAIL_ERROR" || response.status === "EMAIL_ALREADY_EXISTS_ERROR") { window.alert("Invalid email"); } else if ( response.status === "INVALID_CREDENTIALS_ERROR" || response.status === "OPTIONS_NOT_FOUND_ERROR" || response.status === "INVALID_OPTIONS_ERROR" || response.status === "AUTHENTICATOR_ALREADY_REGISTERED" || response.status === "FAILED_TO_REGISTER_USER" || response.status === "WEBAUTHN_NOT_SUPPORTED" ) { // These errors represent various issues with the authenticator, credential or the flow itself. // These should be handled individually by you. // The user should be informed that they should retry the sign up process or get in touch with you. window.alert("Please try again"); } else if (response.status === "INVALID_GENERATED_OPTIONS_ERROR") { window.alert("The registration request expired. Please try again."); } else if (response.status === "GENERAL_ERROR") { window.alert(response.message); } else if (response.status === "OK") { // User signed up successfully. window.alert("You have been signed up successfully"); } else { window.alert("Sign up could not be completed. Please try another authentication method."); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` The requests in this standalone authentication flow omit `shouldTryLinkingWithSessionUser`, so it defaults to `false`. Set it to `true` only for an authenticated add-factor or account-linking flow where your backend policy permits linking to the session user. 1. **Get the email address from the user** Add a form where the user can input their email address. 2. **Fetch the registration options from the backend SDK** When the user submits the form, call the `register options` API. Save the response to use it in the next step. ```bash curl --location --request POST '/auth/webauthn/options/register' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@gmail.com", "displayName": "John Doe" }' ``` :::warning[The returned result matches the format required by a WebAuthn client API.] You will have to map the properties to the correct format based on the requirements of your platform. ::: 3. **Register a new credential authenticator API** Use the received options generate a new credential. The implementation will vary based on the platform you are using. - **React Native**: You can use the [`react-native-passkey`](https://github.com/f-23/react-native-passkey) library. - **iOS**: Use the [`Authentication Services`](https://developer.apple.com/documentation/authenticationservices) framework. - **Android**: Use the [`Android Credential Manager API`](https://developer.android.com/identity/sign-in/credential-manager). - **Flutter**: Use [platform channels](https://docs.flutter.dev/platform-integration/platform-channels#architecture) to access the native APIs. 4. **Call the sign up API** Using the newly generated credential, call the sign up API to save the new authentication method. Encode `id`, `rawId`, `clientDataJSON`, and `attestationObject` as unpadded Base64URL. Include `transports` when the authenticator supplies it. #### 2.2 Add the login form Add a button that can trigger the sign in flow. This is all that you need in terms of UI. When the user clicks it, call the `authenticateCredentialWithSignIn` method to handle the whole process. The function uses the backend authentication options to trigger the challenge signing action through the browser API. Then, it forwards the result to the backend for validation. For a more detailed overview of the login flow check the [Important Concepts page](/authentication/passkeys/important-concepts#login). ```ts import { authenticateCredentialWithSignIn } from "supertokens-web-js/recipe/webauthn"; async function signIn() { try { let response = await authenticateCredentialWithSignIn({ userContext: {} }); if (response.status === "SIGN_IN_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else if (response.status === "WEBAUTHN_NOT_SUPPORTED") { // the user's browser does not support the WebAuthn standard window.alert("Login method not supported"); } else if ( response.status === "INVALID_CREDENTIALS_ERROR" || response.status === "INVALID_OPTIONS_ERROR" || response.status === "FAILED_TO_AUTHENTICATE_USER" ) { // These errors represent various issues with the authenticator, credential or the flow itself. // FAILED_TO_AUTHENTICATE_USER can also indicate that the user cancelled the authenticator prompt. // These should be handled individually by you. // The user should be informed that they should retry the sign in process or get in touch with you. window.alert("Please try again"); } else if (response.status === "GENERAL_ERROR") { window.alert(response.message); } else if (response.status === "OK") { // User signed in successfully. window.alert("You have been signed in successfully"); } else { window.alert("Sign in could not be completed. Please try another authentication method."); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` 1. **Add a button that can trigger the sign in flow** 2. **Get the sign in options from the backend SDK** When the user taps the sign in button, call the backend API to fetch the sign in options. ```bash curl --location --request POST '/auth/webauthn/options/signin' \ --header 'Content-Type: application/json; charset=utf-8' ``` 3. **Use the authenticator to sign the challenge** With the received options, invoke the authenticator to sign the challenge. The implementation will vary based on the platform you are using. 4. **Call the sign in API** Send the signed challenge to the backend for validation. Encode `id`, `rawId`, `clientDataJSON`, `authenticatorData`, `signature`, and an optional `userHandle` as unpadded Base64URL. Include `userHandle` in `credential.response` when the authenticator returns it. ### 2. Initialize the backend SDK ### 3. Initialize the backend SDK Initialize the backend SDK and include the **WebAuthn** `recipe`. The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. The recipe exposes the required endpoints that get accessed by the frontend code, and communicates with the **SuperTokens Core** to complete the authentication flow. You can [configure different aspects](/authentication/passkeys/customization) of the recipe's behavior but, for the completion of this guide, use the default values. After you confirm that the flow works as expected, you can explore more advanced customization options. ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthN from "supertokens-node/recipe/webauthn"; supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [WebAuthN.init(), Session.init()], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/webauthn" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "https://try.supertokens.io", // APIKey: "", }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", }, RecipeList: []supertokens.Recipe{ webauthn.Init(nil), session.Init(nil), }, }) if err != nil { panic(err) } } ``` ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key="" ), framework='flask', # Replace this with the framework you are using recipe_list=[ webauthn.init(), session.init() ] ) ``` --- # Passkey Authentication Source: https://supertokens.com/docs/authentication/passkeys/introduction ## Passkey authentication summary - This section covers passkeys as a standalone passwordless authentication method based on WebAuthn, not passkeys used as an MFA factor. - Users authenticate with a compatible authenticator, using a PIN, biometrics, a security key, or a cross-device flow instead of a password. - WebAuthn requires a secure context and an RP ID and origin that match the deployment. - Credential generation and credential validation can be customized. ## Overview **Passkeys** are a passwordless authentication method based on the **WebAuthn**, Web Authentication, specification. The standard allows users to log in using biometric authentication, security keys, or device-based credentials, replacing traditional username and password combinations. Sign in form UI for passkeys login ## Prerequisites Users need a compatible browser and authenticator. Depending on the authenticator, they may use a PIN, biometrics, a security key, or a cross-device flow. WebAuthn is available only in a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), normally HTTPS, with limited exceptions such as localhost. Your relying party (RP) ID and origin must also match the frontend deployment; see the [customization guidance](/authentication/passkeys/customization#backend-recipe-configuration). ## Getting started Before going into the actual quickstart guide, read through the [**Important Concepts** page](/authentication/passkeys/important-concepts). It provides a high-level overview of the terms and concepts used in the passkeys authentication flow. Go through a quick summary of the WebAuthn specifications to get accustomed with the language used in the guides. Implement an authentication flow that uses passkeys to log in users. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Read through all the options that you can set during the initialization step. See how you can adjust the process that generates credentials. Discover how to customize the validation process. --- # Implement allow list based sign up Source: https://supertokens.com/docs/authentication/passwordless/allow-list-flow ## Overview In this flow, you create a list of emails or phone numbers that are allowed to sign up. Based on that users can go through the passwordless flow. ## Before you start This guide assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). ### Prerequisites This guide uses the `UserMetadata` recipe to store the allow list. You need to [enable it](/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps ### 1. Add a way to keep track of allowed emails or phone numbers Start by maintaining an allow list of emails. Use transactional application storage for a production allow list. The User Metadata examples below are suitable for a simple prototype, but their read-modify-write updates are not atomic: concurrent additions can overwrite each other. The following code samples show you how to save the allow list in the user metadata. ```tsx import UserMetadata from "supertokens-node/recipe/usermetadata"; async function addEmailToAllowlist(tenantId: string, email: string) { const metadataKey = `${tenantId}:emailAllowList`; let existingData = await UserMetadata.getUserMetadata(metadataKey); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, email]; await UserMetadata.updateUserMetadata(metadataKey, { allowList, }); } async function isEmailAllowed(tenantId: string, email: string) { let existingData = await UserMetadata.getUserMetadata(`${tenantId}:emailAllowList`); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(email); } async function addPhoneNumberToAllowlist(tenantId: string, phoneNumber: string) { const metadataKey = `${tenantId}:phoneNumberAllowList`; let existingData = await UserMetadata.getUserMetadata(metadataKey); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, phoneNumber]; await UserMetadata.updateUserMetadata(metadataKey, { allowList, }); } async function isPhoneNumberAllowed(tenantId: string, phoneNumber: string) { let existingData = await UserMetadata.getUserMetadata(`${tenantId}:phoneNumberAllowList`); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(phoneNumber); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/usermetadata" ) func stringListFromMetadata(value interface{}) ([]string, error) { if value == nil { return []string{}, nil } items, ok := value.([]interface{}) if !ok { return nil, fmt.Errorf("allowList metadata is not an array") } result := make([]string, 0, len(items)) for _, item := range items { text, ok := item.(string) if !ok { return nil, fmt.Errorf("allowList metadata contains a non-string value") } result = append(result, text) } return result, nil } func addEmailToAllowlist(tenantId, email string) error { metadataKey := tenantId + ":emailAllowList" existingData, err := usermetadata.GetUserMetadata(metadataKey) if err != nil { return err } allowList, err := stringListFromMetadata(existingData["allowList"]) if err != nil { return err } allowList = append(allowList, email) _, err = usermetadata.UpdateUserMetadata(metadataKey, map[string]interface{}{ "allowList": allowList, }) return err } func isEmailAllowed(tenantId, email string) (bool, error) { existingData, err := usermetadata.GetUserMetadata(tenantId + ":emailAllowList") if err != nil { return false, err } allowList, err := stringListFromMetadata(existingData["allowList"]) if err != nil { return false, err } for _, allowedEmail := range allowList { if allowedEmail == email { return true, nil } } return false, nil } func addPhoneNumberToAllowlist(tenantId, phoneNumber string) error { metadataKey := tenantId + ":phoneNumberAllowList" existingData, err := usermetadata.GetUserMetadata(metadataKey) if err != nil { return err } allowList, err := stringListFromMetadata(existingData["allowList"]) if err != nil { return err } allowList = append(allowList, phoneNumber) _, err = usermetadata.UpdateUserMetadata(metadataKey, map[string]interface{}{ "allowList": allowList, }) return err } func isPhoneNumberAllowed(tenantId, phoneNumber string) (bool, error) { existingData, err := usermetadata.GetUserMetadata(tenantId + ":phoneNumberAllowList") if err != nil { return false, err } allowList, err := stringListFromMetadata(existingData["allowList"]) if err != nil { return false, err } for _, allowedPhoneNumber := range allowList { if allowedPhoneNumber == phoneNumber { return true, nil } } return false, nil } ``` ```python from typing import List from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) async def add_email_to_allow_list(tenant_id: str, email: str): metadata_key = f"{tenant_id}:emailAllowList" metadataResult = await get_user_metadata(metadata_key) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] allow_list.append(email) await update_user_metadata(metadata_key, { "allowList": allow_list }) async def is_email_allowed(tenant_id: str, email: str): metadataResult = await get_user_metadata(f"{tenant_id}:emailAllowList") allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] return email in allow_list async def add_phone_number_to_allow_list(tenant_id: str, phone_number: str): metadata_key = f"{tenant_id}:phoneNumberAllowList" metadataResult = await get_user_metadata(metadata_key) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] allow_list.append(phone_number) await update_user_metadata(metadata_key, { "allowList": allow_list }) async def is_phone_number_allowed(tenant_id: str, phone_number: str): metadataResult = await get_user_metadata(f"{tenant_id}:phoneNumberAllowList") allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] return phone_number in allow_list ``` :::info[Multi Tenancy] The helpers separate prototype metadata by `tenantId`, which the API overrides provide. User Metadata has no `tenantId` argument, so never use one shared synthetic key across tenants. For production, enforce tenant isolation and atomic updates in application storage. ::: ### 2. Check if the user is on the allow list Update the backend SDK API function to only allow sign up requests from users that are on the allow list. To do this you need to use the check functions from the previous code snippet. ```tsx check=false reason="This example uses allow-list helpers defined in the preceding application code." import Passwordless from "supertokens-node/recipe/passwordless"; import supertokens from "supertokens-node"; Passwordless.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email, }); let userWithPasswordles = existingUsers.find( (u) => u.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined, ); if (userWithPasswordles === undefined) { // this is sign up attempt if (!(await isEmailAllowed(input.tenantId, input.email))) { return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin.", }; } } } else { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { phoneNumber: input.phoneNumber, }); let userWithPasswordles = existingUsers.find( (u) => u.loginMethods.find( (lM) => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless", ) !== undefined, ); if (userWithPasswordles === undefined) { // this is sign up attempt if (!(await isPhoneNumberAllowed(input.tenantId, input.phoneNumber))) { return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin.", }; } } } return await originalImplementation.createCodePOST!(input); }, }; }, }, }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func isEmailAllowed(tenantId, email string) (bool, error) { // ... from previous code snippet return false, nil } func isPhoneNumberAllowed(tenantId, phoneNumber string) (bool, error) { // ... from previous code snippet return false, nil } func main() { passwordless.Init(plessmodels.TypeInput{ Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { if email != nil { existingUser, err := passwordless.GetUserByEmail(tenantId, *email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt emailAllowed, err := isEmailAllowed(tenantId, *email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if !emailAllowed { return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } } else { existingUser, err := passwordless.GetUserByPhoneNumber(tenantId, *phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt phoneNumberAllowed, err := isPhoneNumberAllowed(tenantId, *phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if !phoneNumberAllowed { return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } } return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } return originalImplementation }, }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import ( APIInterface, APIOptions, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput async def is_email_allowed(tenant_id: str, email: str): # from previous code snippet.. return False async def is_phone_number_allowed(tenant_id: str, phone_number: str): # from previous code snippet.. return False def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): if email is not None: existing_user = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) user_with_passwordless = next( ( user for user in existing_user if any( login_method.recipe_id == "passwordless" and login_method.has_same_email_as(email) for login_method in user.login_methods ) ), None, ) if user_with_passwordless is None: # sign up attempt if not (await is_email_allowed(tenant_id, email)): return GeneralErrorResponse( "Sign ups disabled. Please contact admin." ) else: assert phone_number is not None existing_user = await list_users_by_account_info( tenant_id, AccountInfoInput(phone_number=phone_number) ) user_with_passwordless = next( ( user for user in existing_user if any( login_method.recipe_id == "passwordless" and login_method.has_same_phone_number_as(phone_number) for login_method in user.login_methods ) ), None, ) if user_with_passwordless is None: # sign up attempt if not (await is_phone_number_allowed(tenant_id, phone_number)): return GeneralErrorResponse( "Sign ups disabled. Please contact admin." ) return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) original_implementation.create_code_post = create_code_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ passwordless.init( flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), ) ], ) ``` --- ## See also --- # Configure email and SMS behavior Source: https://supertokens.com/docs/authentication/passwordless/configure-email-and-sms-behavior ## Changing email / SMS resend time interval :::warning These instructions are only applicable if you are using the pre-built UI. ::: You can set `resendEmailOrSMSGapInSeconds` to establish a minimum delay before the frontend allows the user to click the "Resend" button. This limit is only enforced on the client-side. For API rate-limiting please check out the [deployment section](/deployment/rate-limits). ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. signInUpFeature: { // The default value is 15 seconds resendEmailOrSMSGapInSeconds: 60, }, }), Session.init({ /* ... */ }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. signInUpFeature: { // The default value is 15 seconds resendEmailOrSMSGapInSeconds: 60, }, }), supertokensUISession.init({ /* ... */ }), ], }); ``` --- ## Setting default country for phone inputs :::warning These instructions are only applicable if you are using the pre-built UI. ::: Since your delivery method is email, this section is not relevant. If you would still like to see the content, you can resubmit your desired configuration by clicking on the button above. By default, there is no default country selected. This means that users have to select / type in their phone number international code when signing in / signing up. If you would like to set a default country (for all users), then you should use the `defaultCountry` configuration: ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "PHONE", signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", }, }), Session.init({ /* ... */ }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "PHONE", signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", }, }), supertokensUISession.init({ /* ... */ }), ], }); ``` By default, there is no default country selected. This means that users have to select / type in their phone number international code when signing in / signing up. If you would like to set a default country (for all users), then you should use the `defaultCountry` configuration: ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", }, }), Session.init({ /* ... */ }), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", signInUpFeature: { /* * Must be a two-letter ISO country code (e.g.: "US") */ defaultCountry: "HU", }, }), supertokensUISession.init({ /* ... */ }), ], }); ``` --- ## See also --- # Customize the Magic Link Source: https://supertokens.com/docs/authentication/passwordless/customize-the-magic-link ## Change the magic link URL ### Override the email delivery backend function You can change the URL of Magic Links by providing overriding the email delivery configuration on the backend. ```tsx 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: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "MAGIC_LINK" flows. flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { return originalImplementation.sendEmail({ ...input, urlWithLinkCode: input.urlWithLinkCode?.replace( // This is: `/auth/verify` "http://localhost:3000/auth/verify", "http://your.domain.com/your/path", ), }); }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "strings" "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "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{ EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // By default: `//auth/verify` newUrl := strings.Replace( *input.PasswordlessLogin.UrlWithLinkCode, "http://localhost:3000/auth/verify", "http://localhost:3000/custom/path", 1, ) input.PasswordlessLogin.UrlWithLinkCode = &newUrl return ogSendEmail(input, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe.passwordless.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import passwordless from typing import Dict, Any from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: assert template_vars.url_with_link_code is not None # By default: `//auth/verify` template_vars.url_with_link_code = template_vars.url_with_link_code.replace( "http://localhost:3000/auth/verify", "http://localhost:3000/custom/path") return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ) ] ) ``` ### Change the frontend page When the user clicks the magic link, you need to render the `LinkClicked` component that exported by the SDK on that page. By default, this already happens on the `/auth/verify` path. To change this, you need to: #### 1. Disable the default UI for the link clicked screen: When the user clicks the magic link, you need to build your own UI on that page to handle the link clicked. You also need to disable the pre-built UI provided by the SDK for the link clicked screen as shown below: ```tsx import Passwordless from "supertokens-auth-react/recipe/passwordless"; Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod linkClickedScreenFeature: { disableDefaultUI: true, }, }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIPasswordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod linkClickedScreenFeature: { disableDefaultUI: true, }, }); ``` #### 2. Render the link clicked screen on your custom route: ```tsx import React from "react"; import { LinkClicked } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; function CustomLinkClickedScreen() { return ; } ``` :::info[Caution] Not applicable since you do not use the pre-built UI ::: --- ## Generate the link manually You can use the backend SDK to generate magic links as shown below: ```tsx import Passwordless from "supertokens-node/recipe/passwordless"; async function createMagicLink(email: string) { const magicLink = await Passwordless.createMagicLink({ email, tenantId: "public" }); console.log(magicLink); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/passwordless" ) func main() { email := "..." tenantId := "public" magicLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // handle error } fmt.Println(magicLink) } ``` ```python from supertokens_python.recipe.passwordless.asyncio import create_magic_link async def create_link(email: str): magic_link = await create_magic_link("public", email, phone_number=None) print(magic_link) ``` ```python from supertokens_python.recipe.passwordless.syncio import create_magic_link def create_link(email: str): magic_link = create_magic_link("public", email, phone_number=None) print(magic_link) ``` :::info[Multi Tenancy] Notice that you pass the `"public"` `tenantId` to the function call above - which is the default `tenantId`. If you are using the multi tenancy feature, you can pass in another `tenantId` which SuperTokens embeds in the link. This ensures that when the user clicks on the link and signs up, they sign up to the tenant you want to give them access to. Note that the generated link uses the configured `websiteDomain` from the `appInfo` object (in `supertokens.init`), however, you can change the domain of the generated link to match that of the tenant ID. ::: --- ## Change the link lifetime You can change how long a user can use an OTP or a Magic Link to log in by changing the `passwordless_code_lifetime` core configuration value. You configure this value in milliseconds and it defaults to `900000` (15 minutes). :::warning[Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime according to the `passwordless_code_lifetime` setting.] ::: - Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. - In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. ```bash docker run \ -p 3567:3567 \ -e PASSWORDLESS_CODE_LIFETIME=60000 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_code_lifetime: 60000 ``` ```yaml passwordless_code_lifetime: 60000 ``` --- ## See also --- # Customize the One-Time Password (OTP) Source: https://supertokens.com/docs/authentication/passwordless/customize-the-otp ## Change the OTP format By default, the generated OTP is 6 digits long and is numbers only. You can change this to be any length you like and have any character set by providing the `getCustomUserInputCode` function. ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "USER_INPUT_CODE" flows. flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", getCustomUserInputCode: async (userCtx) => { // TODO: return "123abcd"; }, }), ], }); ``` ```go 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{ GetCustomUserInputCode: func(tenantId string, userContext supertokens.UserContext) (string, error) { // TODO: return "123abcd", nil }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from typing import Dict, Any async def get_custom_user_input_code(tenant_id: str, user_context: Dict[str, Any]): return "123abcd" # TODO init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( contact_config=..., flow_type="...", get_custom_user_input_code=get_custom_user_input_code ) ] ) ``` --- ## Limit OTP retries You can change how many times a user can attempt to enter an OTP before they have to enter their email / phone number again (thereby force generating a new OTP). By default, this value is `5` attempts, and you can modify it by changing the `passwordless_max_code_input_attempts` core configuration: - Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. - In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. ```bash docker run \ -p 3567:3567 \ -e PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS=3 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_max_code_input_attempts: 3 ``` ```yaml passwordless_max_code_input_attempts: 3 ``` --- ## Change the OTP lifetime You can change how long a user can use an OTP or a Magic Link to log in by changing the `passwordless_code_lifetime` core configuration value. This value defaults to `900000` milliseconds (15 minutes). :::warning[Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime per the `passwordless_code_lifetime` setting.] ::: - Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. - In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. ```bash docker run \ -p 3567:3567 \ -e PASSWORDLESS_CODE_LIFETIME=60000 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_code_lifetime: 60000 ``` ```yaml passwordless_code_lifetime: 60000 ``` --- ## See also --- # Hooks and overrides Source: https://supertokens.com/docs/authentication/passwordless/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can adjust the `passwordless` recipe to your needs. Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Frontend hook This method gets fired, after certain events in the `passwordles` authentication flow. Use it to fire different types of events immediately and introduce custom logic based on your use case. ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", onHandleEvent: async (context) => { if (context.action === "PASSWORDLESS_RESTART_FLOW") { // TODO: } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else { let { id, emails, phoneNumbers } = context.user; if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL_OR_PHONE", onHandleEvent: async (context) => { if (context.action === "PASSWORDLESS_RESTART_FLOW") { // TODO: } else if (context.action === "PASSWORDLESS_CODE_SENT") { // TODO: } else { let { id, emails, phoneNumbers } = context.user; if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } } }, }), supertokensUISession.init(), ], }); ``` :::warning[Not applicable] This section is not applicable for custom UI since you are calling the consume code API yourself anyway. You can perform any actions post sign in / up based on the result of the API call. ::: ## Backend override Overriding the `consumeCode` function allows you to introduce your own logic for the authentication process. Use it to persist different types of data or trigger actions. ```tsx 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: "...", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", // This example will work with any contactMethod flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // This example will work with any flowType override: { functions: (originalImplementation) => { return { ...originalImplementation, consumeCode: async (input) => { // First we call the original implementation of consumeCode. let response = await originalImplementation.consumeCode(input); // Post sign up response, we check if it was successful if (response.status === "OK") { let { id, emails, phoneNumbers } = response.user; if (input.session === undefined) { if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: post sign up logic } else { // TODO: post sign in logic } } } return response; }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "fmt" "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 { // create a copy of the original function originalConsumeCode := *originalImplementation.ConsumeCode // override the sign in up function (*originalImplementation.ConsumeCode) = func(userInput *plessmodels.UserInputCodeWithDeviceID, linkCode *string, preAuthSessionID string, tenantId string, userContext supertokens.UserContext) (plessmodels.ConsumeCodeResponse, error) { // First we call the original implementation of ConsumeCode. response, err := originalConsumeCode(userInput, linkCode, preAuthSessionID, tenantId, userContext) if err != nil { return plessmodels.ConsumeCodeResponse{}, err } if response.OK != nil { // sign in was successful // user object contains the ID and email or phone number user := response.OK.User fmt.Println(user) if response.OK.CreatedNewUser { // TODO: Post sign up logic } else { // TODO: Post sign in logic } } return response, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session, passwordless from supertokens_python.recipe.passwordless.interfaces import ( RecipeInterface, ConsumeCodeOkResult, ) from typing import Dict, Any, Union, Optional from supertokens_python.recipe.session.interfaces import SessionContainer def override_passwordless_functions( original_implementation: RecipeInterface, ) -> 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], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, user_context: Dict[str, Any], ): # First we call the original implementation of consume_code. result = await original_consume_code( pre_auth_session_id, user_input_code, device_id, link_code, session, should_try_linking_with_session_user, tenant_id, user_context, ) # Post sign up response, we check if it was successful if session is None: if ( isinstance(result, ConsumeCodeOkResult) and len(result.user.login_methods) == 1 ): if result.created_new_recipe_user: # TODO: post sign up logic pass else: # TODO: post sign in logic pass return result 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=passwordless.ContactConfig( contact_method="EMAIL", # This example will work with any contactMethod ), flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", # This example will work with any flowType override=passwordless.InputOverrideConfig( functions=override_passwordless_functions ), ), session.init(), ], ) ``` --- ## See also --- # Passwordless Setup for Magic Link Login and OTPs Source: https://supertokens.com/docs/authentication/passwordless/initial-setup ## Magic link login and passwordless setup - Configure the Passwordless and Session recipes on both the frontend and backend, then add the selected UI and authentication routes. - Set `contactMethod` to email, phone, or email-or-phone delivery. - Set `flowType` to `MAGIC_LINK`, `USER_INPUT_CODE` for OTPs, or `USER_INPUT_CODE_AND_MAGIC_LINK` for both. - Configure email or SMS delivery. Test sign-in, resend, expired or invalid credentials, and session creation. For email magic link login, initialize the backend Passwordless recipe with `contactMethod: "EMAIL"` and `flowType: "MAGIC_LINK"`, alongside the Session recipe. Initialize Passwordless with `contactMethod: "EMAIL"` and Session on the frontend, configure the application domains and authentication routes, and choose the prebuilt UI or a custom UI. Configure email delivery for the links. The steps below include SDK-specific examples; the frontend does not need the backend's `flowType` option. Implement SuperTokens passwordless authentication in this application. Inspect the existing frontend, backend, recipes, and routing first. Ask whether users should authenticate through email, SMS, or both, and whether the flow should use magic links, OTPs, or both. Configure the frontend and backend Passwordless and Session recipes, the selected UI, auth routes, and email or SMS delivery. Preserve existing conventions, keep credentials in environment variables, and validate sign-in, resend, expiry, and session behavior. ## Overview This page shows you how to add the **Passwordless** `recipe` to your project. The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ### Terminology Before going into the actual steps lets first talk about two terms that influence how you configure the **Passwordless** recipe. - **Contact Method**: This defines how the user receives the credentials from your app. You can choose between `email`, `phone number` or both (the user has to choose one during the login flow). - **Flow Type**: This is the credential type used for authentication. You can choose **Magic Link**, **OTP** (One-Time Password), or both. The combined flow sends both credentials and the user can complete authentication with either one. ## Steps ### 1. Initialize the frontend SDK #### 1.1 Add the `Passwordless` recipe in your main configuration file. Add the `Passwordless` recipe in your `AuthComponent`. Add the `Passwordless` recipe in your `AuthView` file. ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ Passwordless.init({ contactMethod: "EMAIL", }), Session.init(), ], }); ``` ```tsx title="/app/auth/auth.component.ts" check=false reason="This example omits surrounding application and SuperTokens configuration." import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; import { DOCUMENT } from "@angular/common"; @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ supertokensUIPasswordless.init({ contactMethod: "EMAIL", }), supertokensUISession.init(), ], }); }; this.renderer.appendChild(this.document.body, script); } } ```
```html ```
#### 1.2 Include the pre-built UI components in your application. To render the **Pre-Built UI** inside your application, you need to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. ```tsx import React from "react"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [PasswordlessPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([PasswordlessPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([PasswordlessPreBuiltUI]); } return {/*Your app*/}; } } ``` :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx import React from "react"; import { BrowserRouter, useRoutes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ ]); const routes = useRoutes([ ...authRoutes.map((route) => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: ### 2. Initialize the backend SDK You need to initialize the **Backend SDK** alongside the code that starts your server. The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. Click one of the options from the next form and the code snippet updates. ```tsx title="Backend SDK Init" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ Passwordless.init({ flowType: "MAGIC_LINK", contactMethod: "EMAIL", }), Session.init(), ], }); ``` ```python title="Backend SDK Init" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import passwordless, session from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features passwordless.init( flow_type="MAGIC_LINK", contact_config=ContactEmailOnlyConfig() ) ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. ConnectionURI: "https://try.supertokens.io", // APIKey: }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "MAGIC_LINK", ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true}, }), session.Init(nil), // initializes session features }, }) if err != nil { panic(err.Error()) } } ```
### 1. Initialize the frontend SDK Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. First, you need to add the recipe script tag. Add the `SuperTokens.init` function call at the start of your application. ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import Passwordless from "supertokens-web-js/recipe/passwordless"; SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, recipeList: [Session.init(), Passwordless.init()], }); ``` ```html ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "", apiBasePath: "/auth", }); ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "") .apiBasePath("/auth") .build() } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "", apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "", apiBasePath: "/auth", ); } ``` You can initialize the SDK ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, recipeList: [supertokensSession.init(), supertokensPasswordless.init()], }); ``` ### 2. Add the login UI Follow the section that matches your configured `flowType`. For `USER_INPUT_CODE_AND_MAGIC_LINK`, one create-code request sends both a magic link and an OTP. Use the shared create and resend behavior from steps 2.1 and 2.2, then implement both consumption paths so the user can complete either one. #### Magic Link The following section shows you what aspects you need to cover to implement the UI for a `Magic Link` flow. The same flow applies during either sign up or sign in. This guide shows you how to determine if the system creates a new user in the next steps. ##### 2.1 Sending the Magic link You need to add a form that asks the user for their email address or phone number. When the user submits the form, you need to call the following API to create and send them a **Magic Link**. :::info[You configure the contact method on the next page, where you discuss the process of adding the `SDK` to your backend app.] ::: For email based login ```tsx import { createCode } from "supertokens-web-js/recipe/passwordless"; async function sendMagicLink(email: string) { try { let response = await createCode({ email, }); /** * For phone number, use this: let response = await createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function sendMagicLink(email: string) { try { let response = await supertokensPasswordless.createCode({ email, }); /** * For phone number, use this: let response = await supertokensPasswordless.createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else { // Magic link sent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/auth/public/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@gmail.com" }' ``` For phone number based login ```bash curl --location --request POST '/auth/public/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+1234567890" }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```typescript check=false reason="This block documents the response shape rather than executable code." { status: "OK"; deviceId: string; preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } ``` You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: - Resend a new magic link. - Detect if the user has already sent a magic link before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the resend link button. ##### 2.2 Resending a magic link After sending the initial magic link to the user, you may want to display a resend button to them. When the user clicks on this button, you should call the following API ```tsx import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function resendMagicLink() { try { let response = await resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } else { // Magic link resent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function resendMagicLink() { try { let response = await supertokensPasswordless.resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } else { // Magic link resent successfully. window.alert("Please check your email for the magic link"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/auth/public/signinup/code/resend' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "deviceId": "...", "preAuthSessionId": "...." }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the magic link was successfully sent. - `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. ##### How to detect if the initial OTP has been sent If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page. To prevent this you need a way to know which UI to show. Since you save the `preAuthSessionId` and `deviceId` after sending the initial magic link, you can know if the user is on either **step 2.1** or **step 2.2**. Check if these tokens are on the device. If they aren't, you should follow **step 2.1**, else follow **step 2.2**. :::note[You need to clear these tokens if:] - the user navigates away from the **step 2.2** page - you get a `RESTART_FLOW_ERROR` at any point in time from an API call - the user has successfully logged in. ::: ```tsx import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function hasInitialMagicLinkBeenSent() { return (await getLoginAttemptInfo()) !== undefined; } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function hasInitialMagicLinkBeenSent() { return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined; } ``` If `hasInitialMagicLinkBeenSent` returns `true`, it means that the user has already sent the initial magic link to themselves, and you can show the resend link UI. Else show a form asking them to enter their email / phone number. ##### 2.3 Consuming the magic link When a user clicks on a magic link, you first need to know if the action came from the same browser/device as the one that started the flow. To do this you ca use this code sample. Since you save the `preAuthSessionId` and `deviceId`, you can check if they exist on the app. If they do, then it's the same device that the user has opened the link on, else it's a different device. ```tsx import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function isThisSameBrowserAndDevice() { return (await getLoginAttemptInfo()) !== undefined; } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function isThisSameBrowserAndDevice() { return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined; } ``` :::note[Add a intermediate step if the user came from a different device.] ::: If the user clicked on a link from a different device, you need to show some kind of an intermediate UI. This is to protect against email clients opening the magic link on their servers and consuming the link. The page should require additional user interaction before consuming the magic link. For example, you could show a button with the following text: `Click here to login into this device`. On click, you can consume the magic link to log the user into that device. With this understanding of how to avoid potential errors, proceed with the actual instructions on how to authenticate with the magic link. You need to remove the `linkCode` and `preAuthSessionId` from the Magic link. For example, if the Magic link is ```tsx import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function handleMagicLinkClicked() { try { let response = await consumeCode(); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home"); } else { // this can happen if the magic link has expired or is invalid // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function handleMagicLinkClicked() { try { let response = await supertokensPasswordless.consumeCode(); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await supertokensPasswordless.clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home"); } else { // this can happen if the magic link has expired or is invalid // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```text https://example.com/auth/verify?preAuthSessionId=PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=#s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs= ``` Then the `preAuthSessionId` is the value of the query parameter `preAuthSessionId` (`PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=` in the example), and the `linkCode` is the part after the `#` (`s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=` in the example). We can then use these to call the consume API ```bash curl --location --request POST '/auth//signinup/code/consume' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "linkCode": "s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=", "preAuthSessionId": "PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=" }' ``` :::info[Multi Tenancy] Use the `tenantId` query parameter from the magic link as ``. If the link has no `tenantId`, use `public`. The create, resend, and OTP-consume endpoints must use the same tenant path; replace `public` in those examples when authenticating another tenant. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. #### OTP The following section shows you what aspects you need to cover to implement the UI for a `OTP`, One-Time Password, flow. The same flow applies during either sign up or sign in. This guide shows you how to determine if you create a new user in the next steps. ##### 2.1 Creating and sending the OTP You have to add a form that asks the user for their email address or phone number. When the users submit the form you have to call the following API to create and send them an OTP. For email based login ```tsx import { createCode } from "supertokens-web-js/recipe/passwordless"; async function sendOTP(email: string) { try { let response = await createCode({ email, }); /** * For phone number, use this: let response = await createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function sendOTP(email: string) { try { let response = await supertokensPasswordless.createCode({ email, }); /** * For phone number, use this: let response = await supertokensPasswordless.createCode({ phoneNumber: "+1234567890" }); */ if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else { // OTP sent successfully. window.alert("Please check your email for an OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you, // or if the input email / phone number is not valid. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/auth/public/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@gmail.com" }' ``` For phone number based login ```bash curl --location --request POST '/auth/public/signinup/code' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+1234567890" }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. The response from the API call is the following object (in case of `status: "OK"`): ```typescript check=false reason="This block documents the response shape rather than executable code." { status: "OK"; deviceId: string; preAuthSessionId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } ``` You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: - Resend a new OTP. - Detect if the user has already sent an OTP before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the enter OTP form with a resend button. - Verify the user's input OTP. ##### 2.2 Resending a OTP After you send the OTP to the user, you may want to display a resend button to them. When the user clicks on this button, you should call the following API ```tsx import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function resendOTP() { try { let response = await resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } else { // OTP resent successfully. window.alert("Please check your email for the OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function resendOTP() { try { let response = await supertokensPasswordless.resendCode(); if (response.status === "RESTART_FLOW_ERROR") { // this can happen if the user has already successfully logged in into // another device whilst also trying to login to this one. // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } else { // OTP resent successfully. window.alert("Please check your email for the OTP"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/auth/public/signinup/code/resend' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "deviceId": "...", "preAuthSessionId": "...." }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: This means that the OTP was successfully sent. - `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. ##### How to detect if the initial OTP has been sent If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page. To prevent this you need a way to know which UI to show. Since you save the `preAuthSessionId` and `deviceId` after sending the initial OTP, you can determine which interface to show. Check if you stored these tokens on the device. If they aren't present, show the form from step 2.1. Otherwise, show the OTP form from step 2.3 with the resend action from step 2.2. :::note[You need to clear these tokens if:] - the user navigates away from the OTP entry page - you get a `RESTART_FLOW_ERROR` at any point in time from an API call - the user has successfully logged in. ::: ```tsx import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function hasInitialOTPBeenSent() { return (await getLoginAttemptInfo()) !== undefined; } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function hasInitialOTPBeenSent() { return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined; } ``` If `hasInitialOTPBeenSent` returns `true`, show the OTP form from step 2.3 with the resend action from step 2.2. Otherwise, show the form from step 2.1 asking users to enter their email or phone number. ##### 2.3 Verifying the OTP When the user enters an OTP you have to call the following API to verify it ```tsx import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; async function handleOTPInput(otp: string) { try { let response = await consumeCode({ userInputCode: otp, }); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home"); } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { // the user entered an invalid OTP window.alert( "Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount), ); } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { // it can come here if the entered OTP was correct, but has expired because // it was generated too long ago. window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function handleOTPInput(otp: string) { try { let response = await supertokensPasswordless.consumeCode({ userInputCode: otp, }); if (response.status === "OK") { // we clear the login attempt info that was added when the createCode function // was called since the login was successful. await supertokensPasswordless.clearLoginAttemptInfo(); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // user sign up success } else { // user sign in success } window.location.assign("/home"); } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { // the user entered an invalid OTP window.alert( "Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount), ); } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { // it can come here if the entered OTP was correct, but has expired because // it was generated too long ago. window.alert("Old OTP entered. Please regenerate a new one and try again"); } else { // this can happen if the user tried an incorrect OTP too many times. // or if it was denied due to security reasons in case of automatic account linking // we clear the login attempt info that was added when the createCode function // was called - so that if the user does a page reload, they will now see the // enter email / phone UI again. await supertokensPasswordless.clearLoginAttemptInfo(); window.alert("Login failed. Please try again"); window.location.assign("/auth"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash curl --location --request POST '/auth/public/signinup/code/consume' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "deviceId": "...", "preAuthSessionId": "...", "userInputCode": "" }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "INCORRECT_USER_INPUT_CODE_ERROR"`: The entered OTP is invalid. The response contains information about the maximum number of retries and the number of failed attempts. - `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. - `status: "RESTART_FLOW_ERROR"`: The user entered invalid OTPs too many times and must restart the flow. - `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. On success, the backend sends session tokens in the response. Web SDK requests handle them automatically. Native SDKs only do so when the request uses their integrated HTTP client or interceptor; raw requests such as the `curl` examples must be implemented through that integration in the app. ### 3. Initialize the backend SDK You need to initialize the **Backend SDK** alongside the code that starts your server. The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. Click one of the options from the next form and the code snippet updates. ```tsx title="Backend SDK Init" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ Passwordless.init({ flowType: "MAGIC_LINK", contactMethod: "EMAIL", }), Session.init(), ], }); ``` ```python title="Backend SDK Init" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import passwordless, session from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features passwordless.init( flow_type="MAGIC_LINK", contact_config=ContactEmailOnlyConfig() ) ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. ConnectionURI: "https://try.supertokens.io", // APIKey: }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "MAGIC_LINK", ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true}, }), session.Init(nil), // initializes session features }, }) if err != nil { panic(err.Error()) } } ``` ## Next steps Having completed the main setup, you can explore more advanced topics related to the **Passwordless** recipe. Change how Magic Links get created. Change the format of the generated One-Time Password. Add custom logic after the logs in or signs up. Customize how emails get delivered to your users. Customize how SMS messages get delivered to your users. --- # Passwordless Authentication Source: https://supertokens.com/docs/authentication/passwordless/introduction ## Passwordless summary - The Passwordless recipe authenticates users with generated Magic Links or One-Time Passwords. - Use the prebuilt UI or implement a custom interface with the SDKs. - Customize Magic Link creation, OTP format, and email or SMS delivery when needed. ## Overview The **Passwordless** `recipe` provides a way of authenticating users through generated credentials like **Magic Links** or **One-Time Passwords**. You can use it out of the box, with the **Pre-Built UI**, or implement your own interface through the available SDKs. Sign in form UI for passwordless login ## Getting started You can either follow setup guide or use the `CLI` tool to generate an example app that shows you how the recipe works. Go through a quick tutorial that shows you how to add the **Passwordless** recipe to your app. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Change how you create Magic Links. Change the format of the generated One-Time Password. Add custom logic after the logs in or signs up. Customize how you deliver emails to your users. Customize how you deliver SMS messages to your users. --- # Implement invite link based sign up Source: https://supertokens.com/docs/authentication/passwordless/invite-link-flow ## Overview In this flow, the admin of the app calls an API to sign up a user and send them an invite link. Once the user clicks on that, they log in and can access the app. If a user has not received an invitation before, their sign in attempt fails. ## Before you start This guide assumes that you have initialized the [Passwordless recipe](/authentication/passwordless/initial-setup), Session, and User Roles, and have a working application integrated with **SuperTokens**. The User Roles recipe protects the invitation endpoint in these examples. If you have not, please check the [Quickstart Guide](/quickstart). ## Steps ### 1. Add the ability to invite new users Add a new endpoint that allows you to invite users to your app. You need to first create the new user and then use the `passwordless` API to send the magic link to them. Additionally, protect the endpoint with a role requirement. The `passwordless` API uses the default magic link path, `/auth/verify`, for the invite link. If you are using the pre-built UI, the frontend SDK automatically logs the user in. For custom UI implementations, use the [`consumeCode` function provided by the frontend SDK](/authentication/passwordless/initial-setup#23-consuming-the-magic-link) to verify the code in the URL and authenticate the user created by the invitation endpoint. Validate and normalize the email address before creating the user. Configure the framework's JSON body parser before this route; for Koa, expose the parsed payload as `ctx.request.body`. ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; let app = express(); app.post( "/create-user", verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }), async (req: SessionRequest, res) => { let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email res.send("Success"); }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/create-user", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }), }, ], }, handler: async (req: SessionRequest, res) => { let email = (req.payload.valueOf() as any).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email res.response("Success").code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; let fastify = Fastify(); fastify.post( "/create-user", { preHandler: verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }), }, async (req, res) => { let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email res.code(200).send("Success"); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; async function createUser(awsEvent: SessionEventV2) { let email = JSON.parse(awsEvent.body!).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email return { statusCode: "200", body: "Success", }; } exports.handler = verifySession(createUser, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; let router = new KoaRouter(); router.post( "/create-user", verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }), async (ctx: SessionContext, next) => { let email = ((ctx.request as any).body as { email: string }).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email ctx.status = 200; ctx.body = "Success"; }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/create-user") @intercept( verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }), ) async handler() { let email = ""; // TODO: get from request body // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email // TODO: send 200 response to the client } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; export default async function createUser(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, })(req, res, next); }, req, res, ); let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email res.status(200).json({ message: "Success" }); } ``` ```tsx check=false reason="This example depends on local application modules." import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; @Controller() export class CreateUserController { @Post("create-user") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async function (globalClaimValidators: any) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }), ) // For more information about this guard please read our NestJS guide. async postAPI(@Session() session: SessionContainer): Promise { let email = ""; // TODO: get from request body // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email // TODO: send 200 response to the client } } ``` ```go import ( "fmt" "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/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI).ServeHTTP(rw, r) }) } func createUserAPI(w http.ResponseWriter, r *http.Request) { email := "" // TODO: read email from request body // This will create the user in supertokens if they don't already exist. tenantId := "public" _, err := passwordless.SignInUpByEmail(tenantId, email) if err != nil { http.Error(w, "Could not create invited user", http.StatusInternalServerError) return } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```go import ( "fmt" "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/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/create-user", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }), createUserAPI) } // This is a function that wraps the supertokens verification function // to work the 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 createUserAPI(c *gin.Context) { email := "" // TODO: read email from request body // This will create the user in supertokens if they don't already exist. tenantId := "public" _, err := passwordless.SignInUpByEmail(tenantId, email) if err != nil { c.String(http.StatusInternalServerError, "Could not create invited user") return } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```go import ( "fmt" "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/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI)) } func createUserAPI(w http.ResponseWriter, r *http.Request) { email := "" // TODO: read email from request body // This will create the user in supertokens if they don't already exist. tenantId := "public" _, err := passwordless.SignInUpByEmail(tenantId, email) if err != nil { http.Error(w, "Could not create invited user", http.StatusInternalServerError) return } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```go import ( "fmt" "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/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI)).Methods(http.MethodPost) } func createUserAPI(w http.ResponseWriter, r *http.Request) { email := "" // TODO: read email from request body // This will create the user in supertokens if they don't already exist. tenantId := "public" _, err := passwordless.SignInUpByEmail(tenantId, email) if err != nil { http.Error(w, "Could not create invited user", http.StatusInternalServerError) return } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { // TODO: send 500 to the client return } fmt.Println(inviteLink) // TODO: send invite link // TODO: send 200 to the client } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from fastapi import Depends from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.post('/create-user') async def create_user(session: SessionContainer = Depends(verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ))): email = "" # TODO: read from request body. # this will creat the user in supertokens if they don't already exist await signinup("public", email, None) invite_link = await create_magic_link("public", email, None) print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python.recipe.passwordless.syncio import create_magic_link, signinup from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @app.route('/create_user', methods=['POST']) @verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) def create_user(): email = "" # TODO: read from request body. # this will creat the user in supertokens if they don't already exist signinup("public", email, None) invite_link = create_magic_link("public", email, None) print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client ``` ```python from django.http import HttpRequest from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim @verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) async def create_user(request: HttpRequest): email = "" # TODO: read from request body. # this will creat the user in supertokens if they don't already exist await signinup("public", email, None) invite_link = await create_magic_link("public", email, None) print(invite_link) # TODO: send invite_link to email # TODO: send 200 responspe to client ``` ```tsx check=false reason="This example depends on local application modules." import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; 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 }); } const body = await request.json(); let email = body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ tenantId: "public", email, }); let inviteLink = await Passwordless.createMagicLink({ tenantId: "public", email, }); // TODO: send inviteLink to user's email return NextResponse.json({ message: "Success" }); }, { overrideGlobalClaimValidators: async function (globalClaimValidators) { return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, }, ); } ``` :::info[Multi Tenancy] The examples use the default `public` tenant. In a multi-tenant application, derive an authorized tenant ID from the authenticated administrator's server-side context; do not trust an arbitrary request-body tenant ID. Pass it to both user creation and magic-link creation so the user and invitation belong to the same tenant. You also need to pass in the `tenantId` to the createMagicLink function which adds the `tenantId` to the generated magic link. The resulting link uses the `websiteDomain` configured in the `appInfo` object in `SuperTokens.init`, but you can change the link's domain to match that of the tenant before sending it. ::: ### 2. Check if a user was invited Update the backend SDK API function to only allow sign up requests from invited users. To do this you need to check if a user exists in **SuperTokens**. ```tsx import Passwordless from "supertokens-node/recipe/passwordless"; import supertokens from "supertokens-node"; Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", flowType: "MAGIC_LINK", override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email, }); let existingPasswordlessUser = existingUsers.find( (user) => user.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined, ); if (existingPasswordlessUser === undefined) { // this is sign up attempt return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin.", }; } } else { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { phoneNumber: input.phoneNumber, }); let existingPasswordlessUser = existingUsers.find( (user) => user.loginMethods.find( (lM) => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless", ) !== undefined, ); if (existingPasswordlessUser === undefined) { // this is sign up attempt return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin.", }; } } return await originalImplementation.createCodePOST!(input); }, }; }, }, }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { passwordless.Init(plessmodels.TypeInput{ Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { if email != nil { existingUser, err := passwordless.GetUserByEmail(tenantId, *email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } else { existingUser, err := passwordless.GetUserByPhoneNumber(tenantId, *phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if existingUser == nil { // sign up attempt return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign ups are disabled. Please contact the admin.", }, }, nil } } return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } return originalImplementation }, }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput def override_passwordless_apis(original_implementation: APIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): if email is not None: existing_user = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) user_with_passwordless = next( ( user for user in existing_user if any( login_method.recipe_id == "passwordless" and login_method.has_same_email_as(email) for login_method in user.login_methods ) ), None, ) if user_with_passwordless is None: # sign up attempt return GeneralErrorResponse("Sign ups disabled. Please contact admin.") else: assert phone_number is not None existing_user = await list_users_by_account_info( tenant_id, AccountInfoInput(phone_number=phone_number) ) user_with_passwordless = next( ( user for user in existing_user if any( login_method.recipe_id == "passwordless" and login_method.has_same_phone_number_as(phone_number) for login_method in user.login_methods ) ), None, ) if user_with_passwordless is None: # sign up attempt return GeneralErrorResponse("Sign ups disabled. Please contact admin.") return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) original_implementation.create_code_post = create_code_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ passwordless.init( flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), ) ], ) ``` --- ## See also --- # Add multiple clients for the same provider Source: https://supertokens.com/docs/authentication/social/add-multiple-clients-for-the-same-provider ## Overview If you use a third-party login method for your web and mobile app, then you might need to setup different Client ID/Secret for the same provider on the backend. For example, in case of Apple login, Apple gives you different client IDs for iOS login vs web & Android login (same client ID for web and Android). ## Before you start This guide assumes that you have already implemented the [EmailPassword recipe](/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). ## Steps ### 1. Update the backend configuration Add more clients to the Apple.init on the backend. Each client would need to be uniquely identified, and you achieve this using the `clientType` string. For example, you can add one `clientType` for `web-and-android` and one for `ios`. ```tsx import { ProviderInput } from "supertokens-node/recipe/thirdparty/types"; let providers: ProviderInput[] = [ { config: { thirdPartyId: "apple", clients: [ { clientType: "web-and-android", clientId: "...", additionalConfig: { keyId: "...", privateKey: "...", teamId: "...", }, }, { clientType: "ios", clientId: "...", additionalConfig: { keyId: "...", privateKey: "...", teamId: "...", }, }, ], }, }, ]; ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { _ = []tpmodels.ProviderInput{{ Config: tpmodels.ProviderConfig{ ThirdPartyId: "apple", Clients: []tpmodels.ProviderClientConfig{ { ClientType: "web-and-android", ClientID: "...", AdditionalConfig: map[string]interface{}{ "keyId": "...", "privateKey": "...", "teamId": "...", }, }, { ClientType: "ios", ClientID: "...", AdditionalConfig: map[string]interface{}{ "keyId": "...", "privateKey": "...", "teamId": "...", }, }, }, }, }} } ``` ```python from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig providers = [ ProviderInput( config=ProviderConfig( third_party_id="apple", clients=[ ProviderClientConfig( client_type="web-and-android", client_id="...", additional_config={ "keyId": "...", "privateKey": "...", "teamId": "...", }, ), ProviderClientConfig( client_type="ios", client_id="...", additional_config={ "keyId": "...", "privateKey": "...", "teamId": "...", }, ), ], ), ), ] ``` ### 2. Update the frontend configuration Use the right `clientType` as shown below: We pass in the `clientType` during the init call. When making calls to the APIs from your mobile app, the request body also takes a `clientType` prop as seen in the above API calls. ```tsx import SuperTokens from "supertokens-web-js"; SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, clientType: "web-and-android", recipeList: [ /*...*/ ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, clientType: "web-and-android", recipeList: [ /*...*/ ], }); ``` If you are using the pre-built UI SDK (SuperTokens-auth-react) as well, you can provide the `clientType` configuration to it as follows: ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "", websiteDomain: "", apiBasePath: "/auth", appName: "...", }, clientType: "web-and-android", recipeList: [ /*...*/ ], }); ``` ## See also --- # Built-in providers Source: https://supertokens.com/docs/authentication/social/built-in-providers-config This page shows a full list of all the built-in providers exposed by **SuperTokens**. ## Google To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en). Set the authorisation callback URL to `/auth/callback/google` ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [ { clientId: "", clientSecret: "", }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "google", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", ClientSecret: "", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="google", clients=[ ProviderClientConfig( client_id="", client_secret="", ), ], ), ), ] ) ) ] ) ``` ## Google workspaces ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google-workspaces", clients: [ { clientId: "TODO", clientSecret: "TODO", additionalConfig: { hd: "example.com", }, }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "google-workspaces", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", AdditionalConfig: map[string]interface{}{ "hd": "example.com", }, }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="google-workspaces", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", additional_config={ "hd": "example.com", }, ), ], ), ), ] ) ) ] ) ``` ## Apple To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003). Initialize the ThirdParty and Session recipes on the frontend and backend. When using the prebuilt UI, add Apple to its frontend provider list. Unlike other providers, Apple sends a form POST to your backend callback instead of redirecting directly to the frontend. Set `redirectURIOnProviderDashboard` to a backend route such as `/auth/callback/apple`, and set `frontendRedirectURI` to the frontend callback page. The backend middleware redirects to that page, which completes authentication by calling `signInAndUp`. Apple doesn't allow `localhost` in the provider callback URL. If you are in `dev` mode, you can use the `dev` keys provided above. ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "apple", clients: [ { clientId: "", additionalConfig: { keyId: "", privateKey: "", teamId: "", }, }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "apple", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "4398792-io.supertokens.example.service", AdditionalConfig: map[string]interface{}{ "keyId": "7M48Y4RYDL", "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", "teamId": "YWQCXGJRJL", }, }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="apple", clients=[ ProviderClientConfig( client_id="4398792-io.supertokens.example.service", additional_config={ "keyId": "7M48Y4RYDL", "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", "teamId": "YWQCXGJRJL", }, ), ], ), ), ] ) ) ] ) ``` ## Discord ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "discord", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "discord", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="discord", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], ), ), ] ) ) ] ) ``` ## Facebook ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "facebook", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "facebook", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="facebook", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], ), ), ] ) ) ] ) ``` ## GitHub To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app). Set the authorisation callback URL to `/auth/callback/github` ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "github", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "github", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="github", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], ), ), ] ) ) ] ) ``` ## GitLab ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "gitlab", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "gitlab", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, OIDCDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="gitlab", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" ), ), ] ) ) ] ) ``` ## Twitter ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "twitter", clients: [ { clientId: "", clientSecret: "", }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "twitter", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", ClientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="twitter", clients=[ ProviderClientConfig( client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", ), ], ), ), ] ) ) ] ) ``` ## LinkedIn ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "linkedin", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "linkedin", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="linkedin", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], ), ), ] ) ) ] ) ``` ## Okta ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "okta", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "okta", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, OIDCDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="okta", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", ), ), ] ) ) ] ) ``` ## SAML ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "boxy-saml", name: "", // Replace with the correct provider name clients: [ { clientId: "TODO", clientSecret: "TODO", additionalConfig: { boxyURL: "", }, }, ], }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "boxy-saml", Name: "", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", AdditionalConfig: map[string]interface{}{ "boxyURL": "", }, }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="boxy-saml", name="", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", additional_config={ "boxyURL": "", }, ), ], ), ), ] ) ) ] ) ``` ## Active Directory ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "active-directory", clients: [ { clientId: "TODO", clientSecret: "TODO", }, ], oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }, }, ], }, }), // initializes signin / sign up features ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "active-directory", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO:", ClientSecret: "TODO:", }, }, OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="active-directory", clients=[ ProviderClientConfig( client_id="TODO:", client_secret="TODO:", ), ], oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", ), ), ] ) ) ] ) ``` Call the following function / API to add the third party provider to a specific tenant. ## Google To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en) Create Google Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "google", name: "Google", clients: [ { clientId: "...", clientSecret: "...", }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "google", Name: "Google", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="google", name="Google", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="google", name="Google", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "google", "name": "Google", "clients": [ { "clientId": "...", "clientSecret": "..." } ] } }' ``` ## Google workspaces Create Google Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "google-workspaces", name: "Google Workspaces", clients: [ { clientId: "...", clientSecret: "...", additionalConfig: { hd: "example.com", }, }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "google-workspaces", Name: "Google Workspaces", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", AdditionalConfig: map[string]interface{}{ "hd": "example.com", }, }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="google-workspaces", name="Google Workspaces", clients=[ ProviderClientConfig( client_id="...", client_secret="...", additional_config={ "hd": "example.com", }, ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="google-workspaces", name="Google Workspaces", clients=[ ProviderClientConfig( client_id="...", client_secret="...", additional_config={ "hd": "example.com", }, ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "google-workspaces", "name": "Google Workspaces", "clients": [ { "clientId": "...", "clientSecret": "...", "additionalConfig": { "hd": "example.com" } } ] } }' ``` ## Apple To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003) Note that Apple doesn't allow `localhost` in the URL. If you are in `dev` mode, you can use the `dev` keys provided above. Call the following function / API to add the third party provider to a specific tenant. Create Apple Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "apple", name: "Apple", clients: [ { clientId: "...", additionalConfig: { keyId: "...", privateKey: "...", teamId: "...", }, }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "apple", Name: "Apple", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", AdditionalConfig: map[string]interface{}{ "keyId": "...", "privateKey": "...", "teamId": "...", }, }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="apple", name="Apple", clients=[ ProviderClientConfig( client_id="...", client_secret="...", additional_config={ "keyId": "...", "privateKey": "...", "teamId": "...", }, ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="apple", name="Apple", clients=[ ProviderClientConfig( client_id="...", client_secret="...", additional_config={ "keyId": "...", "privateKey": "...", "teamId": "...", }, ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "apple", "name": "Apple", "clients": [ { "clientId": "...", "additionalConfig": { "keyId": "...", "privateKey": "...", "teamId": "..." } } ] } }' ``` ## Discord Create Discord Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "discord", name: "Discord", clients: [ { clientId: "...", clientSecret: "...", }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "discord", Name: "Discord", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="discord", name="Discord", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="discord", name="Discord", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "discord", "name": "Discord", "clients": [ { "clientId": "...", "clientSecret": "..." } ] } }' ``` ## Facebook Create Facebook Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "facebook", name: "Facebook", clients: [ { clientId: "...", clientSecret: "...", }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "facebook", Name: "Facebook", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="facebook", name="Facebook", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="facebook", name="Facebook", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "facebook", "name": "Facebook", "clients": [ { "clientId": "...", "clientSecret": "..." } ] } }' ``` ## GitHub To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app) Create GitHub Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "github", name: "GitHub", clients: [ { clientId: "...", clientSecret: "...", }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "github", Name: "GitHub", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="github", name="GitHub", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="github", name="GitHub", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '/recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "github", "name": "GitHub", "clients": [ { "clientId": "...", "clientSecret": "..." } ] } }' ``` ## GitLab Create GitLab Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "gitlab", name: "GitLab", clients: [ { clientId: "...", clientSecret: "...", }, ], oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "gitlab", Name: "GitLab", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, OIDCDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="gitlab", name="Gitlab", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="gitlab", name="Gitlab", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "gitlab", "name": "GitLab", "clients": [ { "clientId": "...", "clientSecret": "..." } ], "oidcDiscoveryEndpoint": "https://gitlab.example.com/.well-known/openid-configuration" } }' ``` ## Twitter Create Twitter Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "twitter", name: "Twitter", clients: [ { clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "twitter", Name: "Twitter", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", ClientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="twitter", name="Twitter", clients=[ ProviderClientConfig( client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="twitter", name="Twitter", clients=[ ProviderClientConfig( client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "twitter", "name": "Twitter", "clients": [ { "clientId": " ", "clientSecret": "" } ] } }' ``` ## LinkedIn Create LinkedIn Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "linkedin", name: "LinkedIn", clients: [ { clientId: "...", clientSecret: "...", }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "linkedin", Name: "LinkedIn", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="linkedin", name="LinkedIn", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="linkedin", name="LinkedIn", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "linkedin", "name": "LinkedIn", "clients": [ { "clientId": "...", "clientSecret": "..." } ] } }' ``` ## Okta Create Okta Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "okta", name: "Okta", clients: [ { clientId: "...", clientSecret: "...", }, ], oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "okta", Name: "Okta", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, OIDCDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="okta", name="Okta", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="okta", name="Okta", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "okta", "name": "Okta", "clients": [ { "clientId": "...", "clientSecret": "..." } ], "oidcDiscoveryEndpoint": "https://dev-.okta.com/.well-known/openid-configuration" } }' ``` ## SAML Create SAML Provider ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "boxy-saml", name: "", clients: [ { clientId: "...", clientSecret: "...", additionalConfig: { boxyURL: "", }, }, ], }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "boxy-saml", Name: "", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", AdditionalConfig: map[string]interface{}{ "boxyURL": "", }, }, }, }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="boxy-saml", name="", clients=[ ProviderClientConfig( client_id="...", client_secret="...", additional_config={ "boxyURL": "", }, ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig tenant_id = "customer1" result = create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="boxy-saml", name="", clients=[ ProviderClientConfig( client_id="...", client_secret="...", additional_config={ "boxyURL": "", }, ), ], )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` ```bash curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "boxy-SAML", "name": "", "clients": [ { "clientId": "...", "clientSecret": "...", "additionalConfig": { "boxyURL": "" } } ] } }' ``` ## Active Directory ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyToTenant() { let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { thirdPartyId: "active-directory", name: "Active Directory", clients: [ { clientId: "...", clientSecret: "...", }, ], oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }); if (resp.createdNew) { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { tenantId := "customer1" resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "active-directory", Name: "Active Directory", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", }, }, OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", }, nil) if err != nil { // handle error } if resp.OK.CreatedNew { // Provider added to customer1 } else { // Existing provider config overwritten for customer1 } } ``` ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig async def some_func(): tenant_id = "customer1" result = await create_or_update_third_party_config(tenant_id, ProviderConfig( third_party_id="active-directory", name="Active Directoy", clients=[ ProviderClientConfig( client_id="...", client_secret="...", ), ], oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", )) if result.status != "OK": print("handle error") elif result.created_new: print("Provider added to customer1") else: print("Existing provider config overwritten for customer1") ``` --- ## See also --- # Implement a custom invite flow Source: https://supertokens.com/docs/authentication/social/custom-invite-flow ## Overview This guide shows you how to disable public sign-ups to allow only certain people to access your app. For third-party login, maintain a list of approved email addresses and validate users against it. ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). ### Prerequisites This guide uses the `UserMetadata` recipe to store the list of approved email addresses. You need to [enable it](/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps ### 1. Implement the approved email list You can store this list in your own database or use the metadata feature provided by SuperTokens. The following code samples show you how to save the approved email list in user metadata. ```tsx import UserMetadata from "supertokens-node/recipe/usermetadata"; function allowlistKey(tenantId: string) { return `emailAllowList:${tenantId}`; } async function addEmailToAllowlist(tenantId: string, email: string) { let existingData = await UserMetadata.getUserMetadata(allowlistKey(tenantId)); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, email]; await UserMetadata.updateUserMetadata(allowlistKey(tenantId), { allowList, }); } async function isEmailAllowed(tenantId: string, email: string) { let existingData = await UserMetadata.getUserMetadata(allowlistKey(tenantId)); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(email); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/usermetadata" ) func allowlistKey(tenantID string) string { return fmt.Sprintf("emailAllowList:%s", tenantID) } func getAllowList(metadata map[string]interface{}) []string { allowList := []string{} rawAllowList, ok := metadata["allowList"].([]interface{}) if !ok { return allowList } for _, value := range rawAllowList { email, ok := value.(string) if ok { allowList = append(allowList, email) } } return allowList } func addEmailToAllowlist(tenantID string, email string) error { existingData, err := usermetadata.GetUserMetadata(allowlistKey(tenantID)) if err != nil { return err } allowList := getAllowList(existingData) allowList = append(allowList, email) _, err = usermetadata.UpdateUserMetadata(allowlistKey(tenantID), map[string]interface{}{ "allowList": allowList, }) return err } func isEmailAllowed(tenantID string, email string) (bool, error) { existingData, err := usermetadata.GetUserMetadata(allowlistKey(tenantID)) if err != nil { return false, err } allowList := getAllowList(existingData) for _, allowedEmail := range allowList { if allowedEmail == email { return true, nil } } return false, nil } ``` ```python from typing import List from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) def allowlist_key(tenant_id: str): return f"emailAllowList:{tenant_id}" async def add_email_to_allow_list(tenant_id: str, email: str): metadataResult = await get_user_metadata(allowlist_key(tenant_id)) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] allow_list.append(email) await update_user_metadata(allowlist_key(tenant_id), { "allowList": allow_list }) async def is_email_allowed(tenant_id: str, email: str): metadataResult = await get_user_metadata(allowlist_key(tenant_id)) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] return email in allow_list ``` ### 2. Check if the email is allowed Update the backend SDK API function to only allow sign-up requests from users whose email addresses are on the approved list. Use the check functions from the previous code snippet. The overrides reject a provider response without an email before the SDK can generate a synthetic email for a provider configured not to require one. ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." import ThirdParty from "supertokens-node/recipe/thirdparty"; import supertokens from "supertokens-node"; class SignUpNotAllowedError extends Error {} ThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signInUp: async function (input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { thirdParty: { id: input.thirdPartyId, userId: input.thirdPartyUserId, }, }); if (existingUsers.length === 0) { if (!input.isVerified) { throw new SignUpNotAllowedError(); } if (!(await isEmailAllowed(input.tenantId, input.email))) { throw new SignUpNotAllowedError(); } } return originalImplementation.signInUp(input); }, }; }, apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: async function (input) { try { const provider = input.provider; const providerWithEmailCheck = provider.type === "oauth2" ? { ...provider, getUserInfo: async (getUserInfoInput: Parameters[0]) => { const userInfo = await provider.getUserInfo(getUserInfoInput); if (userInfo.email === undefined) { throw new SignUpNotAllowedError(); } return userInfo; }, } : { ...provider, getUserInfo: async (getUserInfoInput: Parameters[0]) => { const userInfo = await provider.getUserInfo(getUserInfoInput); if (userInfo.email === undefined) { throw new SignUpNotAllowedError(); } return userInfo; }, }; return await originalImplementation.signInUpPOST!({ ...input, provider: providerWithEmailCheck, }); } catch (err: unknown) { if (err instanceof SignUpNotAllowedError) { return { status: "GENERAL_ERROR", message: "Sign-ups are disabled. Please contact the admin.", }; } throw err; } }, }; }, }, }); ``` Pass the `isEmailAllowed` helper from the previous step to `initThirdPartyWithInvites`, and include the returned recipe in your SuperTokens `RecipeList`. Add your provider configuration to the `TypeInput` below. ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" "github.com/supertokens/supertokens-golang/supertokens" ) var errSignUpNotAllowed = errors.New("sign up not allowed") func initThirdPartyWithInvites(isEmailAllowed func(tenantID, email string) (bool, error)) supertokens.Recipe { return thirdparty.Init(&tpmodels.TypeInput{ Override: &tpmodels.OverrideStruct{ APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { originalSignInUpPOST := *originalImplementation.SignInUpPOST (*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) { providerWithInviteCheck := *provider originalGetUserInfo := provider.GetUserInfo providerWithInviteCheck.GetUserInfo = func(oAuthTokens tpmodels.TypeOAuthTokens, userContext supertokens.UserContext) (tpmodels.TypeUserInfo, error) { userInfo, err := originalGetUserInfo(oAuthTokens, userContext) if err != nil { return tpmodels.TypeUserInfo{}, err } if userInfo.Email == nil { return tpmodels.TypeUserInfo{}, errSignUpNotAllowed } existingUser, err := thirdparty.GetUserByThirdPartyInfo(tenantId, provider.ID, userInfo.ThirdPartyUserId, userContext) if err != nil { return tpmodels.TypeUserInfo{}, err } if existingUser == nil { if !userInfo.Email.IsVerified { return tpmodels.TypeUserInfo{}, errSignUpNotAllowed } allowed, err := isEmailAllowed(tenantId, userInfo.Email.ID) if err != nil { return tpmodels.TypeUserInfo{}, err } if !allowed { return tpmodels.TypeUserInfo{}, errSignUpNotAllowed } } return userInfo, nil } resp, err := originalSignInUpPOST(&providerWithInviteCheck, input, tenantId, options, userContext) if errors.Is(err, errSignUpNotAllowed) { return tpmodels.SignInUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Sign-ups are disabled. Please contact the admin.", }, }, nil } return resp, err } return originalImplementation }, }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from copy import copy from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import thirdparty from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface, APIOptions, RecipeInterface, ) from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo from supertokens_python.recipe.thirdparty.types import ( RawUserInfoFromProvider, ThirdPartyInfo, UserInfo, ) from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput async def is_email_allowed(tenant_id: str, email: str): # from previous code snippet.. return False class SignUpNotAllowedError(Exception): pass 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], ): existing_users = await list_users_by_account_info( tenant_id, AccountInfoInput( third_party=ThirdPartyInfo( third_party_user_id=third_party_user_id, third_party_id=third_party_id, ) ), ) if len(existing_users) == 0: if not is_verified: raise SignUpNotAllowedError() if not await is_email_allowed(tenant_id, email): raise SignUpNotAllowedError() return 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, ) original_implementation.sign_in_up = thirdparty_sign_in_up return original_implementation def override_thirdparty_apis(original_implementation: APIInterface): original_sign_in_up_post = original_implementation.sign_in_up_post async def thirdparty_sign_in_up_post( provider: Provider, redirect_uri_info: Optional[RedirectUriInfo], oauth_tokens: Optional[Dict[str, Any]], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): provider_with_email_check = copy(provider) original_get_user_info = provider.get_user_info async def get_user_info_with_email_check( oauth_tokens: Dict[str, Any], user_context: Dict[str, Any] ) -> UserInfo: user_info = await original_get_user_info(oauth_tokens, user_context) if user_info.email is None: raise SignUpNotAllowedError() return user_info setattr( provider_with_email_check, "get_user_info", get_user_info_with_email_check, ) try: return await original_sign_in_up_post( provider_with_email_check, redirect_uri_info, oauth_tokens, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) except SignUpNotAllowedError: return GeneralErrorResponse( "Sign-ups are disabled. Please contact the admin." ) original_implementation.sign_in_up_post = thirdparty_sign_in_up_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis, functions=override_thirdparty_functions ), ) ], ) ``` ## See also --- # Custom providers Source: https://supertokens.com/docs/authentication/social/custom-providers ## Overview If you can't find a provider in [the built-in list](/authentication/social/built-in-providers-config), you can add your own custom implementation as shown below. :::info[Note] If you think that SuperTokens should support this provider by default, make sure to let the team know [on GitHub](https://github.com/supertokens/supertokens-node/issues/88). ::: --- ## Create a custom provider ### 1. Render the authentication method in the authentication UI Include the provider in the `providers` array in the frontend SDK. :::warning[This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form.] ::: :::warning[This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form.] ::: ```tsx import React from "react"; import SuperTokens from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { id: "custom", name: "X", // Will display "Continue with X" // optional // you do not need to add a click handler to this as // we add it for you automatically. buttonComponent: (props: { name: string }) => (
{"Login with " + props.name}
), }, ], // ... }, // ... }), // ... ], }); ```
You need to build your own UI listing the buttons for each of the social login providers you want your users to use. See [the implementation details page](/authentication/social/initial-setup#2-add-the-login-ui) for what to do after a user clicks one of the buttons. ### 2. Configure the credentials **OAuth** You can define a custom provider in a couple of ways. The simplest method is to provide the configuration for the `AuthorizationEndpoint`, `TokenEndpoint`, and the mapping for how the user's ID and email from the provider's profile information endpoint. This appears below: Select the **public** tenant from the tenant management page and then click on **Add new provider** in the Social/Enterprise Providers section Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "custom", name: "Custom provider", clients: [ { clientId: "...", clientSecret: "...", scope: ["profile", "email"], }, ], authorizationEndpoint: "https://example.com/oauth/authorize", authorizationEndpointQueryParams: { someKey1: "value1", someKey2: null, }, tokenEndpoint: "https://example.com/oauth/token", tokenEndpointBodyParams: { someKey1: "value1", }, userInfoEndpoint: "https://example.com/oauth/userinfo", userInfoMap: { fromUserInfoAPI: { userId: "id", email: "email", emailVerified: "email_verified", }, }, }, }, ], }, }), ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "custom", Name: "Custom provider", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", Scope: []string{"profile", "email"}, }, }, AuthorizationEndpoint: "https://example.com/oauth/authorize", AuthorizationEndpointQueryParams: map[string]interface{}{ // optional "someKey1": "value1", "someKey2": nil, }, TokenEndpoint: "https://example.com/oauth/token", TokenEndpointBodyParams: map[string]interface{}{ // optional "someKey1": "value1", }, UserInfoEndpoint: "https://example.com/oauth/userinfo", UserInfoMap: tpmodels.TypeUserInfoMap{ FromIdTokenPayload: struct { UserId string "json:\"userId,omitempty\"" Email string "json:\"email,omitempty\"" EmailVerified string "json:\"emailVerified,omitempty\"" }{ UserId: "id", Email: "email", EmailVerified: "email_verified", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature from supertokens_python.recipe.thirdparty.provider import UserInfoMap, UserFields init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], authorization_endpoint="https://example.com/oauth/authorize", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, token_endpoint="https://example.com/oauth/token", token_endpoint_body_params={ "someKey1": "value1", }, user_info_endpoint="https://example.com/oauth/userinfo", user_info_map=UserInfoMap( from_id_token_payload=UserFields( user_id="id", email="email", email_verified="email_verified", ), from_user_info_api=UserFields(), ), ), ), ] ) ) ] ) ``` ```bash curl --location --request PUT '/public/recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "custom", "name": "Custom provider", "clients": [{ "clientId": "...", "clientSecret": "...", "scope": ["email", "profile"] }], "authorizationEndpoint": "https://example.com/oauth/authorize", "authorizationEndpointQueryParams": { "someKey1": "value1", "someKey2": "value2" }, "tokenEndpoint": "https://example.com/oauth/token", "tokenEndpointBodyParams": { "someKey1": "value1" }, "userInfoEndpoint": "https://example.com/oauth/userinfo", "userInfoMap": { "fromUserInfoAPI": { "userId": "id", "email": "email", "emailVerified": "email_verified" } } } }' ``` | Configuration Field | Description | Example | |---|---|---| | `thirdPartyId` | Unique identifier for the provider | For Google: `"google"` | | `name` | Display name used on the frontend login button | Setting `"XYZ"` shows "Login using XYZ" | | `clients` | Array of client credentials for frontend clients | Multiple entries needed for web/mobile apps with different credentials. Include `clientType` if using multiple clients | | `AuthorizationEndpoint` | URL for user login | Google: `"https://accounts.google.com/o/oauth2/v2/auth"` | | `AuthorizationEndpointQueryParams` | Optional configuration to modify query parameters | Can add, modify, or remove (using null) query params | | `TokenEndpoint` | API endpoint for exchanging Authorization Code | Google: `"https://oauth2.googleapis.com/token"` | | `TokenEndpointBodyParams` | Optional configuration to modify request body | Can add, modify, or remove (using null) body params | | `UserInfoEndpoint` | API endpoint for getting user information | Google: `"https://www.googleapis.com/oauth2/v1/userinfo"` | | `UserInfoMap.FromUserInfoAPI` | Maps provider's JSON response fields to user info | Example mapping:
`userId: "id"`
`email: "email"`
`emailVerified: "email_verified"`
For nested values use: `userId: "user.id"` | **OIDC** If the provider is Open ID Connect (OIDC) compatible, you can provide URL for the `OIDCDiscoverEndpoint` configuration. The backend SDK automatically discovers authorization endpoint, token endpoint and the user info endpoint by querying the `/.well-known/openid-configuration`. Below is an example of how to set the OIDC discovery endpoint: Select the **public** tenant from the tenant management page and then click on **Add new provider** in the Social/Enterprise Providers section Social/Enterprise providers Select **Add Custom Provider** option New Provider Fill in the details as shown below and click on **Save** OAuth2 provider ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "custom", name: "Custom provider", clients: [ { clientId: "...", clientSecret: "...", scope: ["profile", "email"], }, ], oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", authorizationEndpointQueryParams: { someKey1: "value1", someKey2: null, }, userInfoMap: { fromIdTokenPayload: { userId: "id", email: "email", emailVerified: "email_verified", }, }, }, }, ], }, }), ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "custom", Name: "Custom provider", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", Scope: []string{"profile", "email"}, }, }, OIDCDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", AuthorizationEndpointQueryParams: map[string]interface{}{ // optional "someKey1": "value1", "someKey2": nil, }, UserInfoMap: tpmodels.TypeUserInfoMap{ FromIdTokenPayload: struct { UserId string "json:\"userId,omitempty\"" Email string "json:\"email,omitempty\"" EmailVerified string "json:\"emailVerified,omitempty\"" }{ UserId: "id", Email: "email", EmailVerified: "email_verified", }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature from supertokens_python.recipe.thirdparty.provider import UserInfoMap, UserFields init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="custom", name="Custom Provider", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["email", "profile"], ), ], oidc_discovery_endpoint="https://example.com/.well-known/openid-configuration", authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, }, user_info_map=UserInfoMap( from_user_info_api=UserFields( user_id="id", email="email", email_verified="email_verified", ), from_id_token_payload=UserFields(), ), ), ), ] ) ) ] ) ``` ```bash curl --location --request PUT '/public/recipe/multitenancy/config/thirdparty' \ --header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { "thirdPartyId": "custom", "name": "Custom provider", "clients": [{ "clientId": "...", "clientSecret": "...", "scope": ["email", "profile"] }], "oidcDiscoveryEndpoint": "https://example.com/.well-known/openid-configuration", "authorizationEndpointQueryParams": { "someKey1": "value1", "someKey2": "value2" }, "userInfoMap": { "fromIdTokenPayload": { "userId": "id", "email": "email", "emailVerified": "email_verified" } } } }' ``` - The configuration values are similar to the ones in the "Via OAuth endpoints" method. Please read that section to understand the `thirdPartyId`, `name`, `clients` configuration. - Unlike the "Via OAuth endpoints", you can obtain the user's info from the ID token payload using the configuration specified by you in the `UserInfoMap.FromIdTokenPayload` map. - You can also add the `UserInfoMap.FromUserInfoAPI` map as done in the "Via OAuth endpoints" section. SuperTokens auto merges the user information. --- ## Handle non standard providers. Sometimes, one of the steps in the providers interaction may not be per a standard. Therefore, providing the configuration like shown above may not work. To handle this case, you can override any of the steps that happen during the OAuth exchange. For example, the API call made to get the user's profile info makes a `GET` call to the `UserInfoEndpoint` with the user's access token. If your provider requires a different method or requires multiple calls to different endpoints to get the profile info, then you can override the default implementation as shown below: ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "custom", name: "Custom provider", clients: [ { clientId: "...", clientSecret: "...", scope: ["profile", "email"], }, ], authorizationEndpoint: "https://example.com/oauth/authorize", authorizationEndpointQueryParams: { response_type: "token", // Changing an existing parameter response_mode: "form", // Adding a new parameter scope: null, // Removing a parameter }, tokenEndpoint: "https://example.com/oauth/token", }, override: (originalImplementation) => { return { ...originalImplementation, getUserInfo: async function (input: Parameters[0]) { // Call provider's APIs to get profile info // ... return { thirdPartyUserId: "...", email: { id: "...", isVerified: true, }, rawUserInfoFromProvider: { fromUserInfoAPI: { first_name: "...", last_name: "...", }, }, }; }, }; }, }, ], }, }), ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "custom", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "...", ClientSecret: "...", Scope: []string{"profile", "email"}, }, }, AuthorizationEndpoint: "https://example.com/oauth/authorize", AuthorizationEndpointQueryParams: map[string]interface{}{ "response_type": "token", // Changing an existing parameter "response_mode": "form", // Adding a new parameter "scope": nil, // Removing a parameter }, TokenEndpoint: "https://example.com/oauth/token", }, Override: func(originalImplementation *tpmodels.TypeProvider) *tpmodels.TypeProvider { // ... originalImplementation.GetUserInfo = func(oAuthTokens map[string]interface{}, userContext *map[string]interface{}) (tpmodels.TypeUserInfo, error) { // Call provider's APIs to get profile info // ... return tpmodels.TypeUserInfo{ ThirdPartyUserId: "...", Email: &tpmodels.EmailStruct{ ID: "...", IsVerified: true, }, RawUserInfoFromProvider: tpmodels.TypeRawUserInfoFromProvider{ FromUserInfoAPI: map[string]interface{}{ "first_name": "...", "last_name": "...", // ... }, }, }, nil } return originalImplementation }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty.provider import ProviderClientConfig, ProviderConfig, ProviderInput, Provider from supertokens_python.recipe.thirdparty import SignInAndUpFeature from supertokens_python.recipe.thirdparty.types import UserInfo, UserInfoEmail, RawUserInfoFromProvider from typing import Dict, Any def override_custom_provider(provider: Provider) -> Provider: async def get_user_info(oauth_tokens: Dict[str, Any], user_context: Dict[str, Any]) -> UserInfo: return UserInfo( third_party_user_id="...", email=UserInfoEmail( email="...", is_verified=True, ), raw_user_info_from_provider=RawUserInfoFromProvider( from_id_token_payload={}, from_user_info_api={ "first_name": "...", "last_name": "...", }, ), ) provider.get_user_info = get_user_info return provider init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="custom", clients=[ ProviderClientConfig( client_id="...", client_secret="...", scope=["profile", "email"], ), ], authorization_endpoint="https://example.com/oauth/authorize", authorization_endpoint_query_params={ "response_type": "token", # Changing an existing parameter "response_mode": "form", # Adding a new parameter "scope": None, # Removing a parameter }, token_endpoint="https://example.com/oauth/token", ), override=override_custom_provider ), ] ) ) ] ) ``` The original implementation has 4 functions which can be overridden: 1. `GetConfigForClientType` Selects the client configuration from the list of clients provided and returns the complete provider configuration. This is a good place to override configuration dynamically. For example, if `login_hint` appears in the request, you can add it to the `AuthorizationEndpointQueryParams` by overriding this function. 2. `GetAuthorisationRedirectURL` This function returns the full URL (along with query params) to which the user needs to navigate to log in. 3. `ExchangeAuthCodeForOAuthTokens` This function is responsible for exchanging one time use `Authorization Code` with the user's tokens, such as `Access Token`, `ID Token`, etc. 4. `GetUserInfo` This function is responsible for fetching the user information such as `UserId`, `Email` and `EmailVerified` using the tokens returned from the previous function. --- ## See also --- # Hooks and overrides Source: https://supertokens.com/docs/authentication/social/hooks-and-overrides **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can adjust the `thirdparty` recipe to your needs. Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Frontend hook This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up or signing in. With this method you can fire events immediately after a successful sign in. You can use it to send analytics events. ```tsx import SuperTokens from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } }, }), Session.init(), ], }); ``` ```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIThirdParty.init({ onHandleEvent: async (context) => { if (context.action === "SUCCESS") { if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // TODO: Sign up } else { // TODO: Sign in } } }, }), supertokensUISession.init(), ], }); ``` :::warning[Not applicable] This section is not applicable for custom UI since you are calling the `signInUp` API yourself anyway. You can do anything you want post `signIn` / `signUp` based on the result of the API call. ::: ## Backend override Overriding the `signInUp` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data or trigger actions. ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ /* ... */ ], }, override: { functions: (originalImplementation) => { return { ...originalImplementation, signInUp: async function (input) { // First we call the original implementation of signInUp. let response = await originalImplementation.signInUp(input); // Post sign up response, we check if it was successful if (response.status === "OK") { let { id, emails } = response.user; // This is the response from the OAuth 2 provider that contains their tokens or user info. let providerAccessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; if (input.session === undefined) { if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // TODO: Post sign up logic } else { // TODO: Post sign in logic } } } return response; }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```go import ( "fmt" "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 { // create a copy of the originalImplementation originalSignInUp := *originalImplementation.SignInUp // override the sign in up function (*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) { // First we call the original implementation of SignInUp. response, err := originalSignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext) if err != nil { return tpmodels.SignInUpResponse{}, err } if response.OK != nil { // sign in / up was successful // user object contains the ID and email of the user user := response.OK.User fmt.Println(user) fmt.Println(user.ID) fmt.Println(user.Email) providerAccessToken := response.OK.OAuthTokens["access_token"].(string) firstname := response.OK.RawUserInfoFromProvider.FromUserInfoAPI["first_name"].(string) fmt.Println(providerAccessToken) fmt.Println(firstname) if response.OK.CreatedNewUser { // TODO: Post sign up logic } else { // TODO: Post sign in logic } } return response, nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty.interfaces import ( RecipeInterface, SignInUpOkResult, ) from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider from typing import Dict, Any, Optional, Union from supertokens_python.recipe.session.interfaces import SessionContainer def override_thirdparty_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: 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], ): result = 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, ) if isinstance(result, SignInUpOkResult): # user object contains the ID and email of the user user = result.user print(user) # This is the response from the OAuth 2 provider that contains their tokens or user info. provider_access_token = result.oauth_tokens["access_token"] print(provider_access_token) if result.raw_user_info_from_provider.from_user_info_api is not None: first_name = result.raw_user_info_from_provider.from_user_info_api[ "first_name" ] print(first_name) if session is None: if ( result.created_new_recipe_user and len(result.user.login_methods) == 1 ): print("New user was created") # TODO: Post sign up logic else: print("User already existed and was signed in") # TODO: Post sign in logic return result original_implementation.sign_in_up = sign_in_up return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig( functions=override_thirdparty_functions ), sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[]), ) ], ) ``` --- ## See also --- # Set Up Social Login Source: https://supertokens.com/docs/authentication/social/initial-setup ## Social login integration summary - Configure the ThirdParty and Session recipes on the frontend and backend. With the prebuilt UI, add the required providers to its frontend provider list; with a custom UI, select the provider when starting authorization. Configure provider credentials on the backend, load secrets from environment variables or a secret manager, and keep them out of source control. - For Google, use `thirdPartyId: "google"`, provide the Google client ID and secret, and use the same frontend callback URL throughout the flow. A conventional callback is `/auth/callback/google`; call `signInAndUp` when that page loads. - For Apple, use `thirdPartyId: "apple"` and provide the client ID, key ID, private key, and team ID. Apple sends a form POST to the backend callback instead of redirecting directly to the frontend. - For Apple, set `redirectURIOnProviderDashboard` to the backend callback and `frontendRedirectURI` to the frontend callback page, which completes authentication by calling `signInAndUp`. Add SuperTokens social login to this existing application. Inspect the project stack and existing recipes, then ask which providers are required if they cannot be inferred. Configure the frontend and backend ThirdParty and Session recipes, provider client IDs, callback URLs, auth routes, and environment variables. Keep client secrets out of source control, preserve existing conventions, and validate successful login, denied consent, callback failures, and session creation. ## Overview This page shows you how to authenticate, using **ThirdParty Providers**, with **SuperTokens**. The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ## Steps ### 1. Initialize the frontend SDK #### 1.1 Add the `ThirdParty` recipe in your main configuration file. ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import ThirdParty, { Github, Google, Facebook, Apple } from "supertokens-auth-react/recipe/thirdparty"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [Github.init(), Google.init(), Facebook.init(), Apple.init()], }, }), Session.init(), ], }); ``` #### 1.2 Include the pre-built UI components in your application. In order for the **pre-built UI** to render inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. ```tsx import React from "react"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([ThirdPartyPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([ThirdPartyPreBuiltUI]); } return {/*Your app*/}; } } ``` :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ```tsx import React from "react"; import { BrowserRouter, useRoutes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ ]); const routes = useRoutes([ ...authRoutes.map((route) => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` ::: #### Change the button style On the frontend, you can provide a button component to the in-built providers defining your own UI. The component you add is clickable by default. ```tsx import SuperTokens from "supertokens-auth-react"; import ThirdParty, { Google, Github, Facebook, Apple } from "supertokens-auth-react/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ Github.init({ buttonComponent: (props: { name: string }) =>
, }), Google.init({ buttonComponent: (props: { name: string }) =>
, }), Facebook.init({ buttonComponent: (props: { name: string }) =>
, }), Apple.init({ buttonComponent: (props: { name: string }) =>
, }), ], // ... }, // ... }), // ... ], }); ```
### 2. Initialize the backend SDK You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. ```tsx title="Backend SDK Init" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import ThirdParty from "supertokens-node/recipe/thirdparty"; supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ ThirdParty.init({ /*TODO: See next step*/ }), Session.init(), ], }); ``` ```python title="Backend SDK Init" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features thirdparty.init( # TODO: See next step ) ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. ConnectionURI: "https://try.supertokens.io", // APIKey: }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ thirdparty.Init(&tpmodels.TypeInput{/*TODO: See next step*/}), session.Init(nil), // initializes session features }, }) if err != nil { panic(err.Error()) } } ``` ### 3. Add the authentication providers Populate the `providers` array with the third-party authentication providers that you want. ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { // Load these credentials from environment variables or a secret manager. providers: [ { config: { thirdPartyId: "google", clients: [ { clientId: "", clientSecret: "", }, ], }, }, { config: { thirdPartyId: "github", clients: [ { clientId: "", clientSecret: "", }, ], }, }, { config: { thirdPartyId: "apple", clients: [ { clientId: "", additionalConfig: { keyId: "", privateKey: "", teamId: "", }, }, ], }, }, ], }, }), // ... ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { // Inside supertokens.Init thirdparty.Init(&tpmodels.TypeInput{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ // Load these credentials from environment variables or a secret manager. { Config: tpmodels.ProviderConfig{ ThirdPartyId: "google", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", ClientSecret: "", }, }, }, }, { Config: tpmodels.ProviderConfig{ ThirdPartyId: "github", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", ClientSecret: "", }, }, }, }, { Config: tpmodels.ProviderConfig{ ThirdPartyId: "apple", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", AdditionalConfig: map[string]interface{}{ "keyId": "", "privateKey": "", "teamId": "", }, }, }, }, }, }, }, }) } ``` ```python from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig from supertokens_python.recipe import thirdparty # Inside init thirdparty.init( sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[ # Load these credentials from environment variables or a secret manager. ProviderInput( config=ProviderConfig( third_party_id="google", clients=[ ProviderClientConfig( client_id="", client_secret="", ), ], ), ), ProviderInput( config=ProviderConfig( third_party_id="github", clients=[ ProviderClientConfig( client_id="", client_secret="", ) ], ), ), ProviderInput( config=ProviderConfig( third_party_id="apple", clients=[ ProviderClientConfig( client_id="", additional_config={ "keyId": "", "privateKey": "", "teamId": "" }, ), ], ), ), ]) ) ``` :::note[Replace every credential placeholder with credentials for your own provider application.] Load secrets from environment variables or a secret manager. Do not commit client secrets or Apple private keys to source control. Read the list of [built-in providers](/authentication/social/built-in-providers-config) that also includes information on how to generate your own keys. To add a provider that is not listed, you can follow the guide on [setting up custom providers](/authentication/social/custom-providers). ::: #### Set OAuth scopes To add additional OAuth scopes when accessing your third-party provider, add them to the configuration when initializing the backend `SDK`. For example, if you are using Google as your third-party provider, you can add an additional scope as follows: ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [ { clientId: "TODO: GOOGLE_CLIENT_ID", clientSecret: "TODO: GOOGLE_CLIENT_SECRET", scope: ["scope1", "scope2"], }, ], }, }, ], }, }), ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "google", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO: GOOGLE_CLIENT_ID", ClientSecret: "TODO: GOOGLE_CLIENT_SECRET", Scope: []string{ "scope1", "scope2", }, }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="google", clients=[ ProviderClientConfig( client_id="GOOGLE_CLIENT_ID", client_secret="GOOGLE_CLIENT_SECRET", scope=["scope1", "scope2"] ), ], ), ), ] ) ) ] ) ``` :::note[Along with your custom scopes, also add scopes that ask for the user's email and its verification status. For example, with Google, this scope is `"https://www.googleapis.com/auth/userinfo.email"`.] :::
### 1. Initialize the frontend SDK Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you are using in your setup. Add the `SuperTokens.init` function call at the start of your application. ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import ThirdParty from "supertokens-web-js/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, recipeList: [ThirdParty.init(), Session.init()], }); ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "", apiBasePath: "/auth", }); ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "") .apiBasePath("/auth") .build() } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "", apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "", apiBasePath: "/auth", ); } ``` ### 2. Add the login UI The **ThirdParty** flow involves creating a button for each configured provider so that the user can initiate login. After the user clicks one of those buttons the actions that you need to take differ based on which type of authentication scenario you are using: - **Authorization Code** This option can either involve a **Client Secret** configured on the backend or rely on **Proof Key for Code Exchange (PKCE)** exchange. The difference between the two is that the first option uses a private secret, on the backend, to get the access token. Whereas the second one makes use of the **Proof Key for Code Exchange (PKCE)** flow to perform the token exchange. Regardless of which authentication type you are using, in the end, the access token fetches the user info and logs them in. - **OAuth/Access Tokens** This option only applies to mobile/desktop apps. The frontend obtains the access token and then sends it to the backend. SuperTokens then fetches user info using the access token and logs them in. #### Authorization Code ##### Redirecting to a social/single sign-on provider The first step is to fetch the URL on which the user authenticates. You can do this by querying the backend API exposed by SuperTokens (as shown below). The backend SDK automatically appends the right query params to the URL (like scope, client ID etc). After getting the URL, redirect the user there. In the code below, an example of login with Google appears: ##### Sign in with Apple example ###### Fetching the authorization code on the frontend For React Native apps, set up the [react-native-apple-authentication library](https://github.com/invertase/react-native-apple-authentication). Follow its `README`, and request the email scope when your application uses email identity. Apple may return the user's actual address or a private relay address, and the native credential may include it only on the first authorization. Once the integration is complete, call `appleAuth.performRequest` on iOS or `appleAuthAndroid.signIn` on Android. Send the one-time authorization code to your backend as shown in the next step. A full example of this is available in [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/apple.ts). If you use Expo, you can use the [expo-apple-authentication](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) library instead (note that this library only works on iOS). ###### Fetching the authorization code on the frontend :::info[At the moment this flow is not supported on Android.] ::: ###### Fetching the authorization code on the frontend For iOS, use the native Sign in with Apple flow, then send the authorization code to SuperTokens. You can see a full example of this in the `onAppleClicked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). ###### Fetching the authorization code on the frontend For Flutter, use the [`sign_in_with_apple`](https://pub.dev/packages/sign_in_with_apple) package. Make sure to follow the prerequisite steps to get the package setup. After setup, use the snippet below to trigger the apple sign-in flow. You can see a full example of this in the `loginWithApple` function in [the example app](https://github.com/supertokens/supertokens-flutter/blob/master/examples/with-thirdparty/lib/screens/login.dart). ```tsx import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; async function googleSignInClicked() { try { const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ thirdPartyId: "google", // This is where Google should redirect the user back after login or error. // Configure this URL on the Google provider dashboard as well. frontendRedirectURI: "https:///auth/callback/google", }); /* Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow */ // Redirect the user to Google for authentication. window.location.assign(authUrl); } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```swift import UIKit import AuthenticationServices fileprivate class ViewController: UIViewController, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return view.window! } func loginWithApple() { let authorizationRequest = ASAuthorizationAppleIDProvider().createRequest() authorizationRequest.requestedScopes = [.email, .fullName] let authorizationController = ASAuthorizationController(authorizationRequests: [authorizationRequest]) authorizationController.presentationContextProvider = self authorizationController.delegate = self authorizationController.performRequests() } func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { guard let credential: ASAuthorizationAppleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, let authorizationCode = credential.authorizationCode, let authorizationCodeString = String(data: authorizationCode, encoding: .utf8) else { return } let email = credential.email let firstName = credential.fullName?.givenName let lastName = credential.fullName?.familyName // Send the required authorization code and any profile values Apple returned to the backend. // Persist first-login profile values if your application needs them; Apple may omit them later. } } ``` ```dart import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:sign_in_with_apple/sign_in_with_apple.dart'; Future createAppleMobileTransaction() async { final response = await http.post( Uri.parse("/apple-mobile-transactions"), headers: { "Authorization": "Bearer ", "Content-Type": "application/json", "X-App-Installation-ID": "", }, body: jsonEncode({ "appType": "android", "clientType": "", }), ); if (response.statusCode != 201) { throw StateError("Could not create Apple login transaction"); } return jsonDecode(response.body)["transactionId"] as String; } void loginWithApple() async { try { String? transactionId; if (Platform.isAndroid) { transactionId = await createAppleMobileTransaction(); // Keep a copy in memory until the callback returns. } var credential = await SignInWithApple.getAppleIDCredential( scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], state: transactionId, // Required for Android only webAuthenticationOptions: WebAuthenticationOptions( clientId: "", redirectUri: Uri.parse( "//callback/apple", ), ), ); String authorizationCode = credential.authorizationCode; String? idToken = credential.identityToken; String? email = credential.email; String? firstname = credential.givenName; String? lastName = credential.familyName; if (transactionId != null && credential.state != transactionId) { throw StateError("Apple login transaction mismatch"); } // Send the user information and auth code to the backend. Refer to the next step. } catch (e) { // Sign in aborted or failed } } ``` Apple may return the user's email and full name only the first time the user authorizes your app. Treat those fields as optional, but require the one-time authorization code. If your application needs the profile values, store them during the first successful login rather than requiring Apple to return them again. ##### Handling the auth callback on your frontend Once the third-party provider redirects your user back to your app, you need to consume the information to sign in the user. This requires you to: - Set up a route in your app that handles this callback. It's recommended to use something like `https:///auth/callback/google` (for Google). Regardless of what you make this path, remember to use that same path when calling the `getAuthorisationURLWithQueryParamsAndSetState` function in the first step. - On that route, call the following function on page load ###### Additional steps for Android For Android, a way for the web login flow to redirect back to the app is also needed. By default, the API provided by the backend `SDKs` redirects to the website domain you provide when initializing the `SDK`. The API can be overridden to redirect to the app instead. For example, if using the Node.js `SDK`: Before starting authorization, have the mobile app request an Apple login transaction from an application endpoint on your backend. This is not a SuperTokens API. Generate at least 256 random bits, prefix the identifier with `mobile.`, and store only the opaque identifier server-side with: - the app and configured SuperTokens `clientType`; - the exact Apple callback URL and an allowlisted app deep-link target; - a hash of the initiating app installation, authenticated session, or browser context when one is available; and - an expiry no more than five minutes in the future. Return the identifier to the initiating app over HTTPS and pass it through the provider's `state` field. The `sign_in_with_apple` API exposes `state` but no separate transaction field, so `state` transports this app-defined, namespaced transaction identifier. Do not treat SuperTokens' web state as this mobile transaction. The app must also compare the returned identifier with the value it stored locally before sending the authorization code to `/signinup`. Generate the random portion with `crypto.randomBytes(32)` in Node.js, `crypto/rand.Read` in Go, or `secrets.token_urlsafe(32)` in Python. Never accept a callback or deep-link URI directly from the mobile request; select both from a server-side allowlist for the requested app and client type. At the Apple callback, reject a missing state. Values in the `mobile.` namespace must be consumed with one atomic database operation that verifies every binding and expiry. Reject invalid, mismatched, expired, or previously consumed transactions; never fall back to the web handler for one of these failures. Non-mobile state values can be passed to the original SuperTokens web handler. For example, the application transaction store can atomically consume a PostgreSQL row with: ```sql DELETE FROM apple_login_transactions WHERE id = $1 AND app_type = $2 AND client_type = $3 AND expected_callback = $4 AND expires_at > CURRENT_TIMESTAMP RETURNING app_redirect_uri, initiating_context_hash; ``` Create `id` as a primary key and never reinsert an identifier. The delete and validation must be one database statement, not a read followed by a delete. The `consumeAppleMobileTransaction` functions referenced below are application code that execute this query and return the stored, allowlisted redirect URI; they are not SuperTokens SDK APIs. Apple's provider POST does not contain the originating app's local context. Bind that context when creating the row, redirect only to the stored target, and require the app to compare the returned transaction identifier with its locally stored value before continuing. For browser flows, keep using the SuperTokens web state and original callback handler. **Node.js** In the snippet above for Android, you need an additional `webAuthenticationOptions` property when signing in with Apple. This is because on Android the library uses the web login flow and requires the client ID and redirection URI. The `redirectUri` property here is the URL to which Apple makes a `POST` request after the user has logged in. The SuperTokens backend SDKs provide an API for this at `/`<API_BASE_PATH>`/callback/apple`. Set the app-defined transaction identifier as the `state` argument to `SignInWithApple.getAppleIDCredential` before starting authorization. ```tsx import { signInAndUp } from "supertokens-web-js/recipe/thirdparty"; async function handleGoogleCallback() { try { const response = await signInAndUp(); if (response.status === "OK") { console.log(response.user); if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { // sign up successful } else { // sign in successful } window.location.assign("/home"); } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in / up was not allowed. window.alert(response.reason); } else { // The provider did not supply the email identity required by this configuration. window.alert("No email provided by social login. Please use another form of login"); window.location.assign("/auth"); // redirect back to login page } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```tsx import ThirdParty from "supertokens-node/recipe/thirdparty"; declare function consumeAppleMobileTransaction(input: { id: string; appType: "android"; clientType: string; expectedCallback: string; }): Promise<{ appRedirectURI: string } | undefined>; ThirdParty.init({ override: { apis: (original) => { return { ...original, appleRedirectHandlerPOST: async (input) => { if (original.appleRedirectHandlerPOST === undefined) { throw Error("Should never come here"); } const transactionId = input.formPostInfoFromProvider.state; if (typeof transactionId !== "string" || transactionId.length === 0) { input.options.res.setStatusCode(400); input.options.res.sendHTMLResponse("Invalid Apple login transaction"); return; } if (!transactionId.startsWith("mobile.")) { return await original.appleRedirectHandlerPOST(input); } const transaction = await consumeAppleMobileTransaction({ id: transactionId, appType: "android", clientType: "", expectedCallback: "/auth/callback/apple", }); if (transaction === undefined) { input.options.res.setStatusCode(400); input.options.res.sendHTMLResponse("Invalid Apple login transaction"); return; } const query = new URLSearchParams(); for (const [key, value] of Object.entries(input.formPostInfoFromProvider)) { query.set(key, `${value}`); } const redirectUrl = `${transaction.appRedirectURI}?${query.toString()}#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end`; input.options.res.setHeader("Location", redirectUrl, false); input.options.res.setStatusCode(303); input.options.res.sendHTMLResponse(""); }, }; }, }, }); ``` :::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend SDK.] ::: ##### Special case for login with Apple Unlike other providers, Apple does not redirect your user back to your frontend app. Instead, it redirects the user to your backend with a `FORM POST` request. This means that the URL you configure on Apple's dashboard should point to your backend API layer. Here, **middleware** handles the request and redirects the user to your frontend app. Your frontend app should then call the `signInAndUp` API on that page as shown previously. To tell SuperTokens which frontend route to redirect the user back to, set the `frontendRedirectURI` to the frontend route. Also, set the `redirectURIOnProviderDashboard` to point to your backend API route, to which Apple sends a `POST` request. Follow Apple's official [Configure Sign in with Apple for the web](https://developer.apple.com/help/account/capabilities/configure-sign-in-with-apple-for-the-web/) guide when creating the Services ID and registering the return URL. **Go** Pass your transaction-consumption implementation to `initThirdPartyWithAppleMobile`, and include the returned recipe in your SuperTokens `RecipeList`. The helper must return an error for missing, expired, or mismatched transactions. Add your Apple provider configuration to the `TypeInput` below. ```tsx import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; async function appleSignInClicked() { try { const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ thirdPartyId: "apple", frontendRedirectURI: "https:///auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. redirectURIOnProviderDashboard: "/auth/callback/apple", // Configure this URL on the Apple developer dashboard. }); // Redirect the user to Apple for authentication. window.location.assign(authUrl); } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```go import ( "net/http" "net/url" "strings" "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" "github.com/supertokens/supertokens-golang/supertokens" ) type AppleMobileTransaction struct { AppRedirectURI string } func initThirdPartyWithAppleMobile(consumeAppleMobileTransaction func(transactionID, appType, clientType, expectedCallback string) (AppleMobileTransaction, error)) supertokens.Recipe { return thirdparty.Init(&tpmodels.TypeInput{ Override: &tpmodels.OverrideStruct{ APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { originalAppleRedirectPost := *originalImplementation.AppleRedirectHandlerPOST *originalImplementation.AppleRedirectHandlerPOST = func(formPostInfoFromProvider map[string]interface{}, options tpmodels.APIOptions, userContext *map[string]interface{}) error { transactionID, ok := formPostInfoFromProvider["state"].(string) if !ok || transactionID == "" { http.Error(options.Res, "Invalid Apple login transaction", http.StatusBadRequest) return nil } if !strings.HasPrefix(transactionID, "mobile.") { return originalAppleRedirectPost(formPostInfoFromProvider, options, userContext) } transaction, err := consumeAppleMobileTransaction( transactionID, "android", "", "/auth/callback/apple", ) if err != nil { http.Error(options.Res, "Invalid Apple login transaction", http.StatusBadRequest) return nil } queryParams := url.Values{} for key, value := range formPostInfoFromProvider { if stringValue, ok := value.(string); ok { queryParams.Set(key, stringValue) } } redirectURI := transaction.AppRedirectURI + "?" + queryParams.Encode() + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" options.Res.Header().Set("Location", redirectURI) options.Res.WriteHeader(http.StatusSeeOther) return nil } return originalImplementation }, }, }) } ``` :::info[If you are using the **Authorization Code Grant** flow with **PKCE** you do **not** need to provide a client secret during backend init.] This only works for providers which support the [PKCE flow](https://oauth.net/2/pkce/). ::: **Python** ```python from dataclasses import dataclass from typing import Any, Dict, Optional from urllib.parse import urlencode from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty.interfaces import APIInterface, APIOptions @dataclass class AppleMobileTransaction: app_redirect_uri: str async def consume_apple_mobile_transaction( id: str, app_type: str, client_type: str, expected_callback: str, ) -> Optional[AppleMobileTransaction]: # Atomically consume and return the matching transaction from your database. raise NotImplementedError def override_thirdparty_apis(original_implementation: APIInterface): original_apple_redirect_post = original_implementation.apple_redirect_handler_post async def apple_redirect_handler_post( form_post_info: Dict[str, Any], api_options: APIOptions, user_context: Dict[str, Any] ): transaction_id = form_post_info.get("state") if not isinstance(transaction_id, str) or not transaction_id: api_options.response.set_status_code(400) api_options.response.set_html_content("Invalid Apple login transaction") return if not transaction_id.startswith("mobile."): return await original_apple_redirect_post(form_post_info, api_options, user_context) transaction = await consume_apple_mobile_transaction( id=transaction_id, app_type="android", client_type="", expected_callback="/auth/callback/apple", ) if transaction is None: api_options.response.set_status_code(400) api_options.response.set_html_content("Invalid Apple login transaction") return redirect_url = transaction.app_redirect_uri + "?" + urlencode(form_post_info) + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" api_options.response.set_header("Location", redirect_url) api_options.response.set_status_code(303) api_options.response.set_html_content("") original_implementation.apple_redirect_handler_post = apple_redirect_handler_post return original_implementation thirdparty.init( override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis ), ) ``` In the code above, the `appleRedirectHandlerPOST` API rejects missing state. The explicit `mobile.` namespace selects the mobile flow; absence of state never does. A namespaced value must match and atomically consume a bound transaction before the handler redirects to the allowlisted deep link. Any transaction failure returns `400` instead of falling back to the web flow. Other non-empty values remain SuperTokens web state and go to the original handler. Follow the `sign_in_with_apple` README to configure the Android deep link. ###### Calling the `signinup` API to consume the authorization code Once you have the authorization code from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "apple", "clientType": "...", "redirectURIInfo": { "redirectURIOnProviderDashboard": "/auth/callback/apple", "redirectURIQueryParams": { "code": "...", "user": { "name":{ "firstName":"...", "lastName":"..." }, "email":"..." } } } }' ``` :::note[- On iOS, the client ID set in the backend should be the same as the bundle identifier for your app.] - The `clientType` input is optional and required only if you initialize more than one client in the provider on the backend (See the "Social / `SSO` login for both, web and mobile apps" section below). - On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. - On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. - The `user` object contains optional first-login information provided by Apple. Omit `user`, `name`, or `email` when Apple does not return those values; always send the authorization code. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] ::: ##### Sign in with Google example ###### Fetching the authorization code on the frontend This involves setting up the [@react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) in your app. See their `README` for steps on how to integrate their `SDK` into your application. The minimum scope required by SuperTokens is the one that gives the user's email. Once you configure the library, use `GoogleSignin.configure` and `GoogleSignin.signIn` to trigger the login flow and sign the user in with Google. Refer to [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/google.ts) to see the full code for this. ###### Fetching the authorization code on the frontend Follow the [official Google Sign In guide](https://developers.google.com/identity/sign-in/android/start-integrating) to set up their library and sign the user in with Google. Fetch the authorization code from the Google sign-in result. For a full example, refer to the `signInWithGoogle` function in [the example app](https://github.com/supertokens/supertokens-android/blob/master/examples/with-thirdparty/app/src/main/java/com/supertokens/supertokensexample/LoginActivity.kt). ###### Fetching the authorization code on the frontend For iOS, use the `GoogleSignIn` library. Follow the [official guide](https://developers.google.com/identity/sign-in/ios/start-integrating) to set up the library and sign the user in with Google. Use the result of Google sign-in to get the authorization code. For a full example, refer to the `onGoogleCliked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). ###### Fetching the authorization code on the frontend For Flutter, use the [`google_sign_in`](https://pub.dev/packages/google_sign_in) package. Make sure to follow the prerequisite steps to get the package setup. After setup, use the snippet below to trigger the Google sign-in flow. For a full example, refer to the `loginWithGoogle` in [the example app](https://github.com/supertokens/supertokens-flutter/blob/master/examples/with-thirdparty/lib/screens/login.dart). ```tsx import { GoogleSignin } from "@react-native-google-signin/google-signin"; export const performGoogleSignIn = async (): Promise => { GoogleSignin.configure({ webClientId: "GOOGLE_WEB_CLIENT_ID", iosClientId: "GOOGLE_IOS_CLIENT_ID", }); try { const response = await GoogleSignin.signIn({}); const authCode = response.data?.serverAuthCode; // Refer to step 2 return true; } catch (e) { console.log("Google sign in failed with error", e); } return false; }; ``` ```kotlin import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import android.content.Intent class LoginActivity : AppCompatActivity() { private lateinit var googleResultLauncher: ActivityResultLauncher override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) googleResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { onGoogleResultReceived(it) } } private fun signInWithGoogle() { val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestServerAuthCode("GOOGLE_WEB_CLIENT_ID") .requestEmail() .build() val googleClient = GoogleSignIn.getClient(this, gso) val signInIntent = googleClient.signInIntent googleResultLauncher.launch(signInIntent) } private fun onGoogleResultReceived(it: ActivityResult) { val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) val account = task.result val authCode = account.serverAuthCode // Refer to step 2 } } ``` ```swift import UIKit import GoogleSignIn fileprivate class LoginScreenViewController: UIViewController { @IBAction func onGoogleCliked() { GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in guard error == nil else { return } guard let authCode: String = signInResult?.serverAuthCode as? String else { print("Google login did not return an authorization code") return } // Refer to step 2 } } } ``` ```dart import 'package:google_sign_in/google_sign_in.dart'; import 'dart:io'; Future loginWithGoogle() async { GoogleSignIn googleSignIn; if (Platform.isAndroid) { googleSignIn = GoogleSignIn( serverClientId: "GOOGLE_WEB_CLIENT_ID", scopes: [ 'email', ], ); } else { googleSignIn = GoogleSignIn( clientId: "GOOGLE_IOS_CLIENT_ID", serverClientId: "GOOGLE_WEB_CLIENT_ID", scopes: [ 'email', ], ); } GoogleSignInAccount? account = await googleSignIn.signIn(); if (account == null) { print("Google sign in was aborted"); return; } String? authCode = account.serverAuthCode; if (authCode == null) { print("Google sign in did not return a server auth code"); return; } // Refer to step 2 } ``` ###### Step 2) Calling the `signinup` API to consume the authorization code Once you have the authorization code from the auth provider, you need to call the `signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", "clientType": "...", "redirectURIInfo": { "redirectURIOnProviderDashboard": "", "redirectURIQueryParams": { "code": "...", } } }' ``` :::note[When calling the API exposed by the SuperTokens backend `SDK`, pass an empty string for `redirectURIOnProviderDashboard`.] The native login flow using the authorization code does not involve any redirection on the frontend. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] ::: ##### Authorization code grant flow with `PKCE` This is similar to the first one, except that you do **not** need to provide a client secret during backend init. This flow only works for providers which support the [`PKCE` flow](https://oauth.net/2/pkce/). ###### Calling the `signinup` API to consume the authorization code Once you have the authorization code and `PKCE` verifier from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: ###### Fetching the authorization code on the frontend You can use the [react native auth library](https://github.com/FormidableLabs/react-native-app-auth) to also return the `PKCE` code verifier along with the authorization code. Achieve this by setting the `usePKCE` boolean to `true` and also by setting the `skipCodeExchange` to `true` when configuring the react native auth library. ###### Fetching the authorization code on the frontend You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library to use the `PKCE` flow by using the `setCodeVerifier` method when creating a `AuthorizationRequest`. ###### Fetching the authorization code on the frontend You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) library to use the `PKCE` flow. ###### Fetching the authorization code on the frontend You can use [`flutter_appauth`](https://pub.dev/packages/flutter_appauth) to use the `PKCE` flow by providing a `codeVerifier` when you call the `appAuth.token` function. ```bash curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "THIRD_PARTY_ID", "clientType": "...", "redirectURIInfo": { "redirectURIOnProviderDashboard": "REDIRECT_URI", "redirectURIQueryParams": { "code": "...", }, "pkceCodeVerifier": "..." } }' ``` :::note[- Replace `THIRD_PARTY_ID` with the provider id. The provider id must match the one you configure in the backend when initializing SuperTokens.] - `REDIRECT_URI` must exactly match the value you configure on the providers dashboard. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] ::: #### OAuth/Access Tokens :::info[This flow is not applicable for web apps.] ::: ##### Fetching the OAuth/Access tokens on the frontend 1. Sign in with the social provider. The minimum required scope is the one that provides access to the user's email. You can use any library to sign in with the social provider. 2. Get the access token on the frontend if it is available. 3. Get the id token from the sign in result if it is available. :::note[You need to provide either the access token or the id token, or both in step 2, depending on what is available.] ::: ##### Calling the `signinup` API to use the OAuth tokens Once you have the `access_token` or the `id_token` from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: ```bash curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", "clientType": "...", "oAuthTokens": { "access_token": "...", "id_token": "..." }, }' ``` :::note[- The `clientType` input is optional, and you need it only if you have initialised more than one client in the provider on the backend (See the "Social / Single Sign-On login for both, web and mobile apps" section below).] - If you have the `id_token`, you can send that along with the `access_token`. ::: The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. - `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. :::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] ::: ### 3. Initialize the backend SDK You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. ```tsx title="Backend SDK Init" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import ThirdParty from "supertokens-node/recipe/thirdparty"; supertokens.init({ // Replace this with the framework you are using framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ ThirdParty.init({ /*TODO: See next step*/ }), Session.init(), ], }); ``` ```python title="Backend SDK Init" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features thirdparty.init( # TODO: See next step ) ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```go title="Backend SDK Init" import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. ConnectionURI: "https://try.supertokens.io", // APIKey: }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ thirdparty.Init(&tpmodels.TypeInput{/*TODO: See next step*/}), session.Init(nil), // initializes session features }, }) if err != nil { panic(err.Error()) } } ``` ### 4. Add the authentication providers Populate the `providers` array with the third-party authentication providers that you want. ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { // Load these credentials from environment variables or a secret manager. providers: [ { config: { thirdPartyId: "google", clients: [ { clientId: "", clientSecret: "", }, ], }, }, { config: { thirdPartyId: "github", clients: [ { clientId: "", clientSecret: "", }, ], }, }, { config: { thirdPartyId: "apple", clients: [ { clientId: "", additionalConfig: { keyId: "", privateKey: "", teamId: "", }, }, ], }, }, ], }, }), // ... ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" ) func main() { // Inside supertokens.Init thirdparty.Init(&tpmodels.TypeInput{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ // Load these credentials from environment variables or a secret manager. { Config: tpmodels.ProviderConfig{ ThirdPartyId: "google", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", ClientSecret: "", }, }, }, }, { Config: tpmodels.ProviderConfig{ ThirdPartyId: "github", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", ClientSecret: "", }, }, }, }, { Config: tpmodels.ProviderConfig{ ThirdPartyId: "apple", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "", AdditionalConfig: map[string]interface{}{ "keyId": "", "privateKey": "", "teamId": "", }, }, }, }, }, }, }, }) } ``` ```python from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig from supertokens_python.recipe import thirdparty # Inside init thirdparty.init( sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[ # Load these credentials from environment variables or a secret manager. ProviderInput( config=ProviderConfig( third_party_id="google", clients=[ ProviderClientConfig( client_id="", client_secret="", ), ], ), ), ProviderInput( config=ProviderConfig( third_party_id="github", clients=[ ProviderClientConfig( client_id="", client_secret="", ) ], ), ), ProviderInput( config=ProviderConfig( third_party_id="apple", clients=[ ProviderClientConfig( client_id="", additional_config={ "keyId": "", "privateKey": "", "teamId": "" }, ), ], ), ), ]) ) ``` :::note[Replace every credential placeholder with credentials for your own provider application.] Load secrets from environment variables or a secret manager. Do not commit client secrets or Apple private keys to source control. Read the list of [built-in providers](/authentication/social/built-in-providers-config) that also includes information on how to generate your own keys. To add a provider that is not listed, you can follow the guide on [setting up custom providers](/authentication/social/custom-providers). ::: #### Set OAuth scopes To add additional OAuth scopes when accessing your third-party provider, add them to the configuration when initializing the backend `SDK`. For example, if you are using Google as your third-party provider, you can add an additional scope as follows: ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [ { clientId: "TODO: GOOGLE_CLIENT_ID", clientSecret: "TODO: GOOGLE_CLIENT_SECRET", scope: ["scope1", "scope2"], }, ], }, }, ], }, }), ], }); ``` ```go import ( "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{ SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { Config: tpmodels.ProviderConfig{ ThirdPartyId: "google", Clients: []tpmodels.ProviderClientConfig{ { ClientID: "TODO: GOOGLE_CLIENT_ID", ClientSecret: "TODO: GOOGLE_CLIENT_SECRET", Scope: []string{ "scope1", "scope2", }, }, }, }, }, }, }, }), }, }) } ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ thirdparty.init( sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="google", clients=[ ProviderClientConfig( client_id="GOOGLE_CLIENT_ID", client_secret="GOOGLE_CLIENT_SECRET", scope=["scope1", "scope2"] ), ], ), ), ] ) ) ] ) ``` :::note[Along with your custom scopes, also add scopes that ask for the user's email and its verification status. For example, with Google, this scope is `"https://www.googleapis.com/auth/userinfo.email"`.] ::: ## Next steps Having completed the main setup, you can explore more advanced topics related to the **ThirdParty** recipe. Read more about the common providers exposed by the recipe. See how you can create your own custom provider. Disable public sign ups and use your own invite flow. Add custom logic after the logs in or signs up. --- # Social Login Source: https://supertokens.com/docs/authentication/social/introduction ## Social login summary - The ThirdParty recipe authenticates users through third-party providers with either the prebuilt UI or a custom SDK-based interface. - Configure a built-in provider such as Google or Apple, or implement a custom provider. - Hooks and overrides add custom sign-in logic. A custom invite flow can disable public sign-ups. ## Overview The **ThirdParty** `recipe` provides a way of authenticating users through a third party provider. You can use it out of the box, with the **Pre-Built UI**, or implement your own interface through the available SDKs. Sign in form UI for social login ## Getting started You can either follow the quickstart tutorial or use the `CLI` tool to generate an example app that shows you how the recipe works. Go through a quick tutorial that shows you how to add the **ThirdParty** recipe to your app. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Read more about the common providers exposed by the recipe. See how you can create your own custom provider. Add custom logic after the logs in or signs up. Disable public sign ups and use your own invite flow. --- # Add custom claims in tokens Source: https://supertokens.com/docs/authentication/unified-login/add-custom-claims-in-tokens ## Overview If you want to add custom properties in the token payloads you can do this by using overrides. --- ## Add claims in the OAuth2 Access Token Override the `buildAccessTokenPayload` function to include the custom claims. :::warning[At the moment there is no support for creating OAuth2 providers in the Go SDK.] ::: Override the `build_access_token_payload` function to include the custom claims. ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ override: { functions: (originalImplementation) => ({ ...originalImplementation, buildAccessTokenPayload: async (input) => { const addedInfo: Record = {}; if (input.scopes.includes("profile")) { addedInfo.profile = "custom-value"; } return { ...(await originalImplementation.buildAccessTokenPayload(input)), ...addedInfo, }; }, }), }, }); ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_build_access_token_payload = original_implementation.build_access_token_payload async def build_access_token_payload( user: Optional[User], client: OAuth2Client, session_handle: Optional[str], scopes: List[str], user_context: Dict[str, Any], ) -> Dict[str, Any]: added_info = {} if "profile" in scopes: added_info['profile'] = "custom-value" original_payload = await original_build_access_token_payload( user, client, session_handle, scopes, user_context ) return {**original_payload, **added_info} original_implementation.build_access_token_payload = build_access_token_payload return original_implementation init( framework="...", app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init( override=oauth2provider.InputOverrideConfig(functions=override_oauth2provider_functions) ) ], ) ``` --- ## Add claims in the ID Token Override the `buildIdTokenPayload` function to include the custom claims. :::warning[At the moment there is no support for creating OAuth2 providers in the Go SDK.] ::: Override the `build_id_token_payload` function to include the custom claims. ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ override: { functions: (originalImplementation) => ({ ...originalImplementation, buildIdTokenPayload: async (input) => { const addedInfo: Record = {}; if (input.scopes.includes("profile")) { addedInfo.profile = "custom-value"; } return { ...(await originalImplementation.buildIdTokenPayload(input)), ...addedInfo, }; }, }), }, }); ``` ```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import User from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_build_id_token_payload = original_implementation.build_id_token_payload async def build_id_token_payload( user: Optional[User], client: OAuth2Client, session_handle: Optional[str], scopes: List[str], user_context: Dict[str, Any], ) -> Dict[str, Any]: added_info = {} if "profile" in scopes: added_info['profile'] = "custom-value" original_payload = await original_build_id_token_payload( user, client, session_handle, scopes, user_context ) return {**original_payload, **added_info} original_implementation.build_id_token_payload = build_id_token_payload return original_implementation init( framework="...", app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init( override=oauth2provider.InputOverrideConfig(functions=override_oauth2provider_functions) ) ], ) ``` --- # Introduction Source: https://supertokens.com/docs/authentication/unified-login/introduction ## Overview The **Unified Login** feature helps you in scenarios which involve different types of applications using a common **Authorization Server**. With it you can configure a common **OAuth2 Provider** that authenticates all your clients. ## Prerequisites Before you can dive deeper in the functionality there are a few things to keep in mind: - The feature is available with the **SuperTokens Managed Service**. It is not included in the **Self-Hosted** version. - You can use it with the `Node.js` or the `Python` backend SDKs. On `golang` you have to wait for the next releases or configure a separate **Authorization Service**. - Magic link based login is not supported. However, you can switch to `email`/`SMS` **OTP** instead. This method offers the same level of security. - *Step Up Authentication* is not available out of the box. You have to use customizations to support the flow. ## Getting started Three separate quickstart guides are available for you to follow. They organize the content based on the specific use case that you want to implement. Before you explore a guide, read through the **OAuth2 Basics** page first. It explains concepts used in each tutorial. Go through a quick summary of the OAuth2 specifications to get accustomed with the language used in the quickstart guides. Implement an authentication flow that involves multiple frontend applications that communicate with a common backend. Implement an authentication flow that involves multiple frontend applications that communicate with separate backends. Use a common authentication service for both web and mobile applications. ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. Discover the built-in scopes and see how you can override them. Learn how to validate tokens. Modify the token payload with custom claims. --- # OAuth 2.0 Basics Source: https://supertokens.com/docs/authentication/unified-login/oauth2-basics ## Overview Each quickstart guide uses OAuth 2.0-specific concepts that you should be aware of. Read through this page to get a better understanding of the specifications. **OAuth 2.0** is an industry-standard authorization framework that enables applications to obtain limited access to a user's resources without exposing their credentials. OpenID Connect (OIDC) adds an identity layer to OAuth 2.0. ## Terminology ### Roles In OAuth, roles define the different responsibilities of entities involved in the process of granting and obtaining access to protected resources. The specification defines four roles: #### Resource Owner The **Resource Owner** is an entity capable of granting access to a protected resource. This is an actual person that uses an application. #### Client An **OAuth 2.0 Client** is an application that wants to access protected resources. It needs to get an [**OAuth2 Access Token**](#oauth2-access-token) from the [**Authorization Server**](#authorization-server). With that token the client can perform authorized operations on behalf of the [**Resource Owner**](#resource-owner). The term **client** does not imply any particular implementation characteristics (for example, whether the application executes on a server, a desktop, or other devices). OAuth 2.0 distinguishes between two client types: - A **confidential client** runs in an environment that can protect credentials, such as an application backend. It can authenticate at the token endpoint with a client secret or another supported method. Never send its secret to a browser or native application. - A **public client** runs in an environment that cannot protect credentials, such as browser JavaScript or a native application. Configure it with `tokenEndpointAuthMethod: "none"`; it must not have or ship a client secret. Public clients must use the authorization code flow with Proof Key for Code Exchange (PKCE). #### Resource Server The server hosting the protected resources, capable of accepting and responding to protected resource requests using [**OAuth2 Access Tokens**](#oauth2-access-token). Some real-world examples in this case would be things like: - A file storage service that allows users to access only their files - A social media application that allows users to access only posts from their friends - A chat app that shows only messages from conversations in which the user is a participant #### Authorization Server The server issuing [**OAuth2 Access Tokens**](#oauth2-access-token) to the [**Client**](#client) after successfully authenticating the [**Resource Owner**](#resource-owner). ### Tokens Tokens are strings that represent the authorization issued to the [**Client**](#client). They are mainly used to access protected resources on behalf of the [**Resource Owner**](#resource-owner). At the same time, tokens can provide more information about who the owner is. #### OAuth2 Access Token This is the main token that provides temporary access to protected resources. The **OAuth2 Access Token** should only be accessed and validated by the [**Resource Server**](#resource-server). :::info[This token is different from the **SuperTokens Session Access Token**.] The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. ::: #### OAuth2 Refresh Token A token that allows obtaining a new [**OAuth2 Access Token**](#oauth2-access-token) when the current one has expired. Using the refresh token does not require the user to re-authenticate. :::info[This token is different from the **SuperTokens Session Refresh Token**.] The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. ::: #### ID Token This token provides identity information about the [**Resource Owner**](#resource-owner). Unlike [**OAuth2 Access Tokens**](#oauth2-access-token), the **ID Tokens** should only be accessed by the [**Client**](#client). ### Scopes Scopes define the range of access that the [**Client**](#client) is requesting on behalf of the [**Resource Owner**](#resource-owner). They specify what portions of the **Resource Owner’s** data the **Client** can access and what actions it can perform. For example, when a user grants a web application permission to read their email, the application might request the `email` scope. In a general authentication flow scopes get used in the following way: 1. When the **Client** gets created, it configures a series of scopes for the **Authorization Server**. 2. The **Authorization Server** authenticates the **Resource Owner** and uses the scopes to generate an **OAuth2 Access Token**. 3. The **Resource Server** checks the scopes of the **OAuth2 Access Token** and only allows the requested actions. ### Authorization flows The **OAuth 2.0** protocol defines several *flows* to accommodate different use cases. They are a set of steps an **OAuth Client** has to perform to obtain an access token. Our implementation supports the following flow types: #### [Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) Authorization Code Grant This flow is appropriate for confidential web applications and, with PKCE, public browser and native applications. It consists of the following steps: 1. The **Client** redirects the **Resource Owner** to the **Authorization Server’s** authorization endpoint. 2. If the **Resource Owner** grants permission, the **Authorization Server** redirects their browser back to the specified **Redirect URI** and includes an **Authorization Code** as a query parameter. 3. The **Client** then sends a request to the **Authorization Server**’s token endpoint, including the **Authorization Code**. A confidential client authenticates at this endpoint; a public client supplies its PKCE code verifier instead of a client secret. 4. The **Authorization Server** verifies the information sent by the **Client** and, if valid, issues an **OAuth2 Access Token**. 5. The token can make requests to the **Resource Server** to access the protected resources on behalf of the **Resource Owner**. ##### Authorization code An **Authorization Code** is a short-lived code that the [**Authorization Server**](#authorization-server) provides to the [**Client**](#client), via a **Redirect URI**, after authorization approval. This code then gets exchanged for an [**OAuth2 Access Token**](#oauth2-access-token). For confidential clients, the **Authorization Code** flow keeps tokens out of the user agent by letting the [**Client's**](#client) backend communicate with the [**Authorization Server**](#authorization-server). Public clients exchange the code directly and must use PKCE. ##### Proof key for code exchange (PKCE) The **Authorization Code flow** uses [**PKCE**](https://oauth.net/2/pkce/) to bind the authorization request to the token request. PKCE mitigates authorization-code interception and injection. It is mandatory for public browser and native clients and is recommended for confidential clients. At the beginning of the authentication flow, the **Client** generates a random *code verifier* and sends its derived code challenge in the authorization request. The client must provide the original verifier during the code exchange, so an intercepted code alone is insufficient. PKCE is not a replacement for request binding. Generate an unpredictable `state`, bind it to the initiating browser transaction, and verify it exactly on callback before exchanging the code. For OIDC, also generate and validate a `nonce`, and validate the ID token's issuer, audience, signature, expiry, and nonce. Do not accept callback parameters that are not bound to the transaction that initiated login. #### [Client credentials](https://oauth.net/2/grant-types/client-credentials/) Client Credentials Grant This flow is best suited for **machine-to-machine** (M2M) interactions where there is no end-user. It consists of the following steps: 1. The **Client** authenticates with the **Authorization Server** using its own credentials. 2. The **Authorization Server** verifies the credentials. 3. The **Authorization Server** returns an **OAuth2 Access Token**. 4. The **Client** uses the **OAuth2 Access Token** to access protected resources. 5. The **Resource Server** validates the **OAuth2 Access Token**. 6. If the validation is successful, the **Resource Server** returns the requested resources. --- # Multiple frontend domains with a common backend Source: https://supertokens.com/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend ## Overview Use this guide when multiple frontend applications call the same backend service. In this topology, each browser application exchanges its own authorization code, so each is a **public OAuth client**. The authentication flow works in the following way: 1. **The User accesses the frontend application:** - The application `frontend` redirects the user to the **Authorization Service** backend, using the authorize URL. - The **Authorization Service** backend redirects the user to the login UI. 2. **The User completes the login attempt:** - The **Authorization Service** backend redirects the user to the `callback URL`. 3. **The user accesses the callback URL:** - The frontend verifies the callback `state`, then exchanges the Authorization Code with its PKCE code verifier. It never uses a client secret. Multiple Frontend Domains with a Single Backend ## Before you start :::info If your frontend applications are on the same **domain**, but on different **sub-domains**, you can use [Session Sharing Across Subdomains](/post-authentication/session-management/share-session-across-sub-domains). ::: ## Steps ### 1. Enable the Unified Login feature Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Features**. Enable **Unified Login**. Changes are saved automatically. ### 2. Create the OAuth2 Clients For each frontend application, create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). Call the **SuperTokens Core** API from a trusted administrative environment. The examples create public clients: `tokenEndpointAuthMethod` is `none`, no secret is issued or shipped, and `allowedCorsOrigins` contains only the exact origin that may call the token endpoint. Each application must use authorization code with PKCE. **Examples** ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "allowedCorsOrigins": ["https://"], "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] } ' ``` ```tsx const BASE_URL = ""; const API_KEY = ""; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: "POST", headers: { "api-key": API_KEY, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify({ clientName: "", responseTypes: ["code"], grantTypes: ["authorization_code", "refresh_token"], tokenEndpointAuthMethod: "none", allowedCorsOrigins: ["https://"], audience: [""], scope: "offline_access ", redirectUris: ["https:///oauth/callback"], }), }; fetch(url, options) .then((response) => response.json()) .then((json) => console.log(json)) .catch((err) => console.error(err)); ``` ```go import ( "fmt" "net/http" "strings" "io" ) func main() { baseUrl := "" apiKey := "" url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) payload := `{ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "allowedCorsOrigins": ["https://"], "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] }` req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) req.Header.Add("accept", "application/json") req.Header.Add("api-key", apiKey) req.Header.Add("content-type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```python import requests from typing import Dict, Any BASE_URL = "" API_KEY = "" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "allowedCorsOrigins": ["https://"], "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` **Details** Creates an OAuth2 client **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. ## Request ### Body Schema | Name | Type | Description | Required | Default Value | |--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| | `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | | `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | | `redirectUris` | `array` of `string` | Exact redirect URIs registered for the client. Wildcards are not supported. | Yes | - | | `allowedCorsOrigins` | `array` of `string` | Exact browser origins allowed to call OAuth endpoints. | No | - | | `audience` | `array` of `string` | Resource-server identifiers allowed in access tokens. | No | - | | `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | | `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | | `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | | `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | | `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | | `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | | `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | | `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | | `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | #### GrantType - `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. - `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. - `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. #### TokenEndpointAuthMethod - `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. - `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. - `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. - `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). #### ResponseType - `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. - `id_token`: Indicates that the Client expects an ID Token. #### Time Duration A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. ### Example ```bash curl -X POST /recipe/oauth/clients \ -H "Content-Type: application/json" \ -H "api-key: " \ -d '{ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "allowedCorsOrigins": ["https://"], "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] }' ``` ## Response ### 200 The client has been successfully created. ### Relevant response fields The response includes the persisted client configuration, including the fields below. | Property | Type | Description | |-------------|----------------------------------|-----------------------------------------------| | `clientName` | `string` | The name of the client. | | `clientId` | `string` | Unique identifier for the client. | | `clientSecret` | `string` | Client secret for a confidential client. Omitted for a public client. Treat it as a credential and keep it on a trusted backend. | | `redirectUris` | `array` of `string` | The URLs used for redirection. | | `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | | `scope` | `string` | A space-separated string of scopes that the client can request. | | `responseTypes` | `array` of `string` | Registered response types. | | `grantTypes` | `array` of `string` | Registered grant types. | | `tokenEndpointAuthMethod` | `string` | Token endpoint authentication method. | | `allowedCorsOrigins` | `array` of `string` | Exact browser origins allowed to call OAuth endpoints. | | `enableRefreshTokenRotation` | `boolean` | Whether refresh token rotation is enabled. | #### Example ```json { "clientName": "", "clientId": "", "tokenEndpointAuthMethod": "none", "allowedCorsOrigins": ["https://"], "audience": [""], "redirectUris": ["https:///oauth/callback"], "scope": "offline_access " } ``` :::warning[Protect OAuth client credentials] Core persists the client configuration and encrypts confidential client secrets at rest. Store any returned client secret in a secret manager and expose it only to the application backend. Public clients do not receive or use a client secret. ::: ### 3. Set up the Authorization Service Backend #### 3.1 Initialize the OAuth2 recipe Update the `supertokens.init` call to include the new recipe. :::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] ::: ```typescript import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ emailpassword.init(), oauth2provider.init(), ], ) ``` #### 3.2 Update the CORS configuration Set up the Backend API to allow requests from all the frontend domains. :::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] ::: ```tsx import express from "express"; import cors from "cors"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/express"; const app = express(); // Add your actual frontend domains here const allowedOrigins = ["", "", ""]; app.use( cors({ origin: allowedOrigins, allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }), ); ``` ```python from supertokens_python import get_all_cors_headers from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from supertokens_python.framework.fastapi import get_middleware app = FastAPI() app.add_middleware(get_middleware()) app.add_middleware( CORSMiddleware, allow_origins=[ "", "", "" ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) ``` #### 3.3 Implement a custom session verification function Given that the backend, the **Authorization Server**, also acts as a **Resource Server** you have to account for this in the session verification process. This is necessary because the flow uses two types of tokens: - **SuperTokens Session Access Token**: Used during the login and logout. - **OAuth2 Access Token**: Used to access protected resources and perform actions that need authorization. Hence the logic should distinguish between these two and prevent errors. Configure `EXPECTED_ISSUER` from the authorization server discovery document and compare it exactly. The released OAuth2Provider validators verify the signature and expiry; the examples also require the configured client ID, audience, and scopes. `checkDatabase`/`check_database` additionally rejects revoked or otherwise inactive tokens. Here is an example of how to implement this in the context of an Express API: :::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] ::: ```tsx import express, { NextFunction, Request, Response } from "express"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import Session from "supertokens-node/recipe/session"; const EXPECTED_CLIENT_ID = ""; const EXPECTED_AUDIENCE = ""; const EXPECTED_ISSUER = ""; // Usually /auth const REQUIRED_SCOPES = [""]; interface RequestWithUserId extends Request { userId?: string; } function getBearerToken(req: Request): string { const authorization = req.header("authorization"); if (authorization === undefined || !authorization.startsWith("Bearer ")) { throw new Error("Missing bearer token"); } return authorization.slice("Bearer ".length); } async function verifySession(req: RequestWithUserId, res: Response, next: NextFunction) { try { let session; try { session = await Session.getSession(req, res, { sessionRequired: false }); } catch (error) { if ( !Session.Error.isErrorFromSuperTokens(error) || (error.type !== Session.Error.TRY_REFRESH_TOKEN && error.type !== Session.Error.UNAUTHORISED) ) { throw error; } } if (session !== undefined) { req.userId = session.getUserId(); return next(); } const validation = await OAuth2Provider.validateOAuth2AccessToken( getBearerToken(req), { clientId: EXPECTED_CLIENT_ID, audience: EXPECTED_AUDIENCE, scopes: REQUIRED_SCOPES, }, true, ); if (validation.payload.iss !== EXPECTED_ISSUER || typeof validation.payload.sub !== "string") { throw new Error("Unexpected OAuth token issuer or subject"); } req.userId = validation.payload.sub; return next(); } catch (error) { return next(error); } } const app = express(); app.get("/protected", verifySession, async (req, res) => { // Custom logic }); ``` ```python from fastapi.requests import Request from supertokens_python.recipe.oauth2provider.interfaces import ( OAuth2TokenValidationRequirements, ) from supertokens_python.recipe.oauth2provider.syncio import ( validate_oauth2_access_token, ) from supertokens_python.recipe.session.exceptions import ( SuperTokensSessionError, TryRefreshTokenError, UnauthorisedError, ) from supertokens_python.recipe.session.syncio import get_session EXPECTED_CLIENT_ID = "" EXPECTED_AUDIENCE = "" EXPECTED_ISSUER = "" # Usually /auth REQUIRED_SCOPES = [""] def get_bearer_token(request: Request) -> str: authorization = request.headers.get("authorization") if authorization is None or not authorization.startswith("Bearer "): raise ValueError("Missing bearer token") return authorization.removeprefix("Bearer ") def verify_session(request: Request) -> str: session = None try: session = get_session(request, session_required=False) except SuperTokensSessionError as error: if not isinstance(error, (TryRefreshTokenError, UnauthorisedError)): raise if session is not None: return session.get_user_id() validation = validate_oauth2_access_token( get_bearer_token(request), OAuth2TokenValidationRequirements( client_id=EXPECTED_CLIENT_ID, audience=EXPECTED_AUDIENCE, scopes=REQUIRED_SCOPES, ), check_database=True, ) payload = validation.payload if payload.get("iss") != EXPECTED_ISSUER or not isinstance(payload.get("sub"), str): raise ValueError("Unexpected OAuth token issuer or subject") return payload["sub"] ``` For more information on how to verify the **OAuth2 Access Tokens**, please check the [separate guide](/authentication/unified-login/verify-tokens). ### 4. Configure the Authorization Service Frontend #### 4.1 Initialize the recipe Add the import statement for the new recipe and update the list of recipes to also include the new initialization. Update the `AuthComponent` to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array. Update the `AuthView` component to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. ```tsx import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` ```tsx title="/app/auth/auth.component.ts" import { init as supertokensUIInit } from "supertokens-auth-react"; import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; import { DOCUMENT } from "@angular/common"; @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ // Don't forget to also include the other recipes that you are already using supertokensUIOAuth2Provider.init(), ], }); }; this.renderer.appendChild(this.document.body, script); } } ```
```html import {init as supertokensUIInit} from "supertokens-auth-react"; import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; ```
##### Include the pre-built UI in the rendering tree. ```tsx import React from "react"; import { BrowserRouter, Routes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx import React from "react"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI]); } return {/*Your app*/}; } } ``` #### 4.2 Disable network interceptors The **Authorization Service Frontend** that you are configuring makes use of two types of access tokens: - **SuperTokens Session Access Token**: Used only during the login flow to keep track of the authentication state. - **OAuth2 Access Token**: Returned after a successful login attempt. It can then access protected resources. By default, the **SuperTokens** frontend SDK intercepts all the network requests sent to your Backend API and adjusts them based on the **SuperTokens Session Tokens**. This allows operations, such as automatic token refreshing or adding authorization headers, without needing to configure anything else. Given that in the scenario you are implementing, the **OAuth2 Access Tokens** serve authorization purposes. The automatic request interception causes conflicts. To prevent this, you need to override the `shouldDoInterceptionBasedOnUrl` function in the `Session.init` call. :::warning[The code samples assume that you are using `/auth` as the `apiBasePath` for the backend authentication routes.] If that is different please adjust them based on your use case. ::: You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import Session from "supertokens-auth-react/recipe/session"; Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); // Interception should be done only for routes that need the SuperTokens Session Tokens const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); if (!isAuthApiRoute || isOAuth2ApiRoute) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) import supertokensUISession from "supertokens-auth-react/recipe/session"; supertokensUISession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); if (!isAuthApiRoute || isOAuth2ApiRoute) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) import supertokensUISession from "supertokens-auth-react/recipe/session"; supertokensUISession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); if (!isAuthApiRoute || isOAuth2ApiRoute) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); if (!isAuthApiRoute || isOAuth2ApiRoute) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); if (!isAuthApiRoute || isOAuth2ApiRoute) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` The snippets retain interception for SuperTokens authentication routes under `/auth`, but explicitly exclude `/auth/oauth`. OAuth token, introspection, and related protocol requests must not receive SuperTokens session headers or automatic session refresh behavior. For the other routes, you have full control on how you want to attach the **OAuth2 Access Tokens** to the API calls.
The user interface that you are going to build should respect this flow: 1. **A user accesses your application and tries to login.** It's up to you how you want to handle this. They can click a button to login or you can directly start the login flow. 2. **They get redirected to the Authorization Service Backend ** A **OAuth2/OpenID Connect (OIDC)** library can execute this action. Check the previous guides for information on what you could use. 3. **The Authorization Service Backend redirects them to the Authorization Service Frontend login page.** The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. Besides that, the URL can also include a `forceFreshAuth` parameter. As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. This guide shows you how to handle this. 4. **The Authorization Service Frontend renders the login UI and the user performs the login action.** The login UI should render based on instructions that are specific to each authentication method which you are using. The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. 5. **The Authorization Service Frontend redirects the user back to the Authorization Service Backend ** After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. From here, the authentication flow completes. Let's see how you can actually implement this UI. #### 4.1 Configure the redirection URLs As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. The default values for these routes are: - The login page maps to `/auth` (this is also the place where a user ends up after logout) - The token refresh page maps to `/auth/try-refresh` - The logout page maps to `/auth/logout` If you want to change these routes, you need to add a custom override. :::info[This override needs addition to the **Authorization Service Backend**.] ::: :::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] ::: ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ override: { functions: (originalFunctions) => ({ ...originalFunctions, getFrontendRedirectionURL: async (input) => { const websiteDomain = ""; const websiteBasePath = "/auth"; if (input.type === "login") { const queryParams = new URLSearchParams({ loginChallenge: input.loginChallenge, }); if (input.hint !== undefined) { queryParams.set("hint", input.hint); } if (input.tenantId !== undefined) { queryParams.set("tenantId", input.tenantId); } if (input.forceFreshAuth) { queryParams.set("forceFreshAuth", "true"); } return `/auth?${queryParams.toString()}`; } else if (input.type === "try-refresh") { return `/auth/try-refresh?loginChallenge=${input.loginChallenge}`; } else if (input.type === "post-logout-fallback") { return `/auth`; } else if (input.type === "logout-confirmation") { return `/auth/oauth/logout?logoutChallenge=${input.logoutChallenge}`; } return `/auth`; }, }), }, }); ``` #### 4.2 Handle the forceFreshAuth parameter Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. The `forceFreshAuth` parameter shows this. When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. Here is an example of how you can evaluate this case. ```tsx import Session from "supertokens-web-js/recipe/session"; async function shouldLogin() { const urlParams = new URLSearchParams(window.location.search); const forceFreshAuth = urlParams.get("forceFreshAuth") as string; if (forceFreshAuth === "true") return true; return !(await Session.doesSessionExist()); } ``` :::info[Multi Tenancy] If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. ::: #### 4.3 Complete the login attempt After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2.0** flow. The following code sample shows you how to determine which URL to use. :::warning For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. ::: ```tsx import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; async function getInitialRedirectionURL() { const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get("loginChallenge") as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { return redirectionResponse.frontendRedirectTo; } } ``` #### 4.4 Add the token refresh page To have support for token refreshing, you need to add a new page to your application. The path should correspond to the one outlined during the first step. When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. Then they need redirection to a page from your application. Here's a code sample that shows you how to do this. :::warning For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. ::: ```tsx import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; import Session from "supertokens-web-js/recipe/session"; async function refreshToken() { await Session.attemptRefreshingSession(); const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get("loginChallenge") as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { window.location.href = redirectionResponse.frontendRedirectTo; } } ``` #### 4.5 Add the logout page You need to add a logout page that users access when they want to end their session. The path should correspond to the one outlined during the first step. The logout action should first ask the user for confirmation. If the confirmation passes, then you can call the recipe function. Based on the final response you can redirect the user to the provided redirection URL. :::warning For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. ::: ```tsx import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; async function logout() { const confirmation = confirm("Are you sure that you want to log out?"); if (!confirmation) return; const urlParams = new URLSearchParams(window.location.search); const logoutChallenge = urlParams.get("logoutChallenge") as string; const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); window.location.href = redirectResponse.frontendRedirectTo; } ``` ### 5. Update the login flow in your frontend applications Use an OAuth 2.0/OIDC library that supports authorization code with PKCE. For every login: 1. Generate a fresh high-entropy `state` and PKCE verifier; persist them only for the initiating browser transaction. 2. Send the derived S256 code challenge in the authorization request. 3. On callback, verify `state` exactly before exchanging the code with the verifier. If requesting `openid`, also generate and validate `nonce` and validate the ID token. 4. Keep access and refresh tokens in memory where possible. Do not place them in `localStorage`, browser-readable cookies, or URLs. A backend-for-frontend that stores tokens server-side and issues an opaque `HttpOnly`, `Secure`, appropriately `SameSite` session cookie offers stronger protection against token theft. You can use the [react-oidc-context](https://github.com/authts/react-oidc-context) library. Follow the instructions from the library's page. Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - `authority` corresponds to the endpoint of the **Authorization Service** `/auth` - `client_id` corresponds to `clientId` - `redirect_uri` corresponds to a value from `redirectUris` - `scope` corresponds directly to the space-separated `scope` value - Set `response_type` to `"code"`. The library uses S256 PKCE for code flow and generates and validates `state` (and `nonce` when using OIDC). If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "`<TENANT_ID>`" }`. You can use the [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) library. Follow the instructions described in the [GitHub repository](https://github.com/manfredsteyer/angular-oauth2-oidc?tab=readme-ov-file#logging-in). Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - `issuer` corresponds to the endpoint of the **Authorization Service** `/auth` - `clientId` corresponds to `clientId` - `redirectUri` corresponds to a value from `redirectUris` - `scope` corresponds directly to the space-separated `scope` value - Set `responseType` to `"code"`. The library uses S256 PKCE for code flow and generates and validates `state` (and `nonce` when using OIDC). If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. To do this, set `customQueryParams` to `{ tenant_id: "`<TENANT_ID>`" }`. You can use the [oidc-client-ts](https://github.com/authts/oidc-client-ts?tab=readme-ov-file) library. Follow the instructions described in the [GitHub repository](https://github.com/authts/oidc-client-ts/blob/main/docs/protocols/authorization-code-grant-with-pkce). Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - `authority` corresponds to the endpoint of the **Authorization Service** `/auth` - `client_id` corresponds to `clientId` - `redirect_uri` corresponds to a value from `redirectUris` - `scope` corresponds directly to the space-separated `scope` value - Set `response_type` to `"code"`. The library uses S256 PKCE for code flow and generates and validates `state` (and `nonce` when using OIDC). If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "`<TENANT_ID>`" }`. :::info If you want to use the [**OAuth2 Refresh Tokens**](/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: ### 6. Test the new authentication flow With everything set up, you can test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues. --- # Multiple frontend domains with separate backends Source: https://supertokens.com/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends ## Overview You can use the following guide if you have a single [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) that multiple applications use. In turn, each app has separate **`frontend`** and **`backend`** instances that serve from different domains. The authentication flow works in the following way: 1. **The User accesses the frontend app** - The application `frontend` calls a login endpoint on the `backend` application. - The `backend` application generates an `authorization` URL to the [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) and redirects the user to it. - The [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to the login UI 2. **The User completes the login attempt** - The [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to a `callback URL` that includes the **Authorization Code**. 3. **The user accesses the callback URL** - The Authorization Code and `state` are sent to the application backend. - The backend verifies `state`, exchanges the Authorization Code, and keeps the OAuth tokens server-side. - The backend rotates the application session and sends only an opaque session identifier in a cookie. The frontend uses an opaque `HttpOnly`, `Secure`, appropriately `SameSite` application-session cookie to access its backend. OAuth access and refresh tokens never enter browser-readable storage. Multiple Frontend Domains with separate Backends ## Before you start :::info Note that, if the *frontends* and *backends* are in different *subdomains*, you don't need to use *OAuth* and can instead use [session sharing across sub domains](/post-authentication/session-management/share-session-across-sub-domains). ::: ## Steps ### 1. Enable the Unified Login feature Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Features**. Enable **Unified Login**. Changes are saved automatically. ### 2. Create the OAuth2 Clients For each application, create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). Call the **SuperTokens Core** API from a trusted administrative environment. Because each application backend performs the code exchange and can protect credentials, these are **confidential clients**. The examples below register `client_secret_basic`, which is appropriate for Go oauth2, `Authlib`, League OAuth2 Client with `HttpBasicAuthOptionProvider`, Spring Security, and ASP.NET Core. Never expose a client secret to frontend code, logs, URLs, or browser storage. :::warning[passport-oauth2 requires a separately registered client] passport-oauth2 1.8.0 sends `client_id` and `client_secret` in the token request body. For the Node.js Passport application, register its own client with `tokenEndpointAuthMethod: "client_secret_post"` instead of the `client_secret_basic` value shown below. Do not reuse that client or secret in another application. ::: **Examples** ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "client_secret_basic", "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] } ' ``` ```tsx const BASE_URL = ""; const API_KEY = ""; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: "POST", headers: { "api-key": API_KEY, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify({ clientName: "", responseTypes: ["code"], grantTypes: ["authorization_code", "refresh_token"], tokenEndpointAuthMethod: "client_secret_basic", audience: [""], scope: "offline_access ", redirectUris: ["https:///oauth/callback"], }), }; fetch(url, options) .then((response) => response.json()) .then((json) => console.log(json)) .catch((err) => console.error(err)); ``` ```go import ( "fmt" "net/http" "strings" "io" ) func main() { baseUrl := "" apiKey := "" url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) payload := `{ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "client_secret_basic", "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] }` req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) req.Header.Add("accept", "application/json") req.Header.Add("api-key", apiKey) req.Header.Add("content-type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```python import requests from typing import Dict, Any BASE_URL = "" API_KEY = "" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "client_secret_basic", "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` **Details** Creates an OAuth2 client **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. ## Request ### Body Schema | Name | Type | Description | Required | Default Value | |--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| | `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | | `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | | `redirectUris` | `array` of `string` | Exact redirect URIs registered for the client. Wildcards are not supported. | Yes | - | | `audience` | `array` of `string` | Resource-server identifiers allowed in access tokens. | No | - | | `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | | `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | | `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | | `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | | `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | | `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | | `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | | `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | | `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | #### GrantType - `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. - `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. - `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. #### TokenEndpointAuthMethod - `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. - `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. - `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. - `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). #### ResponseType - `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. - `id_token`: Indicates that the Client expects an ID Token. #### Time Duration A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. ### Example ```bash curl -X POST /recipe/oauth/clients \ -H "Content-Type: application/json" \ -H "api-key: " \ -d '{ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "client_secret_basic", "audience": [""], "scope": "offline_access ", "redirectUris": ["https:///oauth/callback"] }' ``` ## Response ### 200 The client has been successfully created. ### Relevant response fields The response includes the persisted client configuration, including the fields below. | Property | Type | Description | |-------------|----------------------------------|-----------------------------------------------| | `clientName` | `string` | The name of the client. | | `clientId` | `string` | Unique identifier for the client. | | `clientSecret` | `string` | Client secret for a confidential client. Omitted for a public client. Treat it as a credential and keep it on a trusted backend. | | `redirectUris` | `array` of `string` | The URLs used for redirection. | | `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | | `scope` | `string` | A space-separated string of scopes that the client can request. | | `responseTypes` | `array` of `string` | Registered response types. | | `grantTypes` | `array` of `string` | Registered grant types. | | `tokenEndpointAuthMethod` | `string` | Token endpoint authentication method. | | `enableRefreshTokenRotation` | `boolean` | Whether refresh token rotation is enabled. | #### Example ```json { "clientName": "", "clientId": "", "clientSecret": "", "tokenEndpointAuthMethod": "client_secret_basic", "audience": [""], "redirectUris": ["https:///oauth/callback"], "scope": "offline_access " } ``` :::warning[Protect OAuth client credentials] Core persists the client configuration and encrypts confidential client secrets at rest. Store any returned client secret in a secret manager and expose it only to the application backend. Public clients do not receive or use a client secret. ::: ### 3. Set up your Authorization Service backend In your [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) you need to initialize the **OAuth2Provider** recipe. The recipe exposes the endpoints needed for enabling the [**OAuth 2.0**](/authentication/unified-login/oauth2-basics) flow. Update the `supertokens.init` call to include the `OAuth2Provider` recipe. Add the import statement for the recipe and update the recipe list with the new initialization step. :::warning[At the moment there is no support for creating OAuth2 providers in the Go SDK.] ::: ```typescript import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, oauth2provider init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ emailpassword.init(), oauth2provider.init(), ], ) ``` ### 4. Configure the Authorization Service frontend #### 4.1 Initialize the recipe Add the import statement for the new recipe and update the list of recipes to also include the new initialization. Update the `AuthComponent` to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array. Update the `AuthView` component to include the `OAuth2Provider` recipe. You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. ```tsx import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` ```tsx title="/app/auth/auth.component.ts" import { init as supertokensUIInit } from "supertokens-auth-react"; import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; import { DOCUMENT } from "@angular/common"; @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit({ appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ // Don't forget to also include the other recipes that you are already using supertokensUIOAuth2Provider.init(), ], }); }; this.renderer.appendChild(this.document.body, script); } } ```
```html import {init as supertokensUIInit} from "supertokens-auth-react"; import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; ```
#### 4.2 Include the pre-built UI in the rendering tree. ```tsx import React from "react"; import { BrowserRouter, Routes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx import React from "react"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI]); } return {/*Your app*/}; } } ```
The user interface that you are going to build should respect this flow: 1. **A user accesses your application and tries to login.** It's up to you how you want to handle this. They can click a button to login or you can directly start the login flow. 2. **They get redirected to the Authorization Service Backend ** A **OAuth2/OpenID Connect (OIDC)** library can execute this action. Check the previous guides for information on what you could use. 3. **The Authorization Service Backend redirects them to the Authorization Service Frontend login page.** The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. Besides that, the URL can also include a `forceFreshAuth` parameter. As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. This guide shows you how to handle this. 4. **The Authorization Service Frontend renders the login UI and the user performs the login action.** The login UI should render based on instructions that are specific to each authentication method which you are using. The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. 5. **The Authorization Service Frontend redirects the user back to the Authorization Service Backend ** After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. From here, the authentication flow completes. Let's see how you can actually implement this UI. #### 4.1 Configure the redirection URLs As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. The default values for these routes are: - The login page maps to `/auth` (this is also the place where a user ends up after logout) - The token refresh page maps to `/auth/try-refresh` - The logout page maps to `/auth/logout` If you want to change these routes, you need to add a custom override. :::info[This override needs addition to the **Authorization Service Backend**.] ::: :::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] ::: ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ override: { functions: (originalFunctions) => ({ ...originalFunctions, getFrontendRedirectionURL: async (input) => { const websiteDomain = ""; const websiteBasePath = "/auth"; if (input.type === "login") { const queryParams = new URLSearchParams({ loginChallenge: input.loginChallenge, }); if (input.hint !== undefined) { queryParams.set("hint", input.hint); } if (input.tenantId !== undefined) { queryParams.set("tenantId", input.tenantId); } if (input.forceFreshAuth) { queryParams.set("forceFreshAuth", "true"); } return `/auth?${queryParams.toString()}`; } else if (input.type === "try-refresh") { return `/auth/try-refresh?loginChallenge=${input.loginChallenge}`; } else if (input.type === "post-logout-fallback") { return `/auth`; } else if (input.type === "logout-confirmation") { return `/auth/oauth/logout?logoutChallenge=${input.logoutChallenge}`; } return `/auth`; }, }), }, }); ``` #### 4.2 Handle the forceFreshAuth parameter Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. The `forceFreshAuth` parameter shows this. When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. Here is an example of how you can evaluate this case. ```tsx import Session from "supertokens-web-js/recipe/session"; async function shouldLogin() { const urlParams = new URLSearchParams(window.location.search); const forceFreshAuth = urlParams.get("forceFreshAuth") as string; if (forceFreshAuth === "true") return true; return !(await Session.doesSessionExist()); } ``` :::info[Multi Tenancy] If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. ::: #### 4.3 Complete the login attempt After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2.0** flow. The following code sample shows you how to determine which URL to use. :::warning For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. ::: ```tsx import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; async function getInitialRedirectionURL() { const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get("loginChallenge") as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { return redirectionResponse.frontendRedirectTo; } } ``` #### 4.4 Add the token refresh page To have support for token refreshing, you need to add a new page to your application. The path should correspond to the one outlined during the first step. When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. Then they need redirection to a page from your application. Here's a code sample that shows you how to do this. :::warning For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. ::: ```tsx import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; import Session from "supertokens-web-js/recipe/session"; async function refreshToken() { await Session.attemptRefreshingSession(); const urlParams = new URLSearchParams(window.location.search); const loginChallenge = urlParams.get("loginChallenge") as string; const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); if (redirectionResponse.status === "OK") { window.location.href = redirectionResponse.frontendRedirectTo; } } ``` #### 4.5 Add the logout page You need to add a logout page that users access when they want to end their session. The path should correspond to the one outlined during the first step. The logout action should first ask the user for confirmation. If the confirmation passes, then you can call the recipe function. Based on the final response you can redirect the user to the provided redirection URL. :::warning For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. ::: ```tsx import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; async function logout() { const confirmation = confirm("Are you sure that you want to log out?"); if (!confirmation) return; const urlParams = new URLSearchParams(window.location.search); const logoutChallenge = urlParams.get("logoutChallenge") as string; const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); window.location.href = redirectResponse.frontendRedirectTo; } ``` ### 5. Set up session handling in each application In each of your individual `applications` you need to set up logic for handling the **OAuth 2.0** authentication flow. Use a framework OAuth 2.0/OIDC login middleware rather than implementing the protocol manually. The authorization endpoint is `/auth/oauth/auth`, and the token endpoint is `/auth/oauth/token`. Each registered callback must exactly match one of the client's `redirectUris`. A secure implementation must: 1. Generate a high-entropy `state`, bind it to the initiating browser session, and verify it exactly before code exchange. Use PKCE as defense in depth where the library supports it. 2. For OIDC, request `openid`, generate and verify `nonce`, and validate the ID token signature, issuer, audience, expiry, and nonce. 3. Keep OAuth access and refresh tokens in a server-side session store. After callback, rotate the application session identifier to prevent session fixation. 4. Return only an opaque session identifier in an `HttpOnly`, `Secure`, appropriately `SameSite` cookie. Never return OAuth tokens in browser-readable cookies, JavaScript storage, or URLs. 5. Preserve the selected `tenantId` through authorization, callback, and the resulting application session. With [passport-oauth2](https://www.passportjs.org/packages/passport-oauth2/), state protection and PKCE are opt-in. Configure both. Install server-side Express session middleware before Passport; do not use a client-side cookie session store. This example uses the separately registered `client_secret_post` client described in step 2. ```typescript import express, { type Request } from "express"; import session, { type Session, type SessionData, type Store } from "express-session"; import passport from "passport"; import OAuth2Strategy from "passport-oauth2"; interface OAuthTransaction { tenantId: string; } interface OAuthResult { tenantId: string; oauthTokens: { accessToken: string; refreshToken: string; }; } interface ApplicationSessionStore { set(sessionId: string, result: OAuthResult): void; } type ApplicationSession = Session & Partial & { oauthTransaction?: OAuthTransaction; }; const CLIENT_ID = ""; const EXPECTED_AUDIENCE = ""; const EXPECTED_ISSUER = ""; const REQUIRED_SCOPES = [""]; const INTROSPECTION_URL = "/auth/oauth/introspect"; const app = express(); const serverSideSessionStore = app.get("serverSideSessionStore") as Store; const applicationSessionStore = app.get("applicationSessionStore") as ApplicationSessionStore; function mustGetEnv(name: string): string { const value = process.env[name]; if (!value) throw new Error(`${name} is required`); return value; } function resolveAllowedTenant(req: Request): string { const tenantId = typeof req.query.tenantId === "string" ? req.query.tenantId : "public"; const allowedTenants = mustGetEnv("ALLOWED_TENANT_IDS").split(","); if (!allowedTenants.includes(tenantId)) throw new Error("Invalid tenant"); return tenantId; } function getApplicationSession(req: Request): ApplicationSession { return req.session as unknown as ApplicationSession; } class SuperTokensOAuth2Strategy extends OAuth2Strategy { authorizationParams(options: { tenantId?: string }): { tenant_id: string | undefined } { return { tenant_id: options.tenantId }; } } async function introspectAndValidateTenant(accessToken: string, expectedTenant: string) { const response = await fetch(INTROSPECTION_URL, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token: accessToken, scope: REQUIRED_SCOPES.join(" ") }), }); if (!response.ok) throw new Error("OAuth introspection failed"); const tokenInfo = (await response.json()) as { active?: boolean; aud?: string | string[]; tId?: string; client_id?: string; iss?: string; sub?: string; }; const audiences = Array.isArray(tokenInfo.aud) ? tokenInfo.aud : [tokenInfo.aud]; if ( tokenInfo.active !== true || tokenInfo.tId !== expectedTenant || tokenInfo.client_id !== CLIENT_ID || tokenInfo.iss !== EXPECTED_ISSUER || !audiences.includes(EXPECTED_AUDIENCE) ) { throw new Error("OAuth token does not match the login transaction"); } if (typeof tokenInfo.sub !== "string") throw new Error("OAuth subject missing"); return tokenInfo; } app.use( session({ secret: mustGetEnv("APPLICATION_SESSION_SECRET"), store: serverSideSessionStore, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: true, sameSite: "lax" }, }), ); app.use(passport.initialize()); app.use(passport.session()); passport.use( new SuperTokensOAuth2Strategy( { authorizationURL: "/auth/oauth/auth", tokenURL: "/auth/oauth/token", clientID: CLIENT_ID, clientSecret: mustGetEnv("OAUTH_CLIENT_SECRET"), callbackURL: "https:///oauth/callback", scope: "offline_access ", state: true, pkce: true, passReqToCallback: true, }, async (req, accessToken, refreshToken, params, profile, done) => { try { const applicationSession = getApplicationSession(req); const transaction = applicationSession.oauthTransaction; delete applicationSession.oauthTransaction; if (transaction === undefined) throw new Error("OAuth transaction missing"); const tokenInfo = await introspectAndValidateTenant(accessToken, transaction.tenantId); const user = { id: tokenInfo.sub }; done(null, user, { oauthTokens: { accessToken, refreshToken }, tenantId: transaction.tenantId, }); } catch (error) { done(error); } }, ), ); app.get("/login", (req, res, next) => { const tenantId = resolveAllowedTenant(req); getApplicationSession(req).oauthTransaction = { tenantId }; const options = { tenantId } as passport.AuthenticateOptions & { tenantId: string }; passport.authenticate("oauth2", options)(req, res, next); }); app.get("/oauth/callback", (req, res, next) => { const completeAuthentication = (error: unknown, user: Express.User | false | null, info: OAuthResult | undefined) => { if (error || !user) return next(error ?? new Error("OAuth login failed")); if (!info?.oauthTokens || typeof info.tenantId !== "string") { return next(new Error("OAuth transaction result missing")); } getApplicationSession(req).regenerate((regenerateError) => { if (regenerateError) return next(regenerateError); req.logIn(user, (loginError) => { if (loginError) return next(loginError); applicationSessionStore.set(req.sessionID, { tenantId: info.tenantId, oauthTokens: info.oauthTokens, }); res.redirect("/"); }); }); }; passport.authenticate("oauth2", { session: false }, completeAuthentication)(req, res, next); }); ``` passport-oauth2 generates and consumes its state and PKCE verifier in the initiating `req.session`. The separate `oauthTransaction` binds the allowlisted tenant to that same session, and `authorizationParams` sends it as the released `tenant_id` authorization parameter. The trusted introspection endpoint validates signature, expiry, revocation, and required scopes before the example compares the released `tId`, issuer, client, and audience fields. The rotated opaque application session stores the validated tenant and tokens server-side. Use [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) with a one-time, server-side transaction store. The store must key each transaction by the initiating application session ID; `Consume` must atomically read and delete it. ```go import ( "context" "crypto/rand" "crypto/subtle" "encoding/base64" "encoding/json" "errors" "io" "net/http" "net/url" "strings" "golang.org/x/oauth2" ) type OAuthTransaction struct { State string Verifier string TenantID string } type Audience []string func (audience *Audience) UnmarshalJSON(data []byte) error { var single string if err := json.Unmarshal(data, &single); err == nil { *audience = Audience{single} return nil } var multiple []string if err := json.Unmarshal(data, &multiple); err != nil { return errors.New("invalid OAuth audience") } *audience = multiple return nil } func (audience Audience) Contains(expected string) bool { for _, value := range audience { if value == expected { return true } } return false } type IntrospectionResponse struct { Active bool `json:"active"` Audience Audience `json:"aud"` TenantID string `json:"tId"` ClientID string `json:"client_id"` Issuer string `json:"iss"` } type AppSession struct { TenantID string Token *oauth2.Token } type OAuthTransactionStore interface { Put(sessionID string, transaction OAuthTransaction) error Consume(sessionID string) (OAuthTransaction, bool) } type AppSessionStore interface { Put(sessionID string, session AppSession) error } type OAuthApp struct { Config *oauth2.Config IntrospectionURL string ExpectedIssuer string ExpectedAudience string RequiredScopes []string Transactions OAuthTransactionStore Sessions AppSessionStore ResolveAllowedTenant func(*http.Request) (string, error) ApplicationSessionID func(*http.Request) string RotateApplicationSession func(http.ResponseWriter, *http.Request) (string, error) } func randomURLSafeToken(size int) (string, error) { value := make([]byte, size) if _, err := rand.Read(value); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(value), nil } func (app *OAuthApp) introspectAndValidateTenant(ctx context.Context, accessToken, expectedTenant string) error { form := url.Values{ "token": {accessToken}, "scope": {strings.Join(app.RequiredScopes, " ")}, } request, err := http.NewRequestWithContext(ctx, http.MethodPost, app.IntrospectionURL, strings.NewReader(form.Encode())) if err != nil { return err } request.Header.Set("content-type", "application/x-www-form-urlencoded") response, err := http.DefaultClient.Do(request) if err != nil { return err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return errors.New("OAuth introspection failed") } var tokenInfo IntrospectionResponse if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&tokenInfo); err != nil { return err } if !tokenInfo.Active || !tokenInfo.Audience.Contains(app.ExpectedAudience) || tokenInfo.TenantID != expectedTenant || tokenInfo.ClientID != app.Config.ClientID || tokenInfo.Issuer != app.ExpectedIssuer { return errors.New("OAuth token does not match the login transaction") } return nil } func (app *OAuthApp) Login(w http.ResponseWriter, r *http.Request) { state, err := randomURLSafeToken(32) if err != nil { http.Error(w, "login unavailable", http.StatusInternalServerError) return } tenantID, err := app.ResolveAllowedTenant(r) if err != nil { http.Error(w, "invalid tenant", http.StatusBadRequest) return } verifier := oauth2.GenerateVerifier() transaction := OAuthTransaction{State: state, Verifier: verifier, TenantID: tenantID} if err := app.Transactions.Put(app.ApplicationSessionID(r), transaction); err != nil { http.Error(w, "login unavailable", http.StatusInternalServerError) return } authURL := app.Config.AuthCodeURL( state, oauth2.S256ChallengeOption(verifier), oauth2.SetAuthURLParam("tenant_id", tenantID), ) http.Redirect(w, r, authURL, http.StatusFound) } func (app *OAuthApp) Callback(w http.ResponseWriter, r *http.Request) { transaction, ok := app.Transactions.Consume(app.ApplicationSessionID(r)) providedState := r.URL.Query().Get("state") if !ok || subtle.ConstantTimeCompare([]byte(transaction.State), []byte(providedState)) != 1 { http.Error(w, "invalid OAuth state", http.StatusBadRequest) return } token, err := app.Config.Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(transaction.Verifier)) if err != nil { http.Error(w, "code exchange failed", http.StatusBadRequest) return } if err := app.introspectAndValidateTenant(r.Context(), token.AccessToken, transaction.TenantID); err != nil { http.Error(w, "token validation failed", http.StatusUnauthorized) return } newSessionID, err := app.RotateApplicationSession(w, r) if err != nil { http.Error(w, "session creation failed", http.StatusInternalServerError) return } if err := app.Sessions.Put(newSessionID, AppSession{TenantID: transaction.TenantID, Token: token}); err != nil { http.Error(w, "session creation failed", http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusFound) } ``` Create an `OAuthApp` with your OAuth client configuration, server-side stores, and application session helpers, then register its `Login` and `Callback` methods as HTTP handlers. `ResolveAllowedTenant` must reject tenants outside your allowlist. `ApplicationSessionID` must identify the initiating server-side session, and `RotateApplicationSession` must invalidate the old session and issue a new opaque session ID. `Transactions.Consume` must atomically read and delete an unexpired transaction; return `false` for missing or expired transactions. Set `IntrospectionURL` to `/auth/oauth/introspect`, `ExpectedIssuer` to the exact configured issuer, `ExpectedAudience` to this resource server, and `RequiredScopes` to its scopes. Core 12.1.1 introspection can return `aud` as a JSON string or array; `Audience.UnmarshalJSON` handles both and the callback requires the configured audience before creating a session. The released `tenant_id` authorization parameter selects the tenant, and introspection returns its signed `tId`. Only the rotated opaque session ID reaches the browser. Use [Authlib](https://docs.authlib.org/) with a one-time server-side transaction store. These functions are framework-agnostic; connect `redirect`, `request_url`, and the session helpers to your framework. ```python import secrets from typing import Dict, Mapping, Optional, Protocol, TypedDict, cast import requests from authlib.common.security import generate_token from authlib.integrations.requests_client import OAuth2Session # pyright: ignore[reportMissingTypeStubs] CLIENT_ID = "" CLIENT_SECRET = "" AUTHORIZATION_URL = "/auth/oauth/auth" TOKEN_URL = "/auth/oauth/token" INTROSPECTION_URL = "/auth/oauth/introspect" CALLBACK_URL = "https:///oauth/callback" EXPECTED_ISSUER = "" EXPECTED_AUDIENCE = "" SCOPES = ["offline_access", "", ""] REQUIRED_SCOPES = [""] class OAuthTransaction(TypedDict): state: str verifier: str tenant_id: str class ApplicationSession(TypedDict): tenant_id: str oauth_token: Mapping[str, object] class TransactionStore(Protocol): def put(self, session_id: str, transaction: OAuthTransaction) -> None: ... def consume(self, session_id: str) -> Optional[OAuthTransaction]: ... class ApplicationSessionStore(Protocol): def put(self, session_id: str, session: ApplicationSession) -> None: ... class InvalidOAuthState(ValueError): pass def resolve_allowed_tenant() -> str: raise NotImplementedError def application_session_id() -> str: raise NotImplementedError def request_query_parameter(name: str) -> Optional[str]: raise NotImplementedError def request_url() -> str: raise NotImplementedError def rotate_application_session() -> str: raise NotImplementedError def redirect(url: str) -> str: raise NotImplementedError def introspect_and_validate_tenant(access_token: str, expected_tenant: str) -> None: response = requests.post( INTROSPECTION_URL, data={"token": access_token, "scope": " ".join(REQUIRED_SCOPES)}, timeout=5, ) response.raise_for_status() token_info = response.json() audiences = token_info.get("aud", []) if isinstance(audiences, str): audiences = [audiences] if ( token_info.get("active") is not True or token_info.get("tId") != expected_tenant or token_info.get("client_id") != CLIENT_ID or token_info.get("iss") != EXPECTED_ISSUER or EXPECTED_AUDIENCE not in audiences ): raise ValueError("OAuth token does not match the login transaction") def login(transaction_store: TransactionStore) -> str: client = OAuth2Session( CLIENT_ID, CLIENT_SECRET, token_endpoint_auth_method="client_secret_basic", scope=SCOPES, redirect_uri=CALLBACK_URL, code_challenge_method="S256", ) verifier = generate_token(48) tenant_id = resolve_allowed_tenant() authorization_url, state = cast( tuple[str, str], client.create_authorization_url( # pyright: ignore[reportUnknownMemberType] AUTHORIZATION_URL, code_verifier=verifier, tenant_id=tenant_id, ), ) transaction_store.put( application_session_id(), {"state": state, "verifier": verifier, "tenant_id": tenant_id}, ) return redirect(authorization_url) def callback( transaction_store: TransactionStore, application_session_store: ApplicationSessionStore, ) -> str: # consume atomically reads and deletes the initiating session's transaction transaction = transaction_store.consume(application_session_id()) provided_state = request_query_parameter("state") or "" if transaction is None or not secrets.compare_digest( transaction["state"], provided_state ): raise InvalidOAuthState() client = OAuth2Session( CLIENT_ID, CLIENT_SECRET, token_endpoint_auth_method="client_secret_basic", state=transaction["state"], redirect_uri=CALLBACK_URL, code_challenge_method="S256", ) token = cast( Dict[str, object], client.fetch_token( # pyright: ignore[reportUnknownMemberType] TOKEN_URL, authorization_response=request_url(), code_verifier=transaction["verifier"], ), ) access_token = token.get("access_token") if not isinstance(access_token, str): raise ValueError("OAuth access token missing") introspect_and_validate_tenant(access_token, transaction["tenant_id"]) new_session_id = rotate_application_session() application_session_store.put( new_session_id, {"tenant_id": transaction["tenant_id"], "oauth_token": token}, ) return redirect("/") ``` Set `INTROSPECTION_URL` to `/auth/oauth/introspect`, `EXPECTED_ISSUER` to the exact configured issuer, and configure the expected client, audience, and required scopes. Authlib sends the released `tenant_id` authorization parameter. Core introspection validates signature, expiry, revocation, and requested scopes and returns the signed `tId`; only the rotated opaque session ID is sent to the browser. Use [League OAuth2 Client](https://oauth2-client.thephpleague.com/usage/) with a server-side application session and one-time transaction store. ```php $clientSecret = getenv('OAUTH_CLIENT_SECRET'); if ($clientSecret === false) { throw new RuntimeException('OAUTH_CLIENT_SECRET is required'); } $httpClient = new GuzzleHttp\Client(['timeout' => 5]); $provider = new League\OAuth2\Client\Provider\GenericProvider( [ 'clientId' => CLIENT_ID, 'clientSecret' => $clientSecret, 'redirectUri' => 'https:///oauth/callback', 'urlAuthorize' => '/auth/oauth/auth', 'urlAccessToken' => '/auth/oauth/token', 'urlResourceOwnerDetails' => '/auth/oauth/userinfo', 'scopes' => ['offline_access', '', ''], 'scopeSeparator' => ' ', 'pkceMethod' => League\OAuth2\Client\Provider\GenericProvider::PKCE_METHOD_S256, ], [ 'httpClient' => $httpClient, 'optionProvider' => new League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider(), ], ); if ($requestPath === '/login') { $tenantId = resolveAllowedTenant(); $authorizationUrl = $provider->getAuthorizationUrl([ 'tenant_id' => $tenantId, ]); $transactionStore->put(session_id(), [ 'state' => $provider->getState(), 'pkceCode' => $provider->getPkceCode(), 'tenantId' => $tenantId, ]); header('Location: ' . $authorizationUrl); exit; } if ($requestPath !== '/oauth/callback') { throw new RuntimeException('Not found'); } // consume atomically reads and deletes the initiating session's transaction $transaction = $transactionStore->consume(session_id()); $providedState = $_GET['state'] ?? ''; if ($transaction === null || !hash_equals($transaction['state'], $providedState)) { throw new RuntimeException('Invalid OAuth state'); } if (isset($_GET['error']) || !isset($_GET['code'])) { throw new RuntimeException('OAuth authorization failed'); } $provider->setPkceCode($transaction['pkceCode']); $token = $provider->getAccessToken('authorization_code', [ 'code' => $_GET['code'], ]); $introspectionResponse = $httpClient->request('POST', INTROSPECTION_URL, [ 'form_params' => [ 'token' => $token->getToken(), 'scope' => implode(' ', REQUIRED_SCOPES), ], ]); $tokenInfo = json_decode( (string) $introspectionResponse->getBody(), true, 512, JSON_THROW_ON_ERROR, ); $audiences = is_array($tokenInfo['aud'] ?? null) ? $tokenInfo['aud'] : [$tokenInfo['aud'] ?? null]; if ( ($tokenInfo['active'] ?? false) !== true || ($tokenInfo['tId'] ?? null) !== $transaction['tenantId'] || ($tokenInfo['client_id'] ?? null) !== CLIENT_ID || ($tokenInfo['iss'] ?? null) !== EXPECTED_ISSUER || !in_array(EXPECTED_AUDIENCE, $audiences, true) ) { throw new RuntimeException('OAuth token does not match the login transaction'); } session_regenerate_id(true); $applicationSessionStore->put(session_id(), [ 'tenantId' => $transaction['tenantId'], 'oauthToken' => $token, ]); header('Location: /'); exit; ``` Set `INTROSPECTION_URL` to `/auth/oauth/introspect`, `EXPECTED_ISSUER` to the exact configured issuer, and configure the expected audience and required scopes. League sends the released `tenant_id` authorization parameter. Core introspection validates signature, expiry, revocation, and requested scopes and returns signed `tId`; only the rotated opaque session ID reaches the browser. You can use the [Spring Security](https://github.com/spring-projects/spring-security) library. Follow these [instructions](https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-client-log-users-in) and implement it in your `backend`. You can determine the configuration parameters based on the response received in **step 2**. - `client-id` corresponds to `clientId` - `client-secret` corresponds to `clientSecret` - `scope` corresponds to `scope` - `issuer-uri` corresponds to `/auth` Use an `OAuth2AuthorizationRequestResolver` to add the allowlisted tenant as the `tenant_id` authorization parameter and retain it in the server-side `AuthorizationRequestRepository` transaction. After callback, call `/auth/oauth/introspect`, require `active`, the configured scopes/client/audience/issuer, and exact `tId`, then persist that tenant and the tokens in the rotated server-side application session. Use ASP.NET Core's OpenID Connect authentication middleware to handle the authorization callback, correlation cookie, `state`, `nonce`, token validation, and application-session rotation. Configure its authority as `/auth`, set `ClientId` and `ClientSecret` from the confidential client, and set `CallbackPath` to the path of an exact `redirectUris` entry. Store tokens server-side rather than in the authentication cookie. In `OnRedirectToIdentityProvider`, add the allowlisted tenant to `AuthenticationProperties.Items` and send it as `ProtocolMessage.SetParameter("tenant_id", tenantId)`. After callback, introspect the access token at `/auth/oauth/introspect`; require `active`, the configured scopes/client/audience/issuer, and exact `tId`. Put that tenant and the tokens in the rotated server-side application session, never in the browser cookie. :::info If you want to use the [**OAuth2 Refresh Tokens**](/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: ### 6. Update the login flow in your frontend applications In your `frontend` applications you need to add a login action that directs the user to the authentication page. The user should first redirect to the `backend` authentication endpoint defined during the previous step. There the `backend` generates a safe `authorization` URL using the **OAuth2** library and then redirects the user there. After login, the Authorization Service redirects the user to the backend callback. The backend verifies the bound `state` (and OIDC `nonce` when applicable), exchanges the code, rotates the application session, and sets only the hardened opaque session cookie described above. ### 7. Test the new authentication flow With everything set up, you can test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues. --- # Reuse website login for desktop and mobile apps Source: https://supertokens.com/docs/authentication/unified-login/quickstart-guides/reuse-website-login ## Overview This pattern is useful if you want to have the same web authentication experience for your desktop and mobile apps. The implementation allows you to save development time but keep in mind that it does not involve a native authentication interface. Users get directed to a separate browser page where they complete the authentication flow and then return to your application. The authentication flow works in the following way: 1. **User accesses the native application** - The user gets redirected to [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) authentication URL. - The [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) redirects the user to the login UI 2. **User completes the login attempt ** - The Authorization Service redirects the user to the registered callback URL. - Prefer an OS-claimed HTTPS universal link or app link. If you must use a custom scheme, follow the platform guidance for preventing other apps from claiming it. 3. **The application completes the authorization code flow** - The application verifies `state`, then exchanges the code with the original PKCE verifier. - It stores returned tokens only in platform-protected secure storage. Reuse website login for desktop and mobile apps ## Before you start ## Steps ### 1. Enable the Unified Login feature Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Features**. Enable **Unified Login**. Changes are saved automatically. ### 2. Create the OAuth2 Clients For each native application, create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). Call the **SuperTokens Core** API from a trusted administrative environment. Native applications are **public clients**: `tokenEndpointAuthMethod` must be `none`, and the application must never contain or receive a client secret. Use authorization code with S256 PKCE. **Examples** ```bash curl --location --request POST '/recipe/oauth/clients' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data ' { "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "scope": "offline_access ", "redirectUris": ["https://app.example.com/oauth/callback"] } ' ``` ```tsx const BASE_URL = ""; const API_KEY = ""; const url = `${BASE_URL}/recipe/oauth/clients`; const options = { method: "POST", headers: { "api-key": API_KEY, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify({ clientName: "", responseTypes: ["code"], grantTypes: ["authorization_code", "refresh_token"], tokenEndpointAuthMethod: "none", scope: "offline_access ", redirectUris: ["https://app.example.com/oauth/callback"], }), }; fetch(url, options) .then((response) => response.json()) .then((json) => console.log(json)) .catch((err) => console.error(err)); ``` ```go import ( "fmt" "net/http" "strings" "io" ) func main() { baseUrl := "" apiKey := "" url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) payload := `{ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "scope": "offline_access ", "redirectUris": ["https://app.example.com/oauth/callback"] }` req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) req.Header.Add("accept", "application/json") req.Header.Add("api-key", apiKey) req.Header.Add("content-type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ```python import requests from typing import Dict, Any BASE_URL = "" API_KEY = "" url = f"{BASE_URL}/recipe/oauth/clients" payload: Dict[str, Any] ={ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "scope": "offline_access ", "redirectUris": ["https://app.example.com/oauth/callback"] } headers = { "api-key": API_KEY, "Content-Type": "application/json", } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` **Details** Creates an OAuth2 client **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. ## Request ### Body Schema | Name | Type | Description | Required | Default Value | |--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| | `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | | `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | | `redirectUris` | `array` of `string` | Exact redirect URIs registered for the client. Wildcards are not supported. | Yes | - | | `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | | `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | | `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | | `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | | `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | | `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | | `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | | `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | | `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | #### GrantType - `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. - `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. - `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. #### TokenEndpointAuthMethod - `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. - `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. - `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. - `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). #### ResponseType - `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. - `id_token`: Indicates that the Client expects an ID Token. #### Time Duration A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. ### Example ```bash curl -X POST /recipe/oauth/clients \ -H "Content-Type: application/json" \ -H "api-key: " \ -d '{ "clientName": "", "responseTypes": ["code"], "grantTypes": ["authorization_code", "refresh_token"], "tokenEndpointAuthMethod": "none", "scope": "offline_access ", "redirectUris": ["https://app.example.com/oauth/callback"] }' ``` ## Response ### 200 The client has been successfully created. ### Relevant response fields The response includes the persisted client configuration, including the fields below. | Property | Type | Description | |-------------|----------------------------------|-----------------------------------------------| | `clientName` | `string` | The name of the client. | | `clientId` | `string` | Unique identifier for the client. | | `clientSecret` | `string` | Client secret for a confidential client. Omitted for a public client. Treat it as a credential and keep it on a trusted backend. | | `redirectUris` | `array` of `string` | The URLs used for redirection. | | `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | | `scope` | `string` | A space-separated string of scopes that the client can request. | | `responseTypes` | `array` of `string` | Registered response types. | | `grantTypes` | `array` of `string` | Registered grant types. | | `tokenEndpointAuthMethod` | `string` | Token endpoint authentication method. | | `enableRefreshTokenRotation` | `boolean` | Whether refresh token rotation is enabled. | #### Example ```json { "clientName": "", "clientId": "", "tokenEndpointAuthMethod": "none", "redirectUris": ["https://app.example.com/oauth/callback"], "audience": [""], "scope": "offline_access " } ``` :::warning[Protect OAuth client credentials] Core persists the client configuration and encrypts confidential client secrets at rest. Store any returned client secret in a secret manager and expose it only to the application backend. Public clients do not receive or use a client secret. ::: Based on the client creation process, you can infer two additional values that you need later on: - `authorizeUrl` corresponds to `/auth/oauth/auth` - `tokenFetchUrl` corresponds to `/auth/oauth/token` ### 3. Configure the Authorization Service Check one of the previous guides that show you how to set up the **Authorization Service** and then return to this page. Choose the tutorial based on whether you use multiple backend services or not: - [Single Backend Setup](/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend#3-set-up-the-authorization-service-backend) - [Multiple Backends Setup](/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends#3-set-up-your-authorization-service-backend) ### 4. Update the login flow in your applications In each of your individual `applications`, you need to set up logic for handling the **OAuth 2.0** authentication flow. Use a maintained native OAuth 2.0/OIDC library that uses the system browser and authorization code with S256 PKCE. For every request, let the library generate a fresh PKCE verifier and high-entropy `state`; verify `state` before code exchange. If you request `openid`, also generate and validate `nonce` and validate the ID token's signature, issuer, audience, expiry, and nonce. Never use an embedded web view. Register an exact callback URI. Prefer an OS-claimed HTTPS universal link or app link; use a custom scheme only when the platform's interception protections are configured. Store access and refresh tokens in Keychain, Android Keystore-backed storage, or the platform equivalent. Never put tokens in logs, URLs, plain-text preferences, or application bundles. You can use the [react-native-app-auth](https://commerce.nearform.com/open-source/react-native-app-auth/) library. Follow [the instructions](https://commerce.nearform.com/open-source/react-native-app-auth/docs/usage/config) to set up your application. Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. - `issuer` corresponds to the endpoint of the **Authorization Service** `/auth` - `clientId` corresponds to `clientId` - `redirectUrl` corresponds to a value from `redirectUris` - `scopes` is the space-separated `scope` value split into a list You also need to set the `additionalParameters` property with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library. Follow [the instructions](https://github.com/openid/AppAuth-Android?tab=readme-ov-file#authorization-service-configuration) to set up your application. Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. For the `AuthorizationServiceConfiguration`, the parameters you need to provide are: `authorizeUrl` and `tokenFetchUrl`. When calling the `AuthorizationRequest.Builder` function you can use `clientId` and a value from `redirectUris` to replace the example values. You need to set additional query parameters by calling the `setAdditionalParameters` function on the `AuthorizationRequest.Builder` object: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) library. Follow [the instructions](https://github.com/openid/AppAuth-iOS?tab=readme-ov-file#auth-flow) to set up your application. Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. - `clientId` corresponds to `clientId` - `redirectUrl` corresponds to a value from `redirectUris` - `scopes` is the space-separated `scope` value split into a list - `authorizationEndpoint` corresponds to `authorizeUrl` - `tokenEndpoint` corresponds to `tokenFetchUrl` You also need to set extra query parameters, when instantiating the `OIDAuthorizationRequest` object, with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. You can use the [AppAuth](https://github.com/MaikuB/flutter_appauth) library. Follow [the instructions](https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth) to set up your application. Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. - `` corresponds to `clientId` - `` corresponds to the endpoint of the **Authorization Service** `/auth` - `` corresponds to a value from `redirectUris` - `scopes` is the space-separated `scope` value split into a list You also need to set the `additionalParameters` property with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. :::info If you want to use the [**OAuth2 Refresh Tokens**](/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: ### 5. Test the new authentication flow With everything set up, you can test your login flow. Use the setup created in the previous step to check if the authentication flow completes without any issues. --- # Verify tokens Source: https://supertokens.com/docs/authentication/unified-login/verify-tokens ## Overview You can verify an **OAuth2 Access Token** locally or enable database-backed validation to detect revocation. One thing to note is that, besides the standard **OAuth2** token claims, the **Unified Login** implementation includes an additional one called `stt`. This stands for `SuperTokens Token Type`. It ensures that the validation occurs for the correct token type: - `0` represents a **SuperTokens Session Access Token** - `1` represents an **OAuth2 Access Token** - `2` represents an **OAuth2 ID Token**. :::warning The following guide covers only **OAuth2 Tokens** verification. For information on how to verify **SuperTokens Session Tokens** please refer to the [following section](/additional-verification/session-verification/protect-api-routes). ::: --- ## Local access token verification Use the released SuperTokens backend SDK validator for most protected operations. It validates the JWT signature, expiration, and `stt=1` token type. Configure the intended audience and required scopes. Restricting the client ID is an optional additional check; it does not replace audience validation. Also compare the token issuer with your Authorization Server's issuer. ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; async function validateToken(token: string): Promise { try { const result = await OAuth2Provider.validateOAuth2AccessToken(token, { audience: "", clientId: "", scopes: [""], }); return result.payload.iss === "/auth"; } catch { return false; } } ``` ```python from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token def validate_token(token: str) -> bool: try: result = validate_oauth2_access_token( token=token, requirements=OAuth2TokenValidationRequirements( audience="", client_id="", scopes=[""], ), ) return result.payload.get("iss") == "/auth" except Exception: return False ``` ### Email verification If you are using email and password based authentication, and you want to validate if the user has verified their email, you must check if the `email_verified` claim is true. --- ## Using the token introspection API Revocation is not visible to local JWT verification, so a revoked token otherwise remains valid until it expires. For high-security operations, use the backend SDK validator with database checking enabled. This calls Core introspection in addition to performing local cryptographic and claim validation. Here is an example of how you can use this validation method: ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; async function validateToken(token: string): Promise { try { const result = await OAuth2Provider.validateOAuth2AccessToken( token, { audience: "", clientId: "", scopes: [""], }, true, ); return result.payload.iss === "/auth"; } catch { return false; } } ``` ```python from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token def validate_token(token: str) -> bool: try: result = validate_oauth2_access_token( token=token, requirements=OAuth2TokenValidationRequirements( audience="", client_id="", scopes=[""], ), check_database=True, ) return result.payload.get("iss") == "/auth" except Exception: return False ``` --- # Work with scopes Source: https://supertokens.com/docs/authentication/unified-login/work-with-scopes ## Overview The creation process of an **OAuth2 Client** determines the allowed scopes. By default, the **OAuth2** implementation adds the following built-in scopes: | Scope | Claims Added | Notes | |-------|-------------|--------| | `email` | `email`, `emails`, `email_verified` | Added to ID Token and User Info | | `phoneNumber` | `phoneNumber`, `phoneNumbers`, `phoneNumber_verified` | Added to ID Token and User Info | | `roles` | The roles return by `getRolesForUser` | Added to ID Token and Access Token | | `permissions` | The list of permissions obtained by concatenating the result of `getPermissionsForRole` for all roles returned by `getRolesForUser` | Added to ID Token and Access Token | --- ## Request specific scopes The client can request specific scopes by adding `scope` query parameter to the **Authorization URL**. The requested scopes have to be a subset of what the client allows, otherwise the authentication request fails. By default, the client receives all scopes. --- ## Override granted scopes If you want to manually modify the list of scopes that the client receives during the authentication flow, you can do this by using overrides. :::warning[The Go SDK does not support creating OAuth2 providers.] ::: ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ override: { functions: (originalFunctions) => ({ ...originalFunctions, getRequestedScopes: async (input) => { const originallyRequestedScopes = await originalFunctions.getRequestedScopes(input); const filteredScopes = originallyRequestedScopes.filter((scope) => scope !== "profile"); return [...filteredScopes, "custom-scope"]; }, }), }, }); ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import RecipeUserId from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_get_requested_scopes = original_implementation.get_requested_scopes async def get_requested_scopes( recipe_user_id: Optional[RecipeUserId], session_handle: Optional[str], scope_param: List[str], client_id: str, user_context: Dict[str, Any], ): originally_requested_scopes = await original_get_requested_scopes( recipe_user_id, session_handle, scope_param, client_id, user_context ) filtered_scopes = [scope for scope in originally_requested_scopes if scope != "profile"] return [*filtered_scopes, "custom-scope"] original_implementation.get_requested_scopes = get_requested_scopes return original_implementation init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), framework="fastapi", supertokens_config=SupertokensConfig( connection_uri="...", api_key="..." ), recipe_list=[ oauth2provider.init( override=oauth2provider.InputOverrideConfig(functions=override_oauth2provider_functions) ) ], ) ``` --- # Migrate from MySQL to PostgreSQL Source: https://supertokens.com/docs/deployment/migrate-from-mysql This is a legacy migration path. The released schemas verified for this page are the matching MySQL and PostgreSQL Core `10.1.4` images, which use storage interface `7.1`. Current Core releases use a newer storage interface. Do not assume that schemas from different Core versions are compatible, and never use an unpinned image tag for this work. ## Before you start Treat this as a manual, high-risk database migration. Plan and validate the procedure with your SuperTokens support contact or a maintainer who understands the released Core storage schemas. Do not test it directly on production data. Do not begin the production migration until the following are true: - Record the exact source Core image tag, storage schema version, database name, schema, and custom table prefix. - Initialize the PostgreSQL schema with the exact same Core version and the same custom table prefix. Upgrade Core only as a separate, subsequently tested operation. - Use a new target with no pre-existing SuperTokens application data. Never merge the source into a target that has accepted writes. - Keep both databases and Core instances on private networks. Do not expose either database or Core publicly during the migration. - Prove that a MySQL backup can be restored before the maintenance window. After writes stop, take the final source backup. Also define how the PostgreSQL target will be reset if an attempt fails. - Schedule a write outage. Stop every Core and other process that can write to the source, and keep the target Core stopped during import and validation. - Define an explicit rollback point and keep MySQL as the authoritative database until every acceptance check passes. ## Steps ### 1. Create a backup of your MySQL database Create a final backup of your MySQL database. The instructions for this step are specific to your database management system. ### 2. Prepare the PostgreSQL database Start the same version of [`supertokens-postgresql`](https://hub.docker.com/r/supertokens/supertokens-postgresql) to initialize the schema in the database. :::warning Make sure to use the same version as the `supertokens-mysql` instance that you are currently running. ::: ### 3. Export data from MySQL #### 3.1 Export standard tables Run the following command to export most of your data: ```bash mysqldump supertokens --fields-terminated-by ',' --fields-enclosed-by '"' --fields-escaped-by '\' --no-create-info --tab /var/lib/mysql-files/ ``` :::info If you do not have permissions to write to the database filesystem you can use the following python script to export tables one by one: ```python #!/usr/bin/env python3 import subprocess import os import csv DB_HOST = "DB_HOST" DB_PORT = "3306" DB_USER = "DB_USER" DB_NAME = "DB_NAME" DB_PASS = "DB_PASS" def run_mysql_command(query: str) -> str: """Run a mysql command and return the output""" cmd = [ "mysql", "-h", DB_HOST, "-P", DB_PORT, "-u", DB_USER, f"-p{DB_PASS}", "--batch", "-e", query, DB_NAME ] result = subprocess.run(cmd, capture_output=True, text=True) return result.stdout def main(): os.makedirs("./mysql", exist_ok=True) print("Getting list of tables") cmd = [ "mysql", "-h", DB_HOST, "-P", DB_PORT, "-u", DB_USER, f"-p{DB_PASS}", "-N", "-e", "SHOW TABLES", DB_NAME ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"ERROR: Failed to get table list: {result.stderr}") return tables = [t.strip() for t in result.stdout.strip().split('\n') if t.strip()] print(f"Found {len(tables)} tables") for table in tables: print(f"Exporting table: {table}") query = f"SELECT * FROM {table}" output = run_mysql_command(query) if not output.strip(): print(f" -> Table {table} is empty, skipping") continue lines = output.strip().split('\n') output_file = f"./mysql/{table}.csv" with open(output_file, 'w', newline='') as f: csv_writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL) for line in lines: fields = line.split('\t') csv_writer.writerow(fields) print(f" -> Exported {len(lines)} rows") print("Export complete!") if __name__ == "__main__": main() ``` ::: This creates CSV files for all tables in the `/var/lib/mysql-files/` directory. #### 3.2 Export the WebAuthn credentials table The `webauthn_credentials` table requires special handling because of the data type used to store the `public_key` field. ```sql SELECT id, app_id, rp_id, user_id, counter, HEX(public_key) AS public_key, transports, created_at, updated_at FROM webauthn_credentials INTO OUTFILE '/var/lib/mysql-files/webauthn_credentials_hex.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n'; ``` This exports the `public_key` field as hexadecimal text for proper conversion to PostgreSQL's Binary Data (BYTEA) format. ### 4. Transfer data files If necessary, copy the exported CSV files to a location where the `PostgreSQL` database can access them. ### 5. Import data into the PostgreSQL database Next, you need to import the data into your PostgreSQL database. #### 5.1. Disable triggers Connect to your PostgreSQL database and disable triggers to prevent constraint violations during import. ```sql SET session_replication_role = 'replica'; ``` :::info If you cannot disable the triggers, use the order specified in the next step. The *With Order* shows you how to import everything one by one without triggering the constraints. ::: #### 5.2 Import the standard tables For most tables, you can import the data directly. ```sql COPY FROM '/pg-data-host/.csv' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` ```bash #!/bin/bash PG_HOST="PG_HOST" PG_PORT="5432" PG_USER="PG_USER" PG_DB="PG_DB" PG_PASS="PG_PASS" CSV_DIR="./mysql" import_table() { local table=$1 local columns=$2 local csv_file="${CSV_DIR}/${table}.csv" if [ ! -f "$csv_file" ]; then echo "File not found: $csv_file, skipping" return fi echo "Importing table: $table" if [ -z "$columns" ] || [ "$columns" = "*" ]; then PGPASSWORD=$PG_PASS psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c \ "\\COPY $table FROM '$csv_file' WITH (FORMAT csv, HEADER true);" else PGPASSWORD=$PG_PASS psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c \ "\\COPY $table ($columns) FROM '$csv_file' WITH (FORMAT csv, HEADER true);" fi if [ $? -eq 0 ]; then echo "Successfully imported $table" else echo "ERROR importing $table" fi } echo "Starting PostgreSQL import" import_table "apps" "" import_table "tenants" "" import_table "key_value" "app_id, tenant_id, name, value, created_at_time" import_table "all_auth_recipe_users" "app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined" import_table "app_id_to_user_id" "app_id, user_id, recipe_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user" import_table "bulk_import_users" "id, app_id, primary_user_id, raw_data, status, error_msg, created_at, updated_at" import_table "dashboard_user_sessions" "app_id, session_id, user_id, time_created, expiry" import_table "dashboard_users" "app_id, user_id, email, password_hash, time_joined" import_table "emailpassword_pswd_reset_tokens" "app_id, user_id, token, email, token_expiry" import_table "emailpassword_user_to_tenant" "app_id, tenant_id, user_id, email" import_table "emailpassword_users" "app_id, user_id, email, password_hash, time_joined" import_table "emailverification_tokens" "app_id, tenant_id, user_id, email, token, token_expiry" import_table "emailverification_verified_emails" "app_id, user_id, email" import_table "jwt_signing_keys" "app_id, key_id, key_string, algorithm, created_at" import_table "oauth_clients" "app_id, client_id, client_secret, enable_refresh_token_rotation, is_client_credentials_only" import_table "oauth_logout_challenges" "app_id, challenge, client_id, post_logout_redirect_uri, session_handle, state, time_created" import_table "oauth_m2m_tokens" "app_id, client_id, iat, exp" import_table "oauth_sessions" "gid, app_id, client_id, session_handle, external_refresh_token, internal_refresh_token, jti, exp" import_table "passwordless_codes" "app_id, tenant_id, code_id, device_id_hash, link_code_hash, created_at" import_table "passwordless_devices" "app_id, tenant_id, device_id_hash, email, phone_number, link_code_salt, failed_attempts" import_table "passwordless_user_to_tenant" "app_id, tenant_id, user_id, email, phone_number" import_table "passwordless_users" "app_id, user_id, email, phone_number, time_joined" import_table "role_permissions" "app_id, role, permission" import_table "roles" "app_id, role" import_table "session_access_token_signing_keys" "app_id, created_at_time, value" import_table "session_info" "app_id, tenant_id, session_handle, user_id, refresh_token_hash_2, session_data, expires_at, created_at_time, jwt_user_payload, use_static_key" import_table "tenant_first_factors" "connection_uri_domain, app_id, tenant_id, factor_id" import_table "tenant_required_secondary_factors" "connection_uri_domain, app_id, tenant_id, factor_id" import_table "tenant_thirdparty_providers" "connection_uri_domain, app_id, tenant_id, third_party_id, name, authorization_endpoint, authorization_endpoint_query_params, token_endpoint, token_endpoint_body_params, user_info_endpoint, user_info_endpoint_query_params, user_info_endpoint_headers, jwks_uri, oidc_discovery_endpoint, require_email, user_info_map_from_id_token_payload_user_id, user_info_map_from_id_token_payload_email, user_info_map_from_id_token_payload_email_verified, user_info_map_from_user_info_endpoint_user_id, user_info_map_from_user_info_endpoint_email, user_info_map_from_user_info_endpoint_email_verified" import_table "thirdparty_user_to_tenant" "app_id, tenant_id, user_id, third_party_id, third_party_user_id" import_table "thirdparty_users" "app_id, third_party_id, third_party_user_id, user_id, email, time_joined" import_table "totp_used_codes" "app_id, tenant_id, user_id, code, is_valid, expiry_time_ms, created_time_ms" import_table "tenant_configs" "connection_uri_domain, app_id, tenant_id, core_config, email_password_enabled, passwordless_enabled, third_party_enabled, is_first_factors_null" import_table "totp_user_devices" "app_id, user_id, device_name, secret_key, period, skew, verified, created_at" import_table "totp_users" "app_id, user_id" import_table "user_last_active" "app_id, user_id, last_active_time" import_table "user_metadata" "app_id, user_id, user_metadata" import_table "user_roles" "app_id, tenant_id, user_id, role" import_table "userid_mapping" "app_id, supertokens_user_id, external_user_id, external_user_id_info" import_table "webauthn_account_recovery_tokens" "app_id, tenant_id, user_id, email, token, expires_at" import_table "webauthn_generated_options" "app_id, tenant_id, id, challenge, email, rp_id, rp_name, origin, expires_at, created_at, user_presence_required, user_verification" import_table "webauthn_user_to_tenant" "app_id, tenant_id, user_id, email" import_table "webauthn_users" "app_id, user_id, email, rp_id, time_joined" echo "" echo "Import complete!" ``` #### 5.3 Handle the third-party provider clients table The `tenant_thirdparty_provider_clients` table requires special handling. You need to do this to differences between the `MySQL` `JSON` and `PostgreSQL` `text[]` formats. ##### 5.3.1 Create a staging table ```sql CREATE TABLE tenant_thirdparty_provider_clients_raw ( connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, third_party_id VARCHAR(28) NOT NULL, client_type VARCHAR(64) DEFAULT '' NOT NULL, client_id VARCHAR(256) NOT NULL, client_secret TEXT, scope JSONB, force_pkce BOOLEAN, additional_config TEXT ); ``` ##### 5.3.2 Import data into the staging table ```sql COPY tenant_thirdparty_provider_clients_raw FROM '/host/tenant_thirdparty_provider_clients.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` ##### 5.3.3 Convert and insert into the final table ```sql INSERT INTO tenant_thirdparty_provider_clients ( connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, force_pkce, additional_config, scope ) SELECT connection_uri_domain, app_id, tenant_id, third_party_id, client_type, client_id, client_secret, force_pkce, additional_config, ARRAY( SELECT jsonb_array_elements_text(scope->jsonb_object_keys(scope)) ) FROM tenant_thirdparty_provider_clients_raw; ``` #### 5.4 Handle the WebAuthn credentials table The `webauthn_credentials` table requires conversion from MySQL Binary Large Object, `BLOB`, to PostgreSQL Binary Data, `BYTEA` format. ##### 5.4.1 Create a staging table ```sql CREATE TABLE IF NOT EXISTS webauthn_credentials_staging ( id VARCHAR(256) NOT NULL, app_id VARCHAR(64) DEFAULT 'public' NOT NULL, rp_id VARCHAR(256) NOT NULL, user_id CHAR(36), counter BIGINT NOT NULL, public_key TEXT NOT NULL, transports TEXT NOT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); ``` ##### 5.4.2 Import the hexadecimal data ```sql COPY webauthn_credentials_staging FROM '/host/webauthn_credentials_hex.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` ##### 5.4.3 Convert and insert into the final table ```sql INSERT INTO webauthn_credentials ( id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at ) SELECT id, app_id, rp_id, user_id, counter, decode(public_key, 'hex'), transports, created_at, updated_at FROM webauthn_credentials_staging; ``` #### 5.5 Delete the staging tables Delete the two temporary tables. ```sql DROP TABLE webauthn_credentials_staging; DROP TABLE tenant_thirdparty_provider_clients_raw; ``` #### 5.6 Re-enable triggers After importing all data, re-enable the triggers: ```sql SET session_replication_role = 'origin'; ``` ### 6. Verify the migration Verify that all data migrated successfully by comparing record counts between your MySQL and PostgreSQL databases: ```bash #!/bin/bash MYSQL_HOST="DB_HOST" MYSQL_PORT="3306" MYSQL_USER="DB_USER" MYSQL_DB="DB_NAME" MYSQL_PASS="DB_PASS" PG_HOST="PG_HOST" PG_PORT="5432" PG_USER="PG_USER" PG_DB="PG_DB" PG_PASS="PG_PASS" echo "Comparing table row counts between MySQL and PostgreSQL" echo "Getting table list from MySQL" TABLES=$(mysql -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS \ -N -e "SHOW TABLES" $MYSQL_DB) if [ $? -ne 0 ]; then echo "ERROR: Failed to get table list from MySQL" exit 1 fi TOTAL_TABLES=$(echo "$TABLES" | wc -l) echo "Found $TOTAL_TABLES tables" echo "" MATCH_COUNT=0 MISMATCH_COUNT=0 ERROR_COUNT=0 for TABLE in $TABLES; do MYSQL_COUNT=$(mysql -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS \ -N -e "SELECT COUNT(*) FROM $TABLE" $MYSQL_DB 2>&1) if [ $? -ne 0 ]; then echo "$TABLE - MySQL: ERROR, PostgreSQL: -, Status: ERROR" ERROR_COUNT=$((ERROR_COUNT + 1)) continue fi PG_COUNT=$(PGPASSWORD=$PG_PASS psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB \ -t -c "SELECT COUNT(*) FROM $TABLE" 2>&1) if [ $? -ne 0 ]; then echo "$TABLE - MySQL: $MYSQL_COUNT, PostgreSQL: ERROR, Status: ERROR" ERROR_COUNT=$((ERROR_COUNT + 1)) continue fi MYSQL_COUNT=$(echo $MYSQL_COUNT | xargs) PG_COUNT=$(echo $PG_COUNT | xargs) if [ "$MYSQL_COUNT" = "$PG_COUNT" ]; then echo "$TABLE - MySQL: $MYSQL_COUNT, PostgreSQL: $PG_COUNT, Status: ✓ MATCH" MATCH_COUNT=$((MATCH_COUNT + 1)) else echo "$TABLE - MySQL: $MYSQL_COUNT, PostgreSQL: $PG_COUNT, Status: ✗ MISMATCH" MISMATCH_COUNT=$((MISMATCH_COUNT + 1)) fi done echo "Summary:" echo " Total tables: $TOTAL_TABLES" echo " Matching: $MATCH_COUNT" echo " Mismatched: $MISMATCH_COUNT" echo " Errors: $ERROR_COUNT" echo "" if [ $MISMATCH_COUNT -eq 0 ] && [ $ERROR_COUNT -eq 0 ]; then echo "✓ All tables match!" exit 0 else echo "✗ Some tables have mismatches or errors" exit 1 fi ``` If the numbers match, you have successfully migrated your SuperTokens data from `MySQL` to `PostgreSQL` :tada: --- # Rate limit policy Source: https://supertokens.com/docs/deployment/rate-limits ## Overview The following page describes how rate limits apply during SuperTokens API calls. ## For managed service The SuperTokens core of a managed account is rate limited on a per app and per IP address basis. This means that if you query the core for `app1` using the same IP address, the rate limit kicks in, and you get a `429` status code back from the core. However, if you query the core using different IP addresses or for a different app, the rate limit of that does not interfere with the previous requests (that had another IP or was for another app). ### Free tier The free tier of the managed service has a rate limit of 50 requests per second with a burst of 50 requests per second (with no delay). This should be enough for 5-10 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). :::note[The backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error).] ::: ### Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit adjust dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **significant** spike in requests. :::info[Paid Feature] If you want higher rate limits, please [email support](mailto:support@supertokens.com), requesting a higher rate limit. ::: ### Special case The `/hello` API exposed by the core is commonly used for health checks. This API does not require any API key, and has its own rate limit of 5 requests per second per app (regardless of the IP address querying it). This is independent to the rate limit described above, and cannot change. ## For self hosted The SuperTokens core has no rate limit other than for the `/hello` API (which is 5 requests per second per app). You are free to add rate limits to the core by using [a reverse proxy like Nginx](https://www.nginx.com/blog/rate-limiting-nginx/). If you want to implement rate limiting policy similar to the managed service described above, add the following to your `http` and `server` block in the `nginx.conf` file: ```text http { # other configs.. map $request_uri $limit_req_zone_key { "~^/(appId|appid)-(\w+)/?" $binary_remote_addr:$2; default $binary_remote_addr; } limit_req_zone $limit_req_zone_key zone=mylimit:10m rate50/s; limit_req_status 429; # other configs.. upstream supertokens { server localhost:3567; } server { limit_req zone=mylimit burst=50 nodelay; # other configs.. listen 0.0.0.0:80; location / { proxy_pass http://supertokens; } } } ``` In the above, the core adds a rate limit per app per IP address. --- # Scalability Source: https://supertokens.com/docs/deployment/scalability ## Overview The following page addresses how the **SuperTokens** components scale based on different factors. --- ## Users and tenants SuperTokens can handle 10s of millions of users and tenants. In fact, you can even make one tenant per user and it would work well. For most operations, the database structure and queries allow partitioning based on tenants and users. As the number of tenants scales, it does not affect performance on most operations per tenant. Similarly, as the number of users scales, it does not affect performance on most operations per user. --- ## SuperTokens core :::note[If you are using the managed service, the SuperTokens core is fully managed, and you don't have to worry about scaling it.] This section is for those who are self-hosting the SuperTokens core service. ::: The SuperTokens core service supports horizontal scalability. This means that you can add more instances of the core service to handle more requests. The core service is also stateless, which means that you can add or remove instances without worrying about the state of the system. The core service can handle a high number of requests per second (`RPS`). The exact number of requests per second (`RPS`) that the core service can handle depends on the hardware you are using. In general, the core service can manage many requests. For example, the average latency of requests is ~40 milliseconds at 100-150 requests per second (6,000-10,000 requests per minute). The compute deployed is 6 instances of the SuperTokens core service, each on a t3. micro EC2 instance behind a round robin load balancer. The `CPU` usage of each instance is around 10%. The scale of end users that this can support is in the order of 1-2 million monthly active users, with a total user count of millions more. ### Average latency over 1 day ### Number of requests per minute over 1 day ### Performance tuning If you are facing performance issues, here are some tips to help you tune the performance of your SuperTokens setup: - If you are self-hosting the SuperTokens core, know that it is stateless and can scale horizontally. You can add more instances of the core service to handle more requests (behind a load balancer). - Check which part of the request cycle is slow. Is it the SuperTokens core responding, or is it the backend SDK APIs responding? The performance of the backend SDK API depends mainly on how you have set up your API layer (that integrates with the backend SDK) to perform. You can check which is slow by enabling debug logs in the backend SDK, and then inspecting the timestamps around the core requests. If they sum up to be much less than the total time taken for the request (from the `frontend`'s point of view), then the bottleneck is likely in the backend SDK. - If you are self-hosting the SuperTokens core, check if there are any database queries that are too slow. You can do this using debugging tools provided by the PostgreSQL database. If you find a query that's causing issues, please reach out to support. - Check that the compute used to run the backend SDK, the SuperTokens core (in case you are self-hosting it), and the database is sufficient. Using a t3.micro EC2 instance for the core should work well for even 100,000 MAUs. You can check the `CPU` and memory usage of the instances to see if they have maxed out, or if you have run out of `CPU` credits. If they are, you can consider upgrading the instances to more powerful ones. - In case you are self-hosting the SuperTokens core, you can tune its performance by setting different values for the following configurations in the configuration.yaml file, or docker `env`: - `max_server_pool_size`: Sets the max thread pool size for incoming `http` server requests. Default value is 10. - `postgresql_connection_pool_size` (if using psql): Defines the connection pool size to PostgreSQL. Default value is 10. - `postgresql_minimum_idle_connections` (if using psql): Minimum number of idle connections to remain active. If not set, minimum idle connections are the same as the connection pool size. By default, this is not set. - `postgresql_idle_connection_timeout`: (if using psql): Timeout in milliseconds for the idle connections to close. Default is 60000 MS. - Check if you have access token blacklisting enabled in the backend SDK. The default is `false`, but if you have it enabled, then it means that every session verification attempt queries the SuperTokens core to check the database. This adds latency to the session verification process and increases the load on the core. If you want to keep this to `true`, consider making it only for non `GET` APIs for your application. - You can increase the value of `access_token_validity` in the SuperTokens core. It sets the validity of the access token. Default value is 3,600 seconds (1 hour). The lower this value, the more often the refresh API calls the core, increasing the load on the core. --- ## Database SuperTokens works with PostgreSQL databases, and one instance of the database is enough to handle tens of millions of MAUs. For example, a database with 1 million users would occupy ~ 1.5 GB of disk space (assuming you add minimal custom metadata to the user object). --- ## Backend SDK The backend SDK does not store any information on its own. It's a "big middleware" between the frontend requests and the SuperTokens core. As such, its scalability depends entirely on the scalability of your API layer into which the backend SDK integrates. --- ## Session verification The access token is a JWT, and the backend SDK verifies them without any network requests, making them fast and scalable. The core service verifies the refresh token, and the scalability of session refresh requests depends on the core service's scalability. However, session refreshes are rare compared to access token verification. --- # Self-host SuperTokens Source: https://supertokens.com/docs/deployment/self-host-supertokens ## Self-hosting summary - Deploy Core with its Docker image or directly on a VM. - Core 11.0.0 dropped MySQL and MongoDB support; an in-memory database is available for testing. Confirm the supported PostgreSQL range for your exact release. - Core listens on port 3567 by default. `/hello` normally performs a storage read, but rate-limited responses can return 200 without one; it is not a complete database-health or security check. - Docker accepts either `POSTGRESQL_CONNECTION_URI` or separate PostgreSQL host, port, database, username, and password variables. See how you can run **SuperTokens** in your own infrastructure. --- ## Overview One of the main features of **SuperTokens** is that you can run it using your own resources. This way you have full control over the authentication data and you can scale based on your needs. ## Before you start To deploy the Core Service you must configure two things: the actual API and the database. - The core service can be deployed using a **Docker** image or directly inside your VM. - The supported database is **PostgreSQL**. Confirm the supported version range for the exact Core/database-plugin release you select. :::danger SuperTokens Core is a trusted backend component. It exposes APIs that can administer users, sessions, and tenants. Run Core and PostgreSQL on private networks reachable only by trusted backend services; never expose either directly to a browser or any client you do not trust. Core has no API key by default. Configure a generated API key, firewall/security-group rules, and TLS at a trusted proxy or load balancer as defense in depth. Tenant isolation must be enforced by your backend; a shared Core API key does not authorize an end user for a tenant. See [Secure the core](#secure-the-core) for details. ::: The exact PostgreSQL support range and current Core/database artifact mapping are not established by this guide. Verify both for the immutable release selected for production. :::info[**SuperTokens Core** has dropped **MySQL** and **MongoDB** support with the `11.0.0` release.] If you want to reference the old documentation, please [open this page](/legacy/core/v10/self-host-supertokens). ::: ## Steps ### 1. Install SuperTokens core #### With Docker Do not use an untagged image or `latest`. Select and verify an exact supported Core image, pin it by digest, and set it as `SUPERTOKENS_IMAGE`. For a local-only in-memory test, bind Core to `127.0.0.1`: ```bash : "${SUPERTOKENS_IMAGE:?Set an immutable image reference such as repository:version@sha256:digest}" docker run -p 127.0.0.1:3567:3567 -d "$SUPERTOKENS_IMAGE" ``` Omitting PostgreSQL configuration starts the container with an in-memory database. Use this only for testing. #### Without Docker ##### 1. Download SuperTokens 1. **Visit the open source download page** Open the [open source download page](https://SuperTokens.com/use-oss). 2. **Click on the Binary tab** 3. **Choose your database** 4. **Download the SuperTokens zip file for your OS** After downloading, verify the release checksum or signature and extract the archive. You should see a folder named `supertokens`. ##### 2. Install SuperTokens ```bash # sudo is required so that the supertokens # command can be added to your PATH variable. cd supertokens sudo ./install ``` ```bash cd supertokens ./install ``` ```batch Rem run as an Administrator. This is required so that the supertokens Rem command can be added to your PATH. cd supertokens install.bat ``` :::warning[You may get an error like `java cannot be opened because the developer cannot be verified`. To solve this, visit System Preferences > Security & Privacy > General Tab, and then click on the Allow button at the bottom. Then retry the command above.] ::: :::note[After installing, you can delete the downloaded folder as you no longer need it.] Make any changes to the configuration in the `config.yaml` file in the installation directory, as specified in the output of the `supertokens --help` command. ::: ##### 3. Start the core service Running the following command starts the service. ```bash supertokens start [--host=...] [--port=...] ``` - The above command starts the Core service using the configured database. - To see all available options please run `supertokens start --help` :::info[Tip] To stop the service, run the following command: ```bash supertokens stop ``` ::: ### 2. Test that the service is running Open a browser and visit `http://localhost:3567/hello`. If you see a page that says `Hello` back, then the container started successfully! If you are having issues with starting the docker image, please feel free to reach out [over email](mailto:support@supertokens.com) or [via Discord](https://supertokens.com/discord). :::tip `/hello` normally performs a storage read and returns an error if that read fails. However, after its request-rate limit is exhausted, it can return `200 Hello` without querying storage. It also deliberately requires no API key. Use it only as a basic process/readiness signal, not as proof of database health, API-key enforcement, or safe network exposure. Pair it with authenticated application checks and database monitoring; tune liveness separately to avoid restart loops. ::: ### 3. Connect the backend SDK with SuperTokens - The default port for SuperTokens is `3567`. Keep it private. For local testing, bind it only to `127.0.0.1`, for example `-p 127.0.0.1:8080:3567`. - The connection info goes in the `supertokens` object in the `init` function on your backend: ```tsx import supertokens from "supertokens-node"; const apiKey = process.env.SUPERTOKENS_API_KEY; if (apiKey === undefined || apiKey.length === 0) { throw new Error("SUPERTOKENS_API_KEY is required"); } supertokens.init({ supertokens: { connectionURI: "http://localhost:3567", apiKey, }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [], }); ``` ```go import ( "os" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiKey := os.Getenv("SUPERTOKENS_API_KEY") if apiKey == "" { panic("SUPERTOKENS_API_KEY is required") } supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:3567", APIKey: apiKey, }, }) } ``` ```python check=false reason="Partial configuration example" import os from supertokens_python import init, InputAppInfo, SupertokensConfig api_key = os.environ["SUPERTOKENS_API_KEY"] if not api_key: raise RuntimeError("SUPERTOKENS_API_KEY is required") init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='http://localhost:3567', api_key=api_key ), framework='...', recipe_list=[ #... ] ) ``` :::info[Configure the same generated secret in Core and every backend] Generate a key with `openssl rand -hex 32`, store it in your secret manager, and inject it as Core's `API_KEYS` and the `SUPERTOKENS_API_KEY` used by the backend. Never bake it into source code or an image. See the [API-key documentation](/platform-configuration/supertokens-core/api-keys) for validation and rotation. ::: ### 4. Set up the database #### 4.1 Create a database (optional) ```sql CREATE DATABASE supertokens; ``` You can skip this step if you want SuperTokens to write to your own database. In this case, you need to provide your database's name as shown in the step below. #### 4.2 Connect SuperTokens to your database ##### With Docker :::warning Inside a container, `localhost` refers to that container. Attach Core and PostgreSQL to the same private Docker network, or use a private DNS name/interface. Configure PostgreSQL `listen_addresses`, `pg_hba.conf`, host firewall, and cloud security groups so that only Core can connect. Do not publish port 5432 or expose the database through a public address. ::: :::warning[It is important to use the `postgresql://` scheme designator in the PostgreSQL Connection URI. Using `postgres://` will lead to a startup error.] ::: ```bash : "${SUPERTOKENS_IMAGE:?Set an immutable Core image reference}" : "${SUPERTOKENS_API_KEY:?Set a generated Core API key}" docker run \ --network app-network \ -e POSTGRESQL_CONNECTION_URI="postgresql://username:pass@host/dbName" \ -e API_KEYS="$SUPERTOKENS_API_KEY" \ -d "$SUPERTOKENS_IMAGE" # OR docker run \ --network app-network \ -e POSTGRESQL_USER="username" \ -e POSTGRESQL_PASSWORD="password" \ -e POSTGRESQL_HOST="host" \ -e POSTGRESQL_PORT="5432" \ -e POSTGRESQL_DATABASE_NAME="supertokens" \ -e API_KEYS="$SUPERTOKENS_API_KEY" \ -d "$SUPERTOKENS_IMAGE" ``` :::tip[You can also provide the table schema by setting the `POSTGRESQL_TABLE_SCHEMA` option.] ::: ##### Without Docker ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command postgresql_connection_uri: "postgresql://username:pass@host/dbName" # OR postgresql_user: "username" postgresql_password: "password" postgresql_host: "host" postgresql_port: "5432" postgresql_database_name: "supertokens" ``` You can also provide the table schema by setting the `postgresql_table_schema` option. :::info Core creates and migrates its required tables automatically when the database principal has DDL permission. Do not use a hand-copied schema: it can drift from the selected Core and database-plugin release. If your production principal cannot perform DDL, obtain a schema or migration artifact generated for the exact immutable release, apply it with a separate migration principal, and test Core startup before promotion. ::: #### 4.3 Test the connection Start the exact Core release against a staging copy of the database and require startup/migration success. Then exercise an authenticated SDK operation. A query against one table does not prove that every required migration was applied. #### 4.4 Rename database tables (optional) :::warning[If you already have tables created by SuperTokens, and then you rename them, SuperTokens creates new tables. Please be sure to migrate the data from the existing one to the new one.] ::: You can add a prefix to all table names that SuperTokens manages. This way, all will be renamed in a way that has no clashes with your tables. For example, two tables created by SuperTokens have the names `emailpassword_users` and `thirdparty_users`. If you add a prefix to them (something like `"my_prefix"`), then the tables become `my_prefix_emailpassword_users` and `my_prefix_thirdparty_users`. ```bash docker run \ --network app-network \ -e POSTGRESQL_TABLE_NAMES_PREFIX="my_prefix" \ -e API_KEYS="$SUPERTOKENS_API_KEY" \ -d "$SUPERTOKENS_IMAGE" ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command postgresql_table_names_prefix: "my_prefix" ``` ### 5. Add license keys To access some features in your self-hosted service you must use **license keys**. You can sign up on [**SuperTokens**](https://supertokens.com/auth) to receive one. Once you have the license key you need to manually add it to your **SuperTokens Core Instance**. To do this you have to call the Core API with the following request: ```bash title="Add License Key" curl --location --request PUT "${CORE_API_ENDPOINT:?Set the private Core endpoint}/ee/license" \ --header 'Content-Type: application/json' \ --header "api-key: ${SUPERTOKENS_API_KEY:?Set the Core API key}" \ --data-raw "{ \"licenseKey\": \"${SUPERTOKENS_LICENSE_KEY:?Set the license key}\" }" ``` ## Secure the core The SuperTokens Core exposes administrative operations over its API — creating and updating users, issuing password-reset and passwordless codes, managing tenants, and more. By design these are available to the connecting backend, because the Core has no direct channel to your frontend and relies on your backend to mediate every request and to deliver codes and tokens to end users. This trust model means the Core must be treated like your database: reachable only by your own backend, never by untrusted clients. - **Isolate the network.** Run the Core on a private network or subnet that only your backend can reach. This is the primary protection and applies regardless of any other setting. - **Set an API key.** No API key exists by default, so any caller that can reach an unprotected Core can perform administrative operations. Configure a generated [API key](/platform-configuration/supertokens-core/api-keys) as defense in depth. Core supports multiple keys for rotation, but these are not per-tenant authorization credentials and do not replace network isolation. - **Restrict by IP and use TLS.** Limit access with firewall/security-group rules and, optionally, Core's [IP allow/deny configuration](/platform-configuration/supertokens-core/ip-allow-deny). Terminate [TLS/SSL](/platform-configuration/supertokens-core/add-ssl-via-nginx) at a trusted proxy or load balancer. - **Enforce tenant scoping in your backend.** For session-authenticated requests restricted to a specific tenant, verify the session and check that its tenant matches the required tenant. Authorize access to tenant-specific resources in your backend; do not rely on the URL alone. --- # OpenTelemetry Integration Source: https://supertokens.com/docs/deployment/telemetry ## Overview This tutorial shows you how to add **OpenTelemetry** logging to all the **SuperTokens** APIs and function calls using the **OpenTelemetry plugin**. The guide makes use of the plugins functionality to automatically instrument your authentication flows with distributed tracing. ## How it works The plugin manually adds traces to all overridable functions and APIs in SuperTokens. When you initialize the OpenTelemetry SDK alongside this plugin, you automatically get comprehensive tracing at the API level. The plugin provides built-in data protection by automatically removing sensitive fields from traces. ## Before you start The OpenTelemetry plugin supports only the `NodeJS` SDK. Support for other platforms is under active development. Make sure you have the OpenTelemetry SDK installed and configured in your application. For detailed instructions, see the [OpenTelemetry Node.js Getting Started Guide](https://opentelemetry.io/docs/languages/js/getting-started/nodejs/#instrumentation). The implementation is in early stages and APIs might change. For more information on how plugins work, refer to the [references page](/references/plugins/introduction). ## Steps ### 1. Install the plugin ```bash npm install @supertokens-plugins/opentelemetry-nodejs ``` ### 2. Configure the OpenTelemetry SDK Set up the OpenTelemetry SDK in your application. Here's a basic configuration: ```typescript /*instrumentation.ts*/ import { NodeSDK } from "@opentelemetry/sdk-node"; import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; import { PeriodicExportingMetricReader, ConsoleMetricExporter } from "@opentelemetry/sdk-metrics"; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), metricReader: new PeriodicExportingMetricReader({ exporter: new ConsoleMetricExporter(), }), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); ``` :::note For a more thorough explanation on how to setup OpenTelemetry please refer to the [official documentation](https://opentelemetry.io/docs/languages/js/getting-started/nodejs). ::: ### 3. Update your backend SDK configuration Initialize the **SuperTokens** plugin inside your SDK configuration file. ```typescript import SuperTokens from "supertokens-node"; import OpenTelemetryPlugin from "@supertokens-plugins/opentelemetry-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [OpenTelemetryPlugin.init()], }, }); ``` ### 4. Test your setup Inspect your traces and look for **SuperTokens** specific spans. You should be able to identify them based on the following naming schemes: - `.function.` - `.api.` Examples: `emailpassword.function.signIn`, `thirdparty.api.signInUpPOST`, `multitenancy.function.getTenant` ## Customization ### Data protection By default, the plugin removes sensitive fields from traces to protect user data. The following fields are automatically filtered out: `password`, `email(s)`,`phoneNumber(s)`, `accessToken`,`refreshToken`. #### Adding custom sensitive fields You can extend the list of sensitive fields by overriding the `getSensitiveFields` function: ```typescript import SuperTokens from "supertokens-node"; import OpenTelemetryPlugin from "@supertokens-plugins/opentelemetry-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ OpenTelemetryPlugin.init({ override: (oI) => ({ ...oI, getSensitiveFields: (defaultSensitiveFields: string[]) => [ ...defaultSensitiveFields, "customSecretField", "userSecret", ], }), }), ], }, }); ``` #### Advanced data transformation For more granular control over data handling in traces, you can override the transformation functions: ```typescript import SuperTokens from "supertokens-node"; import OpenTelemetryPlugin from "@supertokens-plugins/opentelemetry-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ OpenTelemetryPlugin.init({ override: (oI) => ({ ...oI, transformInputToAttributes: (input: any) => { // Custom logic to transform input data for traces return { userId: input.userId, action: input.action, // Exclude other sensitive data }; }, transformResultToAttributes: (result: any) => { // Custom logic to transform result data for traces return { userId: result.userId, // Only include non-sensitive result data }; }, }), }), ], }, }); ``` #### Using OpenTelemetry data protection Alternatively, you can disable the built-in data filtering and use OpenTelemetry's own [data protection mechanisms](https://opentelemetry.io/docs/security/handling-sensitive-data/): ```typescript import SuperTokens from "supertokens-node"; import OpenTelemetryPlugin from "@supertokens-plugins/opentelemetry-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ OpenTelemetryPlugin.init({ override: (oI) => ({ ...oI, getSensitiveFields: () => [], // Disable built-in filtering }), }), ], }, }); ``` ## See also Besides OpenTelemetry integration, you can also look into other deployment and monitoring features: Self-host SuperTokens in your own infrastructure. Configure rate limits to protect your authentication endpoints. Scale SuperTokens for high-traffic applications. General information on how plugins work. --- # Integrate with AI Source: https://supertokens.com/docs/integrate-with-ai ## Ask AI Use the **Ask AI** action in the documentation to ask questions about SuperTokens. It answers from the current page and relevant SuperTokens documentation, and includes links to the pages it used. ## Connect an AI coding agent Connect your AI coding tool to the public, read-only SuperTokens documentation MCP server. The server lets an agent search and read the documentation without scraping web pages or requiring credentials. ```bash claude mcp add --transport http supertokens-docs https://supertokens.com/docs/mcp ``` Use the **Connect to MCP** action on any documentation page for client-specific setup instructions. The server exposes these tools: - `search_docs`: Search the documentation and return matching pages with excerpts. - `get_page`: Read a documentation page as agent-optimized Markdown. - `list_pages`: List every page with its route, title, description, and content type. - `get_navigation`: Read the documentation navigation hierarchy. ## Machine-readable documentation ### Markdown responses AI tools can request agent-optimized Markdown instead of HTML by sending an `Accept: text/markdown` header. This preserves the tool's context window and includes supported components as plain Markdown. ```bash curl -sL -H "Accept: text/markdown" https://supertokens.com/docs/integrate-with-ai ``` ### Direct Markdown routes Append `.md` to a documentation URL to retrieve agent-optimized Markdown. Append `.mdx` to retrieve the original MDX source. For example: [integrate-with-ai.md](https://supertokens.com/docs/integrate-with-ai.md). Use **Copy as Markdown** to copy the current page, or **Open in chat** to open it in a supported AI assistant with the page context. ### Site indexes The documentation also provides machine-readable site indexes: - [/llms.txt](https://supertokens.com/docs/llms.txt) is a compact, structured index of the documentation. - [/llms-full.txt](https://supertokens.com/docs/llms-full.txt) contains the full documentation corpus. It can exceed an AI tool's context window. - [/agent-readability.json](https://supertokens.com/docs/agent-readability.json) advertises these agent-facing documentation surfaces for automatic discovery. The dashboard application owns MCP discovery under `/.well-known`. Connect clients directly to the MCP URL above. --- # AppSync integration Source: https://supertokens.com/docs/integrations/aws-lambda/appsync-integration ## Overview A Lambda authorizer configured as described in the [authorizer guide](/integrations/aws-lambda/session-verification#using-lambda-authorizers) can protect GraphQL HTTP operations sent from API Gateway to AppSync. :::warning This architecture is not implementation-ready without a deployed IaC fixture. The exact service-integration path, required `Host`/`Content-Type` handling, request and response mappings, GraphQL errors, and cookie behavior must be tested for the selected API Gateway type. It does not proxy AppSync real-time WebSocket subscriptions. ::: ## Before you start Configure SuperTokens in AWS Lambda by following the [AWS Lambda integration guide](/integrations/aws-lambda/quickstart-guide). ## Steps ### 1. Set up AppSync authorization Use `AWS_IAM` authorization so API Gateway signs requests with its execution role. Grant that role only `appsync:GraphQL` for the root fields this integration needs: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "appsync:GraphQL", "Resource": [ "arn:aws:appsync:::apis//types/Query/fields/", "arn:aws:appsync:::apis//types/Mutation/fields/" ] } ] } ``` Do not attach `AWSAppSyncInvokeFullAccess`; it includes broad read/list access and wildcard resources. This architecture uses IAM exclusively for the API Gateway-to-AppSync hop and does not inject shared credentials into integration headers. ### 2. Configure API Gateway with the authorizer Follow the [authorizer guide](/integrations/aws-lambda/session-verification#using-lambda-authorizers) to create `/auth` and `/graphql` resources. Point `/auth` to the Lambda function that handles the auth APIs and require the Lambda authorizer on `POST /graphql`. Configure `POST /graphql` as an AWS service integration that invokes only the target AppSync GraphQL API with the least-privilege execution role above. Do not infer the current console's service, subdomain, or path-override values from this page. Capture them in reproducible IaC and prove the generated request reaches the target API before publishing the integration. ### 3. Set up integration headers - Set the integration request's `x-user-id` header from `context.authorizer.principalId`. This must overwrite any client-supplied `x-user-id`; never pass the incoming identity header through. - Set the required `Content-Type` for the GraphQL request and map the request body without changing the GraphQL document or variables. Verify these mappings in the IaC E2E fixture. ### 4. Consume the context in resolvers In a VTL resolver, read the mapped user ID with: ```text $context.request.headers.get("x-user-id") ``` Treat this value as trusted only after an E2E test proves API Gateway overwrites a spoofed client header after successful authorization. Use it for application-level ownership checks; the execution role limits which root fields API Gateway can invoke, but does not implement per-user authorization inside a resolver. See the [resolver context documentation](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#access-request-headers). ### 5. Validate the deployed integration The required IaC fixture must cover valid, missing, expired, and invalid sessions; a spoofed `x-user-id`; IAM denial for fields outside the allowed field list; request and response body mappings; GraphQL errors; and browser CORS behavior. AppSync subscriptions use a separate real-time WebSocket endpoint and are outside this HTTP proxy design. --- # Quickstart Guide Source: https://supertokens.com/docs/integrations/aws-lambda/quickstart-guide The following guide shows you how to use **SuperTokens** in an AWS Lambda environment. You can also check out the [example repository](https://github.com/supertokens/supertokens-node/tree/master/examples/aws/with-emailpassword) for a full implementation. ## Before you start These instructions assume that you have completed the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk). If not, please go through it and create the example application before you start this tutorial. ## Steps :::warning Follow the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk) first to learn how to set up the frontend. ::: ### 1. Set up API Gateway #### 1.1 Create a REST API Gateway We will be using AWS API Gateway to create a REST API that will be used to communicate with our Lambda functions. Create API gateway step UI #### 1.2 Set up authentication routes Create an `/auth` resource and then an `/auth/{proxy+}` resource. This will act as a catch-all for all SuperTokens auth routes. Create proxy route step UI Route creation complete step UI #### 1.3 Attach a Lambda function to the `ANY` method of the proxy resource Click on the "ANY" method and then "Integration" to configure the Lambda function. Check **Lambda proxy integration** and then select your lambda function. Configure lambda integration UI :::note[Ensure that the **Lambda proxy integration** toggle is turned on.] ::: #### 1.4 Configure CORS for the proxy path Click on the `{proxy+}` resource and then "Enable CORS" button to open the CORS configuration page. Enable CORS for the proxy path UI Configure an `OPTIONS` response for `/auth/{proxy+}` with: - `Access-Control-Allow-Origin: `, using the exact trusted website origin. - `Access-Control-Allow-Credentials: true`. - `Access-Control-Allow-Headers` containing `Content-Type` and every value returned by the backend SDK's `getAllCORSHeaders`/`get_all_cors_headers` function. - `Access-Control-Allow-Methods` containing every method your API accepts, including `OPTIONS`. Do not use `*` for `Access-Control-Allow-Origin` with credentialed browser requests. Because this is a Lambda proxy integration, the Lambda response must also include the CORS headers on actual requests. Configure gateway-generated errors separately if your browser client must read their responses. CORS configuration page #### 1.5 Deploy the API Gateway Deploy the API to a stage named `dev` and record its invoke URL. AWS changes console labels periodically; verify the resource, integration, `OPTIONS`, and gateway-response configuration in the deployed stage rather than relying only on the screenshots in this guide. :::note[Update `apiDomain`, `apiBasePath`, and `apiGatewayPath` in both Lambda configuration and your frontend config if they have changed post API Gateway configuration.] ::: ### 2. Set up Lambda layer #### 2.1 Create Lambda layer with required libraries Build the layer in the AWS SAM build image for the function's exact runtime and architecture. The commands below target Lambda `x86_64` (`linux/amd64`). For a Lambda `arm64` function, change `PLATFORM` to `linux/arm64`. Do not build native dependencies on an unrelated workstation OS or architecture. For Node.js, create `package.json` with exact direct dependency versions. Generate and review `package-lock.json` once, commit it with the Lambda source, and build only with `npm ci`. The lock file pins the complete transitive graph; `package.json` alone is not a deployment lock. For Python, create `requirements.in` with exact direct dependency versions. Compile and commit a hash-locked `requirements.lock`, then install with `--require-hashes`. Do not deploy directly from `requirements.in`. ```json title="package.json" { "private": true, "type": "module", "dependencies": { "@middy/core": "7.9.2", "@middy/http-cors": "7.9.2", "supertokens-node": "24.0.3" } } ``` ```bash PLATFORM=linux/amd64 BUILD_IMAGE=public.ecr.aws/sam/build-nodejs24.x:1.165.0 # Run this only when intentionally updating the committed lock file. docker run --rm --platform "$PLATFORM" \ --volume "$PWD:/var/task" --workdir /var/task \ "$BUILD_IMAGE" npm install --package-lock-only --ignore-scripts # Reproducible layer build from the reviewed lock file. rm -rf node_modules nodejs supertokens-node.zip docker run --rm --platform "$PLATFORM" \ --volume "$PWD:/var/task" --workdir /var/task \ "$BUILD_IMAGE" npm ci --omit=dev mkdir nodejs cp -R node_modules nodejs/ zip -r supertokens-node.zip nodejs/ ``` ```text title="requirements.in" fastapi==0.141.1 mangum==0.22.0 nest-asyncio==1.6.0 supertokens-python==0.31.3 ``` ```bash PLATFORM=linux/amd64 BUILD_IMAGE=public.ecr.aws/sam/build-python3.14:1.165.0 # Run this only when intentionally updating the committed lock file. docker run --rm --platform "$PLATFORM" \ --volume "$PWD:/var/task" --workdir /var/task \ "$BUILD_IMAGE" sh -c \ 'python -m pip install "pip-tools==7.6.1" && pip-compile --generate-hashes --output-file requirements.lock requirements.in' # Reproducible layer build from exact versions and package hashes. rm -rf python supertokens-python.zip docker run --rm --platform "$PLATFORM" \ --volume "$PWD:/var/task" --workdir /var/task \ "$BUILD_IMAGE" python -m pip install \ --require-hashes --only-binary=:all: --target python --requirement requirements.lock zip -r supertokens-python.zip python/ ``` For Node.js, pin the SAM image by digest in CI after verifying that the digest matches the selected platform. The version tag above prevents implicit SAM CLI upgrades, while the digest prevents registry-tag movement. For Python, pin the SAM image by platform-specific digest in CI. Hash locking protects downloaded Python distributions; the image digest protects the build tools and Amazon Linux environment. #### 2.2 Upload the SuperTokens Lambda layer Open AWS Lambda dashboard and click on layers: AWS Lambda sidebar UI Click "Create Layer" button: Create layer button UI Name the layer, upload the ZIP file, and select the same runtime family and architecture used for the container build. These examples target the Amazon Linux 2023 Node.js 24 (`nodejs24.x`) and Python 3.14 (`python3.14`) runtime versions. Test dependency lock updates before promotion. Monitor the [Lambda runtime support schedule](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) and upgrade before deprecation. Lambda layer node configuration UI Lambda layer python configuration UI ### 3. Set up the Lambda function #### 3.1 Create a new Lambda function Click "Create Function" in the AWS Lambda dashboard, enter the function name and runtime, and create your Lambda function. Create new Lambda configurations UI Node Create new Lambda configurations UI Python #### 3.2 Link the Lambda layer with the Lambda function Scroll to the bottom and look for the `Layers` tab. Click on `Add a layer` Link Lambda function with the Lambda layer Select `Custom Layer` and then select the layer created in step 2: Link custom layer with Lambda function Node Link custom layer with Lambda function Python #### 3.3 Create a backend config file Using the editor provided by AWS, create a new config file and write the following code: ```javascript title="config.mjs" import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; export function getBackendConfig() { return { framework: "awsLambda", supertokens: { connectionURI: "", }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", apiGatewayPath: "/dev", }, recipeList: [EmailPassword.init(), Session.init()], isInServerlessEnv: true, }; } ``` ```python title="config.py" from supertokens_python.recipe import emailpassword, session from supertokens_python import SupertokensConfig, InputAppInfo supertokens_config = SupertokensConfig( connection_uri="", ) app_info = InputAppInfo( # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth", api_gateway_path="/dev", ) framework = "fastapi" recipe_list = [ session.init(), emailpassword.init(), ] ``` :::note[In the above code, notice the extra config of `apiGatewayPath` that was added to the `appInfo` object.] The value of this should be whatever you have set as the value of your [AWS stage](https://docs.aws.amazon.com/apigateway/latest/developerguide/stages.html) which scopes your API endpoints. For example, you may have a stage name for each environment: - One for development (`/dev`). - One for testing (`/test`). - One for prod (`/prod`). So the value of `apiGatewayPath` should be set according to the above based on the environment it's running under. You also need to prepend the stage to `apiBasePath` in the frontend config. For example, when the frontend calls the development stage and the backend `apiBasePath` is `/auth`, set the frontend value to `/dev/auth`. ::: :::note[You may edit the `apiBasePath` and `apiGatewayPath` values later if you have not set up API Gateway yet.] ::: #### 3.4 Add the SuperTokens auth middleware Using the editor provided by AWS, create/replace the handler file contents with the following code: ```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/awsLambda"; import { getBackendConfig } from "./config.mjs"; import middy from "@middy/core"; import cors from "@middy/http-cors"; supertokens.init(getBackendConfig()); export const handler = middy( middleware((event) => { // SuperTokens middleware didn't handle the route, return your custom response return { body: JSON.stringify({ msg: "Hello!", }), statusCode: 200, }; }), ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }), ) .onError((request) => { throw request.error; }); ``` ```python title="handler.py" check=false reason="Requires surrounding application context" import nest_asyncio nest_asyncio.apply() from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from mangum import Mangum from supertokens_python import init, get_all_cors_headers from supertokens_python.framework.fastapi import get_middleware import config init( supertokens_config=config.supertokens_config, app_info=config.app_info, framework=config.framework, recipe_list=config.recipe_list, mode="asgi", ) app = FastAPI(title="SuperTokens Example") app.add_middleware(get_middleware()) app = CORSMiddleware( app=app, allow_origins=[ config.app_info.website_domain ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) handler = Mangum(app) ``` Add SuperTokens auth middleware UI The `.mjs` files use native ECMAScript modules. Supported Node.js Lambda runtimes load them without the deprecated `--experimental-specifier-resolution=node` option. Keep explicit file extensions on relative imports. #### 3.5 Filter additional plugins or extensions (optional) If you are using AWS Lambda plugins, extensions, or anything that adds events to the lambda function (e.g. `serverless-plugin-warmup`), then you may need to prevent calling SuperTokens with them. These kinds of events lack request details that SuperTokens expects and might lead to unintended errors. Here's an example of how you can filter them out: ```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/awsLambda"; import { getBackendConfig } from "./config.mjs"; import middy from "@middy/core"; import cors from "@middy/http-cors"; supertokens.init(getBackendConfig()); const httpHandler = middy( middleware((event) => { // SuperTokens middleware didn't handle the route, return your custom response return { body: JSON.stringify({ msg: "Hello!", }), statusCode: 200, }; }), ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }), ) .onError((request) => { throw request.error; }); const postAuth = async (event, context) => { // Plugins generally inject a `source` property in the event object. if (event.source === "serverless-plugin-warmup") { console.info("postAuth 010: warming up lambda. Bypassing authMiddleware."); return { statusCode: 200, body: JSON.stringify({ message: "Warm-up successful" }), }; } return httpHandler(event, context); }; export const handler = postAuth; ``` --- # Session Verification Source: https://supertokens.com/docs/integrations/aws-lambda/session-verification The following page shows three ways to verify sessions in a Lambda integration. Choose the one that works best based on the particularities of your use case. :::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: ## Using Session Verification When building your own APIs, you may need to verify the session of the user before proceeding further. SuperTokens SDK exposes a `verifySession` function that can be utilized for this. In this guide, we will be creating a `/user` `GET` route that will return the current session information. ### 1. Add `/user` `GET` route in your API Gateway Create a `/user` resource and then `GET` method in your API Gateway. Configure the lambda integration and CORS just like we did [for the auth routes](/integrations/aws-lambda/quickstart-guide#13-attach-lambda-to-the-any-method-of-the-proxy-resource). ### 2. Create a file in your Lambda function to handle the `/user` route An example of this is [here](https://github.com/supertokens/supertokens-node/blob/master/examples/aws/with-emailpassword/backend/user.mjs). ```javascript title="user.mjs" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { getBackendConfig } from "./config.mjs"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import middy from "@middy/core"; import cors from "@middy/http-cors"; supertokens.init(getBackendConfig()); const lambdaHandler = async (event) => { return { body: JSON.stringify({ sessionHandle: event.session?.getHandle(), userId: event.session?.getUserId(), accessTokenPayload: event.session?.getAccessTokenPayload(), }), statusCode: 200, }; }; export const handler = middy(verifySession(lambdaHandler)) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }), ) .onError((request) => { throw request.error; }); ``` ```python title="handler.py" check=false reason="Requires surrounding framework application context" import nest_asyncio nest_asyncio.apply() from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from mangum import Mangum from supertokens_python import init, get_all_cors_headers from supertokens_python.framework.fastapi import get_middleware import config init( supertokens_config=config.supertokens_config, app_info=config.app_info, framework=config.framework, recipe_list=config.recipe_list, mode="asgi", ) app = FastAPI(title="SuperTokens Example") from fastapi import Depends from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.session import SessionContainer @app.get("/user") def user(s: SessionContainer = Depends(verify_session())): return { "sessionHandle": s.get_handle(), "userId": s.get_user_id(), "accessTokenPayload": s.get_access_token_payload() } app.add_middleware(get_middleware()) app = CORSMiddleware( app=app, allow_origins=[ config.app_info.website_domain ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) handler = Mangum(app) ``` Now, import this function in your `index.mjs` handler file as shown below: :::note[The `verify_session` middleware automatically returns a 401 Unauthorized error if the session is not valid. You can alter the default behavior by passing `session_required=False` to the `verify_session` middleware.] If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `init` function and include the `session` recipe in the `recipe_list` for each respective lambda function. ::: ```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/awsLambda"; import { getBackendConfig } from "./config.mjs"; import middy from "@middy/core"; import cors from "@middy/http-cors"; import { handler as userHandler } from "./user.mjs"; supertokens.init(getBackendConfig()); export const handler = middy( middleware((event) => { if (event.path === "/user") { return userHandler(event); } return { body: JSON.stringify({ msg: "Hello!", }), statusCode: 200, }; }), ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }), ) .onError((request) => { throw request.error; }); ``` :::note[The `verifySession` middleware automatically returns a 401 Unauthorized error if the session is not valid. You can alter the default behavior by passing `{ sessionRequired: false }` as the second argument to the `verifySession` middleware.] If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `supertokens.init` and include the `Session` recipe in the `recipeList` for each respective lambda function. ::: --- ## Using Lambda Authorizers You can use a Lambda authorizer with an API Gateway REST API to authorize requests to another integration, such as AppSync. The authorizer below requires a valid session and returns its user ID as `principalId`. API Gateway can map `$context.authorizer.principalId` to an integration header. Missing and invalid sessions are rejected; this guide does not claim support for optional sessions because AWS's behavior for an empty principal is not established here. ### 1. Add configurations and dependencies Refer to the [frontend](/quickstart#1-integrate-the-frontend-sdk), [lambda layer](/integrations/aws-lambda/quickstart-guide#2-set-up-lambda-layer), and [lambda setup](/integrations/aws-lambda/quickstart-guide#3-set-up-lambda). ### 2. Add code to the lambda function handler Use the code below as the handler for the lambda. Remember that whenever we want to use any functions from the `supertokens-python` lib, we have to call the `init` function at the top of that serverless function file. We can then use `get_session()` to get the session. Use the code below as the handler for the lambda. Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. We can then use `getSession()` to get the session. ```python title="auth.py" check=false reason="Requires surrounding framework application context" import nest_asyncio import json nest_asyncio.apply() from typing import Optional, Dict, Any from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from mangum import Mangum from supertokens_python import init, get_all_cors_headers from supertokens_python.framework.fastapi import get_middleware import config init( supertokens_config=config.supertokens_config, app_info=config.app_info, framework=config.framework, recipe_list=config.recipe_list, mode="asgi", ) app = FastAPI(title="SuperTokens Example") def generate_policy(principal_id: str, effect: str, resource: str, context: Optional[Dict[str, Any]]): policy_document = { "Version": "2012-10-17", "Statement": [ {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource} ], } auth_response = { "principalId": principal_id, "policyDocument": policy_document, "context": context or {}, } return auth_response def generate_allow(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None): return generate_policy(principal_id, "Allow", resource, context) def generate_deny(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None): return generate_policy(principal_id, "Deny", resource, context) from fastapi import Request from supertokens_python.recipe.session.syncio import get_session from supertokens_python.recipe.session.exceptions import (InvalidClaimsError, TryRefreshTokenError, UnauthorisedError) @app.get("/{full_path:path}") def handle_auth(request: Request, full_path: str): event = request.scope["aws.event"] method_arn = event.get("methodArn") try: session = get_session(request) return generate_allow(session.get_user_id(), method_arn) except Exception as e: if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError): raise Exception("Unauthorized") if isinstance(e, InvalidClaimsError): claim_validation_errors = [err.to_json() for err in e.payload] return generate_deny( "invalid-claims", method_arn, { "body": json.dumps({ "message": "invalid claims", "claimValidationErrors": claim_validation_errors, }) }, ) raise e app.add_middleware(get_middleware()) app = CORSMiddleware( app=app, allow_origins=[ config.app_info.website_domain ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) def handler(event: Dict[str, Any], context: Any): mangum_handler = Mangum(app) response: Dict[str, Any] = mangum_handler(event, context) if event.get("methodArn"): return json.loads(response["body"]) return response ``` ```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import { getBackendConfig } from "./config.mjs"; supertokens.init(getBackendConfig()); export const handler = async function (event) { try { const session = await Session.getSession(event, event); return generateAllow(session.getUserId(), event.methodArn); } catch (ex) { if (ex.type === "TRY_REFRESH_TOKEN" || ex.type === "UNAUTHORISED") { throw new Error("Unauthorized"); } if (ex.type === "INVALID_CLAIMS") { return generateDeny("invalid-claims", event.methodArn, { body: JSON.stringify({ message: "invalid claim", claimValidationErrors: ex.payload, }), }); } throw ex; } }; const generatePolicy = function (principalId, effect, resource, context = {}) { const policyDocument = { Version: "2012-10-17", Statement: [], }; const statementOne = { Action: "execute-api:Invoke", Effect: effect, Resource: resource, }; policyDocument.Statement[0] = statementOne; const authResponse = { principalId: principalId, policyDocument: policyDocument, context, }; return authResponse; }; const generateAllow = function (principalId, resource, context) { return generatePolicy(principalId, "Allow", resource, context); }; const generateDeny = function (principalId, resource, context) { return generatePolicy(principalId, "Deny", resource, context); }; ``` The authorizer `context` map may contain only scalar values. The invalid-claims body is therefore JSON-serialized in both examples. Do not join multiple `Set-Cookie` values into one authorizer context string: commas are valid inside cookie attributes and API Gateway may not reconstruct the original headers. Return auth-route cookies from the Lambda proxy response as distinct values: REST API payload format 1.0 uses `multiValueHeaders: { "Set-Cookie": cookies }`, while HTTP API payload format 2.0 uses the top-level `cookies` array. Before relying on cookie mutation from an authorizer, an E2E fixture must prove no-cookie, one-cookie, multiple-cookie, refresh, denied, and gateway-error paths for the exact REST/HTTP API payload version in use. ### 3. Configure the authorizer Create a request-based Lambda authorizer for the REST API and point it to the function above. AWS changes console labels; capture this configuration in IaC and verify that the deployed authorizer receives the headers and cookies required by your selected SuperTokens token-transfer method. ### 4. Configure API Gateway - Require the authorizer on each protected method. - In the integration request, overwrite `x-user-id` from `context.authorizer.principalId`. Never forward a client-supplied identity header. - If the browser must read gateway-generated `401` or `403` responses, configure them with the exact trusted `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials: true`. Do not combine credentials with a wildcard origin. - Deploy and test the API. The IaC fixture must prove that a spoofed identity header cannot reach the integration. --- ## Using JWT Authorizers :::warning AWS supports JWT authorizers for HTTP APIs and not REST APIs on the API Gateway service. For REST APIs follow the [Lambda authorizer](/integrations/aws-lambda/session-verification#using-lambda-authorizers) guide This guide will work if you are using **SuperTokens Session Tokens**. If you are implementing an **OAuth2** setup, through the [**Unified Login**](/authentication/unified-login/introduction) or the [**Microservice Authentication**](/authentication/m2m/client-credentials) features, you will have to manually set the token audience property. Please check the referenced pages for more information. ::: ### 1. Add the `aud` claim in the JWT based on the authorizer configuration ```javascript title="config.mjs" import Session from "supertokens-node/recipe/session"; export function getBackendConfig() { return { framework: "awsLambda", supertokens: { connectionURI: "", }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", apiGatewayPath: "/dev", }, recipeList: [ Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, override: { functions: function (originalImplementation) { return { ...originalImplementation, createNewSession: async function (input) { input.accessTokenPayload = { ...input.accessTokenPayload, /* * AWS requires JWTs to contain an audience (aud) claim * The value for this claim should be the same * as the value you set when creating the * authorizer */ aud: "jwtAuthorizers", }; return originalImplementation.createNewSession(input); }, }; }, }, }), ], isInServerlessEnv: true, }; } ``` ```python title="config.py" from supertokens_python.recipe import session from supertokens_python import ( InputAppInfo, SupertokensConfig, ) from supertokens_python.recipe.session.interfaces import RecipeInterface as SessionRecipeInterface from typing import Any, Dict, Optional from supertokens_python.types import RecipeUserId supertokens_config = SupertokensConfig( connection_uri="", ) app_info = InputAppInfo( # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth", api_gateway_path="/dev", ) framework = "fastapi" def override_session_functions(oi: SessionRecipeInterface) -> SessionRecipeInterface: oi_create_new_session = oi.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], ): # AWS requires JWTs to contain an audience (aud) claim # The value for this claim should be the same as the # value you set when creating the authorizer if access_token_payload is None: access_token_payload = {} access_token_payload["aud"] = "jwtAuthorizers" return await oi_create_new_session(user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) oi.create_new_session = create_new_session return oi recipe_list = [ session.init( override=session.InputOverrideConfig( functions=override_session_functions, ), expose_access_token_to_frontend_in_cookie_based_auth=True, ), ] ``` ### 2. Configure your authorizer - Go to the "Authorizers" tab in the API Gateway configuration and select the "Manage authorizers" tab - Click "Create", in the creation screen select "JWT" as the "Authorizer type" - Enter a name for your authorizer (You can enter any name for this field) - Use `$request.header.Authorization` for the "Identity source". This means that API requests will contain the JWT as a Bearer token under the request header "Authorization". - Use the exact normalized issuer emitted by SuperTokens for this configuration: `/dev/auth`. This is `apiDomain + apiGatewayPath + apiBasePath`, with one slash at each boundary. - Set a value for the "Audience" field, this will be the value you expect the JWT to have under the `aud` claim. In the backend config above the value is set to `"jwtAuthorizers"` ### 3. Add the authorizer to your API - In the "Authorization" section select the "Attach authorizers to routes" tab - Click on the route you want to add the authorizer to and select the authorizer you created from the dropdown - Click "Attach authorizer" - Deploy your changes and test your API ### 4. Send the access token as a bearer token Exposing the access token does not automatically copy it to the JWT authorizer's identity source in cookie-based auth. Set the header explicitly on requests to protected routes: ```javascript import Session from "supertokens-web-js/recipe/session"; async function fetchProtectedUser() { const accessToken = await Session.getAccessToken(); if (accessToken === undefined) { throw new Error("No session access token is available"); } return fetch("/dev/user", { headers: { Authorization: `Bearer ${accessToken}`, }, }); } ``` Keep the SuperTokens frontend SDK's network interception enabled so session refresh continues to work. Test the expired-token retry path against the deployed HTTP API. ### 5. Check authorization claims in the JWT Once the JWT authorizer successfully validates the JWT, the claims of the JWT will be available to your lambda functions via `$event.requestContext.authorizer.jwt.claims`. You should check for the right authorization access here. For example, if one of your lambda functions requires that the user's email is verified, then it should check for the `jwt` payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. Similar checks need to be done to enforce the right user role or if 2FA is completed or not. This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in this flow, you will have to check them on your own. --- # GraphQL Source: https://supertokens.com/docs/integrations/graphql ## Before you start These instructions only show you how to perform **session verification** in a GraphQL context. You will first have to go through the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk) to configure **SuperTokens** and then return to this page. :::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: ## Using the GraphQL context We want to use the `Session.getSession` function in the `context` function to verify the session, and add the `userId` into our context so that our resolvers can read it. If the user id not logged in, we will set the `userId` to `undefined` in the context ```tsx import { ApolloServer } from "@apollo/server"; import express from "express"; import { expressMiddleware } from "@apollo/server/express4"; import { GraphQLError } from "graphql"; import Session from "supertokens-node/recipe/session"; let app = express(); const typeDefs = "..."; const resolvers = { /* ... */ }; const server = new ApolloServer({ typeDefs, resolvers, }); server.start().then(() => { app.use( express.json(), expressMiddleware(server, { // Note: This example uses the `req` and `res` argument to access headers, // but the arguments received by `context` vary by integration. // This means they vary for Express, Fastify, Lambda, etc. context: async ({ req, res }) => { try { let session = await Session.getSession(req, res, { sessionRequired: false, }); return { userId: session !== undefined ? session.getUserId() : undefined, }; } catch (err) { if (Session.Error.isErrorFromSuperTokens(err)) { throw new GraphQLError("Session related error", { extensions: { code: "UNAUTHENTICATED", http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, }, }); } throw err; } }, }), ); app.listen(3001, () => { console.log("Server started"); }); }); ``` In the above code snippet, we first attempt to verify the session using the `Session.getSession` function. If the session is valid, we will add the `userId` to the context. If the access token has expired, we will throw an error with a status code of `401`. If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. The `401` status code will cause the session refresh flow to start, which will give a new access token to the user, or else if the session was revoked, the user will be logged out. In case the user is not logged in, the `Session.getSession` function will throw return `undefined`, in which case, your resolvers will not have a `userId` in the context. The downside of this method is that if you want to mutate the session's access token payload in one of your resolvers, then you don't have access to the `session` object in there. This is where the method below comes into the picture: ## Using the GraphQL resolver Unlike the method above, we will be doing session verification on a per resolver basis here. This means that you will have access to the `session` object in your resolver using which you can update the information in the session (like its access token payload). We start by creating a helper function (a sort of middleware for your resolver) which you will have to call in all of your resolvers that require a session: ```tsx import Session, { SessionContainer } from "supertokens-node/recipe/session"; import { GraphQLError } from "graphql"; async function withSession(contextValue: any, resolver: (session: SessionContainer) => Promise) { try { let session = await Session.getSession(contextValue.req, contextValue.res); return await resolver(session); } catch (err) { if (Session.Error.isErrorFromSuperTokens(err)) { throw new GraphQLError("Session related error", { extensions: { code: "UNAUTHENTICATED", http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, }, }); } } } ``` In the above function, we attempt to verify the session using `Session.getSession`. If the session is valid, we will call the `resolver` function with the `session` object. If the access token has expired, or if the session does not exist, we will throw an error with a status code of `401`. If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. For this resolver to work, we will have to add the `req` and `res` object into the GraphQL context. This can be done as follows: ```tsx import { ApolloServer } from "@apollo/server"; import express from "express"; import { expressMiddleware } from "@apollo/server/express4"; import { GraphQLError } from "graphql"; let app = express(); const typeDefs = "..."; const resolvers = { /* ... */ }; const server = new ApolloServer({ typeDefs, resolvers, }); server.start().then(() => { app.use( express.json(), expressMiddleware(server, { // Note: This example uses the `req` and `res` argument to access headers, // but the arguments received by `context` vary by integration. // This means they vary for Express, Fastify, Lambda, etc. context: async ({ req, res }) => { return { req, res, }; }, }), ); app.listen(3001, () => { console.log("Server started"); }); }); ``` Finally, we can use our `withSession` in our resolvers as shown below: ```tsx check=false reason="Requires surrounding application context" import { ApolloServer } from "@apollo/server"; const server = new ApolloServer({ typeDefs, resolvers: { Query: { userProfile: async (_: any, __: any, contextValue) => { // starts of your resolver code.. return await withSession(contextValue, async (session) => { // getUserName is a custom application function... let name = await getUserName(session.getUserId()); return { userId: session.getUserId(), sessionHandle: session.getHandle(), name, }; }); }, }, }, }); ``` --- # Hasura Source: https://supertokens.com/docs/integrations/hasura ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/quickstart). Using SuperTokens with Hasura requires you to host your own API layer that uses our Backend SDK. If you do not want to host your own server you can use a serverless environment to achieve this. :::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: ## Steps ### 1. Expose the access token to the frontend For cookie based auth, the access token is not available on the frontend by default. In order to expose it, you need to set the `exposeAccessTokenToFrontendInCookieBasedAuth` config to `true`. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "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{ ExposeAccessTokenToFrontendInCookieBasedAuth: true, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( expose_access_token_to_frontend_in_cookie_based_auth=True, ) ] ) ``` ### 2. Add custom claims to the JWT Hasura requires claims to be set in a specific way, read the [official documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html) to know more. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, override: { functions: function (originalImplementation) { return { ...originalImplementation, createNewSession: async function (input) { input.accessTokenPayload = { ...input.accessTokenPayload, "https://hasura.io/jwt/claims": { "x-hasura-user-id": input.userId, "x-hasura-default-role": "user", "x-hasura-allowed-roles": ["user"], }, }; return originalImplementation.createNewSession(input); }, }; }, }, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "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{ ExposeAccessTokenToFrontendInCookieBasedAuth: true, Override: &sessmodels.OverrideStruct{ Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface { originalCreateNewSession := *originalImplementation.CreateNewSession (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload map[string]interface{}, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } hasuraClaims := map[string]interface{}{ "x-hasura-user-id": userID, "x-hasura-default-role": "user", "x-hasura-allowed-roles": []string{"user"}, } accessTokenPayload["https://hasura.io/jwt/claims"] = hasuraClaims return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.recipe.session.interfaces import RecipeInterface from typing import Dict, Optional, Any from supertokens_python.types import RecipeUserId def override_functions(original_implementation: RecipeInterface): original_implementation_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 access_token_payload is None: access_token_payload = {} access_token_payload["https://hasura.io/jwt/claims"] = { "x-hasura-user-id": user_id, "x-hasura-default-role": "user", "x-hasura-allowed-roles": ["user"], } return await original_implementation_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( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ session.init( override=session.InputOverrideConfig(functions=override_functions), expose_access_token_to_frontend_in_cookie_based_auth=True, ) ], ) ``` ### 3. Configure Hasura environment variables :::info Read the [official documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html#configuring-jwt-mode) to know about setting the JWT secret environment variable on Hasura ::: To use JWT based authentication, Hasura requires setting environment variables when configuring your app. With SuperTokens this can be done in 2 ways: #### Using the JWKS endpoint When configuring Hasura, you can set the `jwk_url` property. ```json { "jwk_url": "//auth/jwt/jwks.json" } ``` You can get the JWKS URL for your backend by using the method explained [here](/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library) #### Using a key string Hasura let's you provide a PEM string in the configuration. Refer to [this page](/additional-verification/session-verification/protect-api-routes#with-the-public-key-string) to learn how to get a public key as a string. You can then use that key string in the Hasura config: ```json { "type": "RS256", "key": "CERTIFICATE_STRING" } ``` ### 4. Check for claim values in Hasura Some checks like if the email is verified, or if 2FA is completed are stored as claim values in the JWT. You should check for the values of these claims in your GraphQL functions wherever required. For example, if one of your GraphQL functions requires that the user's email is verified, then it should check for the JWT payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. You can also use a [custom Hasura authorizer webhook](https://hasura.io/docs/latest/auth/authentication/webhook/) to check for the values of these claims depending on your app's requirements. This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in the Hasura flow, you will have to check them on your own. ### 5. Make requests to Hasura #### 5.1 Get the JWT on the frontend ```tsx import Session from "supertokens-web-js/recipe/session"; async function getToken(): Promise { const accessToken = await Session.getAccessToken(); console.log(accessToken); } ``` ```tsx check=false reason="Requires SDK globals from surrounding application" async function getToken(): Promise { const accessToken = await supertokensSession.getAccessToken(); console.log(accessToken); } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function getToken(): Promise { const accessToken = await SuperTokens.getAccessToken(); console.log(accessToken); } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun getToken(): String { return SuperTokens.getAccessToken(applicationContext) } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func getToken() -> String? { return SuperTokens.getAccessToken() } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future getToken() async { return await SuperTokens.getAccessToken(); } ``` #### 5.2 Make an HTTP requests ```tsx import axios from "axios"; async function makeRequest() { let url = "..."; let jwt = "..."; // Refer to step 5.a let response = await axios.get(url, { headers: { Authorization: `Bearer ${jwt}`, }, }); } ``` ## Local development If you are using Hasura cloud and testing your backend APIs in your local environment, JWT verification will fail because Hasura will not be able to query the JWKS endpoint (because the cloud can not query your local environment i.e localhost, 127.0.0.1). To solve this problem you will need to expose your locally hosted backend APIs to the internet. For example you can use [ngrok](https://ngrok.com/). After that, you need to configure Hasura to use the `//auth/jwt/jwks.json` as the JWKS endpoint (explained in [step 3](#3-configure-hasura-environment-variables)). --- # NestJS Source: https://supertokens.com/docs/integrations/nestjs ## Overview Integrating SuperTokens into a NestJS backend differs in some aspects from the main quickstart guide. That's because of the additional framework specific entities that are involved. To aid the process you can use the `supertokens-nestjs` package which exposes abstractions that speed up the setup. ## Before you start This guide assumes that you have already completed the [main quickstart guide](/quickstart). If not, please go through it before continuing with this page. You need to first understand how to configure the required recipes and run a sample project. You can also explore the [example projects](https://github.com/supertokens/supertokens-nestjs/tree/main/examples) for complete code references on how to use the libraries. ## Steps ### 1. Install the required packages ```bash title="npm" npm i -s supertokens-node supertokens-nestjs ``` ```bash title="Yarn" yarn add supertokens-node supertokens-nestjs ``` ```bash title="pnpm" pnpm add supertokens-node supertokens-nestjs ``` ```bash title="Bun" bun add supertokens-node supertokens-nestjs ``` ### 2. Initialize the `SuperTokensModule` Inside your main application module, initialize the **SuperTokensModule** with your required configuration. ```tsx import { Module } from "@nestjs/common"; import { SuperTokensModule } from "supertokens-nestjs"; @Module({ imports: [ SuperTokensModule.forRoot({ // Choose between 'express' and 'fastify' // If you are using fastify make sure to also set the fastifyAdapter property framework: "express", supertokens: { connectionURI: "...", }, appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, recipeList: [ /* ... */ ], }), ], controllers: [ /* ... */ ], providers: [ /* ... */ ], }) export class AppModule {} ``` :::info[Tip] You can use the `SuperTokensModule.forRootAsync` if you want to load the configuration asynchronously. ::: ### 3. Update the `bootstrap` function Inside your `bootstrap` function, you have to update the CORS configuration and set the exception filter. **SuperTokens** generates a set of CORS headers that the authentication flow requires. And, the global filter ensures that all authentication related errors get handled by the SDK. ```tsx check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { SuperTokensExceptionFilter } from "supertokens-nestjs"; import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; import { appInfo } from "./config"; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: [appInfo.websiteDomain], allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }); app.useGlobalFilters(new SuperTokensExceptionFilter()); await app.listen(3001); } ``` ### 4. Add the `SuperTokensAuthGuard` The `SuperTokensAuthGuard` automatically marks the routes that it targets as protected. By default session validation gets performed based on the default configuration provided in the `Session.init` call. You can customize the validation logic with decorators. More on that in the next step. #### As a global guard This applies the `SuperTokensAuthGuard` to all routes in exposed by controllers registered in that module. ```tsx import { Module } from "@nestjs/common"; import { APP_GUARD } from "@nestjs/core"; import { SuperTokensAuthGuard } from "supertokens-nestjs"; @Module({ imports: [ /* ... */ ], controllers: [ /* ... */ ], providers: [ { provide: APP_GUARD, useClass: SuperTokensAuthGuard, }, ], }) export class AppModule {} ``` #### As a controller guard This applies the `SuperTokensAuthGuard` only to the routes defined in the controller. ```tsx import { Controller, UseGuards } from "@nestjs/common"; import { SuperTokensAuthGuard } from "supertokens-nestjs"; @Controller() @UseGuards(SuperTokensAuthGuard) export class AppController {} ``` ### 5. Manage authentication with decorators The `supertokens-nestjs` package exposes two sets of decorators: - Function decorators like `VerifySession` and `PublicAccess` that you can use on controller methods to customize the session validation logic. - Parameter decorators like `Session` that you can use to access the session data in your controller methods. ```tsx import { Controller, Delete, Get, Patch, Post } from "@nestjs/common"; import { PublicAccess, Session, VerifySession } from "supertokens-nestjs"; import { SessionContainer } from "supertokens-node/recipe/session"; @Controller() class AppController { @Get("/user") @VerifySession() async getUserInfo(@Session("userId") userId: string) {} @Get("/user/:userId") @VerifySession({ roles: ["admin"], }) async deleteUser(@Session() session: SessionContainer) {} @Get("/user/profile") @PublicAccess() async getUserProfile() {} } ``` :::info[tip] With the `VerifySession` decorator, you can specify the following options: | Option | Type | Description | |--------|------|-------------| | `roles` | `string[]` | Roles that the user must have to access the route | | `permissions` | `string[]` | Permissions that the user must have to access the route | | `requiresMfa` | `boolean` | Indicates whether the user must have MFA enabled to access the route | | `requireEmailVerification` | `boolean` | Indicates whether the user must have their email verified to access the route | | `options` | `VerifySessionOptions` | The value that normally passed to the `getSession` or `verifySession` functions. Use it if you want additional levels of customization. | ::: ### 6. Configure SuperTokens core You need to setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: - [Managed service](/quickstart#3-configure-the-core-service) - [Self hosted](/deployment/self-host-supertokens) :::success[You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section.] ::: --- # Netlify Source: https://supertokens.com/docs/integrations/netlify ## Overview The following guide gets you though how to add SuperTokens to a Netlify serverless API. You can also check out the [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-netlify) for a full working example. ## Before you start This guide assumes that you are using Netlify for hosting your serverless API functions. If this is not the case, and you are only hosting your frontend using Netlify, please follow the [Quick setup guide](/quickstart#1-integrate-the-frontend-sdk) instead. ## Steps ### 1. Setup the frontend Follow the [initial quickstart guide](/quickstart#1-integrate-the-frontend-sdk) to configure the frontend. ### 2. Setup the backend #### 2.1 Install the SuperTokens node package ```bash title="npm" npm i supertokens-node ``` ```bash title="Yarn" yarn add supertokens-node ``` ```bash title="pnpm" pnpm add supertokens-node ``` ```bash title="Bun" bun add supertokens-node ``` #### 2.2 Create a configuration file Create a `config` folder in the root directory of your project. Create a `supertokensConfig.js` inside the `config` folder. An example of this file can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/config/supertokensConfig.js). #### 2.3 Create a backend configuration function ```tsx title="/config/supertokensConfig.ts" import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; function getBackendConfig() { return { framework: "awsLambda", supertokens: { connectionURI: "", apiKey: "", }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [EmailPassword.init(), Session.init()], isInServerlessEnv: true, }; } module.exports.getBackendConfig = getBackendConfig; ``` ### 3. Expose the authentication APIs We will add all the backend APIs for auth on `/.netlify/functions/auth/*`. This can be changed by setting the `apiBasePath` property in the `appInfo` object on the backend and frontend. For the rest of this page, we will assume you are using `/.netlify/functions/auth/*`. #### 3.1 Create the `netlify/functions/auth.js` page Be sure to create the `netlify/functions/` folder. An example of this can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/auth.js). ```tsx title="netlify/functions/auth.ts" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/awsLambda"; import middy from "@middy/core"; import cors from "@middy/http-cors"; import { getBackendConfig } from "../../config/supertokensConfig"; supertokens.init(getBackendConfig()); module.exports.handler = middy( middleware(async (event, context) => { if (event.httpMethod === "OPTIONS") { return { statusCode: 200, body: "", }; } return { statusCode: 404, body: "Not Found", }; }), ) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }), ) .onError((request) => { throw request.error; }); ``` :::note[- Notice that we called `supertokens.init` above. We will need to call this in all API endpoints that use any functions related to SuperTokens.] - `CORS` is only needed if you are hosting your frontend using a separate domain (if your website domain is different that your API's domain). ::: #### 3.2 Use the login widget If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) ### 4. Add session verification :::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. ::: For this guide, we will assume that we want an API `/.netlify/functions/user GET` which returns the current session information. #### 4.1 Create a new file `netlify/functions/user.js` An example of this is [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/user.js). #### 4.2 Call the `supertokens.init` function Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. ```tsx title="netlify/functions/user.ts" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { getBackendConfig } from "../../config/supertokensConfig"; supertokens.init(getBackendConfig()); ``` #### 4.3 Use session verification with your API handler We use the `verifySession()` middleware to verify a session. ```tsx title="netlify/functions/user.ts" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import middy from "@middy/core"; import cors from "@middy/http-cors"; import { getBackendConfig } from "../../config/supertokensConfig"; supertokens.init(getBackendConfig()); const handler = async (event: SessionEvent) => { return { body: JSON.stringify({ sessionHandle: event.session!.getHandle(), userId: event.session!.getUserId(), accessTokenPayload: event.session!.getAccessTokenPayload(), }), }; }; module.exports.handler = middy(verifySession(handler)) .use( cors({ origin: getBackendConfig().appInfo.websiteDomain, credentials: true, headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), methods: "OPTIONS,POST,GET,PUT,DELETE", }), ) .onError((request) => { throw request.error; }); ``` --- # About Source: https://supertokens.com/docs/integrations/nextjs/app-directory/about Integrating SuperTokens with a Next.js app involves: - Calling the frontend and backend init functions - Adding a website page to display the auth related widgets (on `/auth` by default) - Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) - Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page - Performing session verification: - In your APIs - In your frontend routes ## Try an example app Download and run an example Next.js app quickly using the following command: ```bash npx create-supertokens-app@latest --frontend=next --recipe=emailpassword ``` ## Follow our video guide [Watch on YouTube](https://www.youtube.com/watch?v=CVxR-OHorUM) Integrating SuperTokens with a Next.js app involves: - Calling the frontend and backend init functions - Building the various auth flows as per the [custom UI setup guide](/quickstart#1-integrate-the-frontend-sdk). - Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) - Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page - Performing session verification: - In your APIs - In your frontend routes ## Try an example app Download and run an example Next.js app quickly using the following command: ```bash npx create-supertokens-app@latest --frontend=next --recipe=emailpassword ``` :::note[This example app uses our pre-built UI] ::: --- # 1. Configuration Source: https://supertokens.com/docs/integrations/nextjs/app-directory/init ## 1. Install `supertokens` package ```bash title="npm" npm install supertokens-node supertokens-auth-react supertokens-web-js ``` ```bash title="Yarn" yarn add supertokens-node supertokens-auth-react supertokens-web-js ``` ```bash title="pnpm" pnpm add supertokens-node supertokens-auth-react supertokens-web-js ``` ```bash title="Bun" bun add supertokens-node supertokens-auth-react supertokens-web-js ``` ## 2. Create configuration files - Create a `config` folder in the app directory of your project. - Create an `appInfo.ts` inside the `config` folder. - Create a `backend.ts` inside the `config` folder. - Create a `frontend.ts` inside the `config` folder. ## 3. Create the `appInfo` configuration. ```tsx title="app/config/appInfo.ts" export const appInfo = { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }; ``` ## 1. Install `supertokens` package ```bash title="npm" npm install supertokens-node supertokens-web-js ``` ```bash title="Yarn" yarn add supertokens-node supertokens-web-js ``` ```bash title="pnpm" pnpm add supertokens-node supertokens-web-js ``` ```bash title="Bun" bun add supertokens-node supertokens-web-js ``` ## 2. Create configuration files - Create a `config` folder in the app directory of your project - Create an `appInfo.ts` inside the `config` folder. - Create a `backend.ts` inside the `config` folder. - Create a `frontend.ts` inside the `config` folder. ## 3. Create the `appInfo` configuration. ```tsx title="app/config/appInfo.ts" export const appInfo = { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", websiteDomain: "", apiDomain: "", apiBasePath: "/auth", }; ``` ## 4. Create a frontend config function ```tsx title="app/config/frontend.tsx" check=false reason="Requires surrounding framework application context" import EmailPasswordReact from "supertokens-auth-react/recipe/emailpassword"; import SessionReact from "supertokens-auth-react/recipe/session"; import { appInfo } from "./appInfo"; import { useRouter } from "next/navigation"; import type { SuperTokensConfig } from "supertokens-auth-react/lib/build/types"; const routerInfo: { router?: ReturnType; pathName?: string } = {}; export function setRouter(router: ReturnType, pathName: string) { routerInfo.router = router; routerInfo.pathName = pathName; } export const frontendConfig = (): SuperTokensConfig => { return { appInfo, recipeList: [EmailPasswordReact.init(), SessionReact.init()], windowHandler: (original) => ({ ...original, location: { ...original.location, getPathName: () => routerInfo.pathName!, assign: (url) => routerInfo.router!.push(url.toString()), setHref: (url) => routerInfo.router!.push(url.toString()), }, }), }; }; ``` ## 4. Create a frontend config function ```tsx title="app/config/frontend.tsx" check=false reason="Requires surrounding framework application context" import EmailPasswordWebJs from "supertokens-web-js/recipe/emailpassword"; import SessionWebJs from "supertokens-web-js/recipe/session"; import { appInfo } from "./appInfo"; import type { SuperTokensConfig } from "supertokens-web-js/types"; export const frontendConfig = (): SuperTokensConfig => { return { appInfo, recipeList: [EmailPasswordWebJs.init(), SessionWebJs.init()], }; }; ``` ## 5. Create a backend config function ```tsx title="app/config/backend.ts" check=false reason="Requires surrounding framework application context" import SuperTokens from "supertokens-node"; import EmailPasswordNode from "supertokens-node/recipe/emailpassword"; import SessionNode from "supertokens-node/recipe/session"; import { appInfo } from "./appInfo"; import type { TypeInput } from "supertokens-node/types"; export const backendConfig = (): TypeInput => { return { framework: "custom", supertokens: { connectionURI: "", apiKey: "", }, appInfo, recipeList: [EmailPasswordNode.init(), SessionNode.init()], isInServerlessEnv: true, }; }; let initialized = false; export function ensureSuperTokensInit() { if (!initialized) { SuperTokens.init(backendConfig()); initialized = true; } } ``` `ensureSuperTokensInit` initializes SuperTokens once before an API route uses the backend SDK. ## 6. Call the frontend `init` functions and wrap with `` component - Create a client component `/app/components/supertokensProvider.tsx`. This file will initialise SuperTokens and wrap its children with the `SuperTokensWrapper` component - Modify the `/app/layout.tsx` file to use the `SuperTokensProvider` component. You can learn more about this file [here](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). - An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/layout.tsx) ```tsx title="/app/components/supertokensProvider.tsx" check=false reason="Requires surrounding framework application context" "use client"; import type { ReactNode } from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import SuperTokensReact from "supertokens-auth-react"; import { frontendConfig, setRouter } from "../config/frontend"; import { usePathname, useRouter } from "next/navigation"; if (typeof window !== "undefined") { // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' SuperTokensReact.init(frontendConfig()); } interface SuperTokensProviderProps { children: ReactNode; } export function SuperTokensProvider({ children }: SuperTokensProviderProps) { setRouter(useRouter(), usePathname() || window.location.pathname); return {children}; } ``` ```tsx title="/app/layout.tsx" check=false reason="Requires surrounding framework application context" import "./globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { SuperTokensProvider } from "./components/supertokensProvider"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## 6. Call the frontend `init` functions and wrap with `` component - Create a client component `/app/components/supertokensInit.tsx`. This file will initialise SuperTokens. - Modify the `/app/layout.tsx` file to use the `SuperTokensInit` component. You can learn more about this file [here](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). ```tsx title="/app/components/supertokensInit.tsx" check=false reason="Requires surrounding framework application context" "use client"; import type { ReactNode } from "react"; import SuperTokensWebJs from "supertokens-web-js"; import { frontendConfig } from "../config/frontend"; if (typeof window !== "undefined") { // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' SuperTokensWebJs.init(frontendConfig()); } interface SuperTokensInitProps { children: ReactNode; } export function SuperTokensInit({ children }: SuperTokensInitProps) { return <>{children}; } ``` ```tsx title="/app/layout.tsx" check=false reason="Requires surrounding framework application context" import "./globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { SuperTokensInit } from "./components/supertokensInit"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` --- # 7. Next steps Source: https://supertokens.com/docs/integrations/nextjs/app-directory/next-steps ## Setting up the core and database You need to now setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: - [Managed service](/quickstart#3-configure-the-core-service) - [Self hosted](/deployment/self-host-supertokens) :::success[You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section.] ::: --- # Using Next.js Proxy Source: https://supertokens.com/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware This method is an alternative method for using sessions in an API. If you are already using [session guards](./session-verification-session-guard), you can skip this step. If you are checking OAuth2 access tokens use your OAuth2/OIDC library instead of the SuperTokens Session SDK. ## Setting up Proxy In Next.js 16, request interception uses `proxy.ts`. Earlier Next.js versions called this file `middleware.ts`. The Proxy checks for a session with `withSession` and forwards the user's ID to Route Handlers through an internal request header. You can forward other information in the same way. :::warning You cannot pass the full session container through Proxy because request headers can only contain strings. If you need the full session container in your APIs, use [session guards](./session-verification-session-guard). ::: ```tsx title="proxy.ts" check=false reason="Requires surrounding framework application context" import { withSession } from "supertokens-node/nextjs"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { ensureSuperTokensInit } from "./app/config/backend"; ensureSuperTokensInit(); export async function proxy(request: NextRequest) { const requestHeaders = new Headers(request.headers); if (requestHeaders.has("x-user-id")) { console.warn("The FE tried to pass x-user-id, which is only supposed to be a backend internal header. Ignoring."); requestHeaders.delete("x-user-id"); } if (request.nextUrl.pathname.startsWith("/api/auth")) { // SuperTokens exposes /api/auth/*, so do not run session verification for these routes. return NextResponse.next({ request: { headers: requestHeaders } }); } return withSession( request, async (err, session) => { if (err) { console.error("Session verification failed", { method: request.method, pathname: request.nextUrl.pathname, }); return new NextResponse("Internal server error", { status: 500 }); } if (session !== undefined) { requestHeaders.set("x-user-id", session.getUserId()); } return NextResponse.next({ request: { headers: requestHeaders } }); }, { sessionRequired: false }, ); } export const config = { matcher: "/api/:path*", }; ``` ## Fetching the user ID in your APIs Proxy runs for the API routes matched by `/api/:path*`. Route Handlers can read the information it forwards: ```tsx title="app/api/userid/route.ts" check=false reason="Requires surrounding framework application context" import { NextResponse, NextRequest } from "next/server"; import { ensureSuperTokensInit } from "../../config/backend"; ensureSuperTokensInit(); export function GET(request: NextRequest) { const userId = request.headers.get("x-user-id"); // Proxy only adds the user ID if a session exists if (userId === null) { return new NextResponse("Authentication required", { status: 401 }); } return NextResponse.json({ userId, }); } ``` This creates a `GET` request for the `/api/userid` route which returns the user ID of the currently signed-in user. --- # Adding a session guard to each API route Source: https://supertokens.com/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard :::warning[OAuth2 token verification] Verify OAuth2 access tokens with your OAuth2/OIDC library instead of the SuperTokens Session SDK. ::: :::note[This is applicable for when the frontend calls an API in the `/app/api` folder.] ::: For this guide, we will assume that we want an API `/api/user GET` which returns the current session information. Create a new file `/app/api/user/route.ts` - An example of this is [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/api/user/route.ts). ```ts title="app/api/user/route.ts" check=false reason="Requires surrounding framework application context" import { withSession } from "supertokens-node/nextjs"; import { NextResponse, NextRequest } from "next/server"; import { ensureSuperTokensInit } from "../../config/backend"; ensureSuperTokensInit(); export function GET(request: NextRequest) { return withSession(request, async (err, session) => { if (err) { return NextResponse.json(err, { status: 500 }); } if (!session) { return new NextResponse("Authentication required", { status: 401 }); } return NextResponse.json({ note: "Fetch any data from your application for authenticated user after using verifySession middleware", userId: session.getUserId(), sessionHandle: session.getHandle(), accessTokenPayload: session.getAccessTokenPayload(), }); }); } ``` In the above snippet we are creating a `GET` handler for the `/api/user` route. We call the `withSession` helper function. The function will pass the session object in the callback which we then use to read user information. If a session does not exist `undefined` will be passed instead. The `withSession` guard will return: - Status `401` if the session does not exist or has expired - Status `403` if the session claims fail their validation. For example if email verification is required but the user's email is not verified. --- # 4. Checking for sessions in frontend routes Source: https://supertokens.com/docs/integrations/nextjs/app-directory/protecting-route :::warning[OAuth2 token verification] Check authentication with your OAuth2/OIDC library when using Unified Login. ::: Protecting a website route means that it cannot be accessed unless a user is signed in. If a non signed in user tries to access it, they will be redirected to the login page. ## Sessions with Client Components Lets create a client component for the `/` route of our website. ### Using the `SessionAuth` wrapper component ```tsx title="app/components/homeClientComponent.tsx" "use client"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; export const HomeClientComponent = () => { return (
Hello world
); }; ``` `SessionAuth` is a component exposed by the SuperTokens React SDK, it checks if a session exists and if it does not exist it will redirect the user to the login page. It also does session claim checking on the frontend and take appropriate action if the claim validators fail. For example, if you have set the email verification recipe to be `"REQUIRED"`, and the user's email is not verified, this component will redirect the user to the email verification page. :::warning[At the moment the `SessionAuth` component does not support server side rendering and will only work on the client side. On the server side, this component renders an empty screen.] Refer to the next section of this page to learn how to use sessions on the server side. ::: ### Using `useSessionContext` ```tsx title="app/components/homeClientComponent.tsx" "use client"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; export const HomeClientComponent = () => { const session = useSessionContext(); if (session.loading) { return
Loading...
; } if (session.doesSessionExist === false) { return
Session does not exist
; } return (

Client side component got userId: {session.userId}

); }; ``` `useSessionContext` lets you access the session information on the client side using the React Context API. `session.loading` indicates if the session is currently being loaded into the context, this will also refresh the session for you if it is expired. You can use `session.doesSessionExist` to check if a valid session exists and handle it accordingly. :::info[`useSessionContext` does not need to be used along with `SessionAuth`. Since our app is wrapped by the `SuperTokensWrapper` component, the `useSessionContext` hook can be used in any of our components.] ::: :::tip[Test by navigating to `/`] You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. ::: ## Sessions with Server Components ### Creating some helper Components #### Creating a wrapper around `SessionAuth` Let's say we want to protect the home page of your website (`/` route). First we will create a wrapper around the `SessionAuth` component to add the `"use client"` directive on top. ```tsx title="app/components/sessionAuthForNextJS.tsx" "use client"; import React, { useState, useEffect } from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; type Props = Parameters[0] & { children?: React.ReactNode | undefined; }; export const SessionAuthForNextJS = (props: Props) => { const [loaded, setLoaded] = useState(false); useEffect(() => { setLoaded(true); }, []); if (!loaded) { return props.children; } return {props.children}; }; ``` This is a client component that renders just its children on the server side and renders the children wrapped with `SessionAuth` on the client side. This way, the server side returns the page content, and on the client, the same page content is wrapper with `SessionAuth` which will handle session related events on the frontend - for example, if the user's session expires whilst they are on this page, then `SessionAuth` will auto redirect them to the login page. #### Creating the `TryRefreshComponent` This component will refresh the user's session if their current session has expired. ```tsx title="app/components/tryRefreshClientComponent.tsx" "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import Session from "supertokens-auth-react/recipe/session"; import SuperTokens from "supertokens-auth-react"; export const TryRefreshComponent = () => { const router = useRouter(); const [didError, setDidError] = useState(false); useEffect(() => { /** * `attemptRefreshingSession` will call the refresh token endpoint to try and * refresh the session. This will throw an error if the session cannot be refreshed. */ void Session.attemptRefreshingSession() .then((hasSession) => { /** * If the user has a valid session, we reload the page to restart the flow * with valid session tokens */ if (hasSession) { router.refresh(); } else { SuperTokens.redirectToAuth(); } }) .catch(() => { setDidError(true); }); }, [router]); /** * We add this check to make sure we handle the case where the refresh API fails with * an unexpected error */ if (didError) { return
Something went wrong, please reload the page
; } return
Loading...
; }; ``` ### Using `SessionAuthForNextJS` and checking for sessions We then create a server component that can check if the session exists and return any session information we may need: ```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { getSSRSession } from "supertokens-node/nextjs"; import { TryRefreshComponent } from "./tryRefreshClientComponent"; import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; import { ensureSuperTokensInit } from "../config/backend"; ensureSuperTokensInit(); export async function HomePage() { const cookieStore = await cookies(); const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); if (error) { console.error("Unable to read the SSR session", { component: "HomePage" }); return
Unable to verify your session. Please try again.
; } // `accessTokenPayload` will be undefined if it the session does not exist or has expired if (accessTokenPayload === undefined) { if (!hasToken) { /** * This means that the user is not logged in. If you want to display some other UI in this * case, you can do so here. */ return redirect("/auth"); } /** * This means that the session does not exist but we have session tokens for the user. In this case * the `TryRefreshComponent` will try to refresh the session. * * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 */ return ; } /** * SessionAuthForNextJS will handle proper redirection for the user based on the different session states. * It will redirect to the login page if the session does not exist etc. */ return (
Your user id is: {accessTokenPayload.sub}
); } ``` The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired. And then we can modify the `/app/page.tsx` file to use our server component ```tsx title="app/page.tsx" check=false reason="Requires surrounding framework application context" import styles from "./page.module.css"; import { HomePage } from "./components/home"; export default function Home() { return (
); } ``` :::tip[Test by navigating to `/`] You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. ::: :::note An example of this can be seen [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/page.tsx). :::
## Sessions with Client Components Checking for sessions in client components involves: - Using the `Session` recipe to manually check if a session exists, rendering some default UI while you check. - Render your UI if a session exists. To learn more about how to do this refer to [this page](/additional-verification/session-verification/protect-frontend-routes). ## Sessions with Server Components ### Creating a helper component for session refreshing Lets start by creating a component that will refresh the session if it exists and has expired. ```tsx title="app/components/tryRefreshClientComponent.tsx" "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import Session from "supertokens-web-js/recipe/session"; export const TryRefreshComponent = () => { const router = useRouter(); const [didError, setDidError] = useState(false); useEffect(() => { void Session.attemptRefreshingSession() .then((hasSession) => { if (hasSession) { router.refresh(); } else { /** * This means that the session is expired and cannot be refreshed. * In this example we redirect the user back to the login page. */ router.replace("/auth"); } }) .catch(() => { setDidError(true); }); }, [router]); if (didError) { return
Something went wrong, please reload the page
; } return
Loading...
; }; ``` `Session.attemptRefreshingSession` will call the refresh endpoint. `hasSession` will be: - `true` if the session was refreshed - `false` if the session could not be refreshed ### Modify home page to check for sessions Lets modify the Home page server component we created earlier: ```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { getSSRSession } from "supertokens-node/nextjs"; import { TryRefreshComponent } from "./tryRefreshClientComponent"; import { ensureSuperTokensInit } from "../config/backend"; ensureSuperTokensInit(); export async function HomePage() { const cookieStore = await cookies(); const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); if (error) { console.error("Unable to read the SSR session", { component: "HomePage" }); return
Unable to verify your session. Please try again.
; } // `accessTokenPayload` will be undefined if it the session does not exist or has expired if (accessTokenPayload === undefined) { if (!hasToken) { /** * This means that the user is not logged in. If you want to display some other UI in this * case, you can do so here. */ return redirect("/auth"); } /** * This means that the session does not exist but we have session tokens for the user. In this case * the `TryRefreshComponent` will try to refresh the session. * * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 */ return ; } return
Your user id is: {accessTokenPayload.sub}
; } ``` The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired. And then we can modify the `/app/page.tsx` file to use our server component ```tsx title="app/page.tsx" check=false reason="Requires surrounding framework application context" import styles from "./page.module.css"; import { HomePage } from "./components/home"; export default function Home() { return (
); } ``` :::tip[Test by navigating to `/`] You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. For custom UI SuperTokens provides no login UI, the code above will redirect the user to the `/auth` route but you will have to build some UI that is served on that route. :::
--- # 6. Making requests from Server Components Source: https://supertokens.com/docs/integrations/nextjs/app-directory/server-components-requests Let's modify the Home page from the [route protection step](/integrations/nextjs/app-directory/protecting-route) to call this API. ```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { getSSRSession } from "supertokens-node/nextjs"; import { TryRefreshComponent } from "./tryRefreshClientComponent"; import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; import { appInfo } from "../config/appInfo"; import { ensureSuperTokensInit } from "../config/backend"; ensureSuperTokensInit(); async function getAccessToken() { const cookiesStore = await cookies(); return cookiesStore.get("sAccessToken")?.value; } export async function HomePage() { const cookieStore = await cookies(); const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); const accessToken = await getAccessToken(); if (error) { console.error("Unable to read the SSR session", { component: "HomePage" }); return
Unable to verify your session. Please try again.
; } // `accessTokenPayload` is undefined if the session does not exist or has expired if (accessTokenPayload === undefined) { if (!hasToken) { /** * This means that the user is not logged in. If you want to display some other UI in this * case, you can do so here. */ return redirect("/auth"); } /** * This means that the session does not exist but we have session tokens for the user. In this case * the `TryRefreshComponent` will try to refresh the session. * * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 */ return ; } const userInfoResponse = await fetch(new URL("/api/user", appInfo.websiteDomain), { headers: { /** * We read the access token from the cookies and use it as a Bearer token when * making network requests. */ Authorization: "Bearer " + accessToken, }, }); let message = ""; if (userInfoResponse.status === 200) { message = `Your user id is: ${accessTokenPayload.sub}`; } else if (userInfoResponse.status === 500) { message = "Something went wrong"; } else if (userInfoResponse.status === 401) { // The TryRefreshComponent will try to refresh the session // To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 return ; } else if (userInfoResponse.status === 403) { // SessionAuthForNextJS will redirect based on which claim is invalid return ; } // You can use `userInfoResponse` to read the user's session information return (
{message}
); } ``` We read the access token of the user from cookies. We can then send the access token as a header to the API. When the API calls `withSession` it will try to read the access token from the headers and if a session exists it will return the session information.
```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { getSSRSession } from "supertokens-node/nextjs"; import { TryRefreshComponent } from "./tryRefreshClientComponent"; import { appInfo } from "../config/appInfo"; import { ensureSuperTokensInit } from "../config/backend"; ensureSuperTokensInit(); async function getAccessToken() { const cookiesStore = await cookies(); return cookiesStore.get("sAccessToken")?.value; } export async function HomePage() { const cookieStore = await cookies(); const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); const accessToken = await getAccessToken(); if (error) { console.error("Unable to read the SSR session", { component: "HomePage" }); return
Unable to verify your session. Please try again.
; } // `accessTokenPayload` is undefined if the session does not exist or has expired if (accessTokenPayload === undefined) { if (!hasToken) { /** * This means that the user is not logged in. If you want to display some other UI in this * case, you can do so here. */ return redirect("/auth"); } /** * This means that the session does not exist but we have session tokens for the user. In this case * the `TryRefreshComponent` will try to refresh the session. * * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 */ return ; } const userInfoResponse = await fetch(new URL("/api/user", appInfo.websiteDomain), { headers: { /** * We read the access token from the cookies and use it as a Bearer token when * making network requests. */ Authorization: "Bearer " + accessToken, }, }); let message = ""; if (userInfoResponse.status === 200) { message = `Your user id is: ${accessTokenPayload.sub}`; } else if (userInfoResponse.status === 500) { message = "Something went wrong"; } else if (userInfoResponse.status === 401) { // The TryRefreshComponent will try to refresh the session // To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 return ; } else if (userInfoResponse.status === 403) { /** * This means that one of the session claims is invalid. You should redirect the user to * the appropriate page depending on which claim is invalid. */ return
Invalid Session Claims
; } // You can use `userInfoResponse` to read the user's session information return
{message}
; } ``` APIs that require sessions will return status: - `401` if there is no valid session or if the session has expired. In this case, we return the `TryRefreshComponent` component, which tries to refresh the session or redirects to the login page if the session can't be refreshed. - `403` if one or more session claims fail validation. In this case, check which session claim failed and redirect the user accordingly. For example, refer to [protecting routes with email verification](/additional-verification/email-verification/protecting-routes) to check the email verification claim.
--- # 3. Adding auth APIs Source: https://supertokens.com/docs/integrations/nextjs/app-directory/setting-up-backend We will add all the backend APIs for auth on `/api/auth`. This can be changed by setting the `apiBasePath` property in the `appInfo` object in the `appInfo.ts` file. For the rest of this page, we will assume you are using `/api/auth`. ## 1. Create the `app/api/auth/[[...path]]/route.ts` route - Be sure to create the `auth/[[...path]]` folder in the `app/api/` folder. - `route.ts` uses the `getAppDirRequestHandler` helper from `supertokens-node` to handle authentication APIs such as sign-up and sign-in. The full folder path should be `/app/api/auth/[[...path]]/route.ts`. - An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/api/auth/%5B...path%5D/route.ts). ## 2. Expose the SuperTokens APIs ```tsx title="app/api/auth/[[...path]]/route.ts" check=false reason="Requires surrounding framework application context" import { getAppDirRequestHandler } from "supertokens-node/nextjs"; import type { NextRequest } from "next/server"; import { ensureSuperTokensInit } from "../../../config/backend"; ensureSuperTokensInit(); const handleCall = getAppDirRequestHandler(); export async function GET(request: NextRequest) { return handleCall(request); } export async function POST(request: NextRequest) { return handleCall(request); } export async function DELETE(request: NextRequest) { return handleCall(request); } export async function PUT(request: NextRequest) { return handleCall(request); } export async function PATCH(request: NextRequest) { return handleCall(request); } export async function HEAD(request: NextRequest) { return handleCall(request); } ``` ## 3. Use the login widget If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) --- # 2. Showing the Login UI Source: https://supertokens.com/docs/integrations/nextjs/app-directory/setting-up-frontend ## 1. Create the `app/auth/[[...path]]/page.tsx` page - Be sure to create the `auth/[[...path]]` folder in the `app` folder. - `page.tsx` will contain the component for showing SuperTokens UI - An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/auth/%5B%5B...path%5D%5D/page.tsx). ## 2. Create the `Auth` component: ```tsx title="app/auth/[[...path]]/page.tsx" "use client"; import { useEffect, useState } from "react"; import { redirectToAuth } from "supertokens-auth-react"; import SuperTokens from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; export default function Auth() { // if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. const [loaded, setLoaded] = useState(false); useEffect(() => { if (SuperTokens.canHandleRoute([EmailPasswordPreBuiltUI]) === false) { redirectToAuth({ redirectBack: false }); } else { setLoaded(true); } }, []); if (loaded) { return SuperTokens.getRoutingComponent([EmailPasswordPreBuiltUI]); } return null; } ``` ## 3. Visit `/auth` page on your website If you see the login UI, you have completed this step. See [Customize the sign-in form](/authentication/email-password/customize-the-sign-in-form) to change its fields and appearance. If you cannot see the UI in your app, ask for help on [Discord](https://supertokens.com/discord). You need to build your own UI. See each [authentication method tutorial](/authentication/overview) for detailed instructions. --- # About Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/about Integrating SuperTokens with a Next.js app involves: - Calling the frontend and backend init functions - Adding a website page to display the auth related widgets (on `/auth` by default) - Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) - Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page - Performing session verification: - In your APIs - In `getServerSideProps` ## Try an example app Download and run an example Next.js app quickly using the following command: ```bash npx create-supertokens-app@latest --frontend=next --recipe=emailpassword ``` Integrating SuperTokens with a Next.js app involves: - Calling the frontend and backend init functions - Building the various auth flows as per the [custom UI setup guide](/quickstart#1-integrate-the-frontend-sdk). - Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) - Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page - Performing session verification: - In your APIs - In `getServerSideProps` ## Try an example app Download and run an example Next.js app quickly using the following command: ```bash npx create-supertokens-app@latest --frontend=next --recipe=emailpassword ``` :::note[This example app uses our pre-built UI] ::: --- # 1. Configuration Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/init ## 1. Install `supertokens` package ```bash title="npm" npm install supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors ``` ```bash title="Yarn" yarn add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors ``` ```bash title="pnpm" pnpm add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors ``` ```bash title="Bun" bun add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors ``` ## 2. Create configuration files - Create a `config` folder in the root directory of your project - Create an `appInfo.ts` inside the `config` folder. - Create a `backendConfig.ts` inside the `config` folder. - Create a `frontendConfig.ts` inside the `config` folder. ## 3. Create the `appInfo` configuration. ```tsx title="/config/appInfo.ts" export const appInfo = { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }; ``` ## 1. Install `supertokens` package ```bash title="npm" npm install supertokens-node supertokens-web-js nextjs-cors ``` ```bash title="Yarn" yarn add supertokens-node supertokens-web-js nextjs-cors ``` ```bash title="pnpm" pnpm add supertokens-node supertokens-web-js nextjs-cors ``` ```bash title="Bun" bun add supertokens-node supertokens-web-js nextjs-cors ``` ## 2. Create configuration files - Create a `config` folder in the root directory of your project - Create an `appInfo.ts` inside the `config` folder. - Create a `backendConfig.ts` inside the `config` folder. - Create a `frontendConfig.ts` inside the `config` folder. ## 3. Create the `appInfo` configuration. ```tsx title="/config/appInfo.ts" export const appInfo = { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", }; ``` ## 4. Create a frontend config function ```tsx title="/config/frontendConfig.ts" check=false reason="Requires surrounding framework application context" import EmailPasswordReact from "supertokens-auth-react/recipe/emailpassword"; import SessionReact from "supertokens-auth-react/recipe/session"; import { appInfo } from "./appInfo"; import Router from "next/router"; export const frontendConfig = () => { return { appInfo, recipeList: [EmailPasswordReact.init(), SessionReact.init()], windowHandler: (oI: any) => { return { ...oI, location: { ...oI.location, setHref: (href: string) => { Router.push(href); }, }, }; }, }; }; ``` ## 4. Create a frontend config function ```tsx title="/config/frontendConfig.ts" check=false reason="Requires surrounding framework application context" import EmailPasswordWebJs from "supertokens-web-js/recipe/emailpassword"; import SessionWebJs from "supertokens-web-js/recipe/session"; import { appInfo } from "./appInfo"; export const frontendConfig = () => { return { appInfo, recipeList: [EmailPasswordWebJs.init(), SessionWebJs.init()], }; }; ``` ## 5. Create a backend config function ```tsx title="/config/backendConfig.ts" check=false reason="Requires surrounding framework application context" import SuperTokens from "supertokens-node"; import EmailPasswordNode from "supertokens-node/recipe/emailpassword"; import SessionNode from "supertokens-node/recipe/session"; import { appInfo } from "./appInfo"; import type { TypeInput } from "supertokens-node/types"; export const backendConfig = (): TypeInput => { return { framework: "express", supertokens: { connectionURI: "", apiKey: "", }, appInfo, recipeList: [EmailPasswordNode.init(), SessionNode.init()], isInServerlessEnv: true, }; }; let initialized = false; export function ensureSuperTokensInit() { if (!initialized) { SuperTokens.init(backendConfig()); initialized = true; } } ``` ## 6. Call the frontend `init` functions and wrap with `` component - Create a `/pages/_app.tsx` file. Learn more in the [Next.js Custom App documentation](https://nextjs.org/docs/pages/building-your-application/routing/custom-app). ```tsx title="/pages/_app.tsx" check=false reason="Requires surrounding framework application context" import "../styles/globals.css"; import type { AppProps } from "next/app"; import SuperTokensReact, { SuperTokensWrapper } from "supertokens-auth-react"; import { frontendConfig } from "../config/frontendConfig"; if (typeof window !== "undefined") { SuperTokensReact.init(frontendConfig()); } function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` ## 6. Call the frontend `init` functions - Create a `/pages/_app.tsx` file. Learn more in the [Next.js Custom App documentation](https://nextjs.org/docs/pages/building-your-application/routing/custom-app). ```tsx title="/pages/_app.ts" check=false reason="Requires surrounding framework application context" import "../styles/globals.css"; import type { AppProps } from "next/app"; import SuperTokensWebJs from "supertokens-web-js"; import { frontendConfig } from "../config/frontendConfig"; if (typeof window !== "undefined") { SuperTokensWebJs.init(frontendConfig()); } function MyApp({ Component, pageProps }: AppProps) { return ; } export default MyApp; ``` --- # 6. Next steps Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/next-steps ## Setting up the core and database You need to now setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: - [Managed service](/quickstart#3-configure-the-core-service) - [Self hosted](/deployment/self-host-supertokens) :::success[You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section.] ::: --- # 5a. Session verification in an API call Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/protecting-backend/in-api :::warning[OAuth2 token verification] Verify OAuth2 access tokens with your OAuth2/OIDC library instead of the SuperTokens Session SDK. ::: :::note[This is applicable for when the frontend calls an API in the `/pages/api` folder.] ::: For this guide, we will assume that we want an API `/api/user GET` which returns the current session information. ## 1. Create a new file `/pages/api/user.ts` ## 2. Call the `supertokens.init` function Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. ```tsx title="pages/api/user.ts" check=false reason="Requires surrounding framework application context" import supertokens from "supertokens-node"; import { backendConfig } from "../../../config/backendConfig"; supertokens.init(backendConfig()); ``` ## 3. Call the `verifySession` session function ```tsx title="pages/api/user.ts" check=false reason="Requires surrounding framework application context" import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import supertokens from "supertokens-node"; import { backendConfig } from "../../../config/backendConfig"; import NextCors from "nextjs-cors"; supertokens.init(backendConfig()); export default async function user(req: any, res: any) { // NOTE: We need CORS only if we are querying the APIs from a different origin await NextCors(req, res, { methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], origin: "", credentials: true, allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], }); // we first verify the session await superTokensNextWrapper( async (next) => { return await verifySession()(req, res, next); }, req, res, ); // if it comes here, it means that the session verification was successful return res.json({ note: "Fetch any data from your application for authenticated user after using verifySession middleware", userId: req.session.getUserId(), sessionHandle: req.session.getHandle(), userDataInAccessToken: req.session.getAccessTokenPayload(), }); } ``` - If no session exists, the API will return a `401` error to the client. In this case, the code `return res.json` will not be executed at all. - In case the session does exist, `req.session` can be used to get session information. Learn more about this object [here](/additional-verification/session-verification/protect-api-routes#using-verify-session). --- # 5b. Session verification in getServerSideProps Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/protecting-backend/in-ssr :::warning[OAuth2 token verification] Verify OAuth2 access tokens with your OAuth2/OIDC library instead of the SuperTokens Session SDK. ::: :::note[This is applicable for when verifying a session in `getServerSideProps` or `getInitialProps`.] ::: For this guide, we will assume that we want to pass the logged in user's ID as a prop to a protected route. ## 1. Check the session in `getServerSideProps` ```tsx import type { GetServerSidePropsContext } from "next"; import { getSSRSession } from "supertokens-node/nextjs"; export function createGetServerSideProps(ensureSuperTokensInit: () => void) { return async function getServerSideProps(context: GetServerSidePropsContext) { ensureSuperTokensInit(); const cookies = Object.entries(context.req.cookies).flatMap(([name, value]) => value === undefined ? [] : [{ name, value }], ); const { accessTokenPayload, error } = await getSSRSession(cookies); if (error) { throw error; } if (accessTokenPayload === undefined) { // This occurs if the token has expired or doesn't exist. // Either way, sending this response prompts the frontend to attempt a session refresh. // // Case 1: Token doesn't exist // - The refresh will fail, and the user will be redirected to the login page. // // Case 2: Token has expired // - The client will call the refresh API and update the session tokens. return { props: { fromSupertokens: "needs-refresh" } }; // or return {fromSupertokens: 'needs-refresh'} in case of getInitialProps } return { props: { userId: accessTokenPayload.sub }, }; // or return { userId: accessTokenPayload.sub } in case of getInitialProps }; } ``` In your page module, import `ensureSuperTokensInit` from your application's backend configuration and export `getServerSideProps = createGetServerSideProps(ensureSuperTokensInit)`. This keeps session verification connected to the same SDK configuration as your authentication routes. :::warning[Use `getSSRSession` rather than `getSession` or `verifySession` here. The latter functions might update the session tokens, but server-side requests cannot propagate those updates through frontend request interceptors.] ::: ## 2. Doing manual refresh on the frontend - The following will refresh a session if needed, for all your website pages - This goes in the `/pages/_app.tsx` file ```tsx title="/pages/_app.tsx" import { useEffect, useState } from "react"; import Session from "supertokens-auth-react/recipe/session"; import { redirectToAuth } from "supertokens-auth-react"; import type { AppProps } from "next/app"; function MyApp({ Component, pageProps }: AppProps<{ fromSupertokens: string }>) { const [didError, setDidError] = useState(false); useEffect(() => { async function doRefresh() { try { if (await Session.attemptRefreshingSession()) { // post session refreshing, we reload the page. This will // send the new access token to the server, and then // getServerSideProps will succeed location.reload(); } else { // the user's session has expired. So we redirect // them to the login page await redirectToAuth(); } } catch { setDidError(true); } } if (pageProps.fromSupertokens === "needs-refresh") { void doRefresh(); } }, [pageProps.fromSupertokens]); if (didError) { return

Unable to refresh your session. Please reload the page.

; } if (pageProps.fromSupertokens === "needs-refresh") { // in case the frontend needs to refresh, we show nothing. // Alternatively, you can show a spinner. return null; } // the below is already there by default return ; } export default MyApp; ```
```tsx title="/pages/_app.tsx" import { useEffect, useState } from "react"; import Session from "supertokens-web-js/recipe/session"; import type { AppProps } from "next/app"; function MyApp({ Component, pageProps }: AppProps<{ fromSupertokens: string }>) { const [didError, setDidError] = useState(false); useEffect(() => { async function doRefresh() { try { if (await Session.attemptRefreshingSession()) { // post session refreshing, we reload the page. This will // send the new access token to the server, and then // getServerSideProps will succeed location.reload(); } else { // the user's session has expired. So we redirect // them to the login page // redirect to login page window.location.assign("/login"); } } catch { setDidError(true); } } if (pageProps.fromSupertokens === "needs-refresh") { void doRefresh(); } }, [pageProps.fromSupertokens]); if (didError) { return

Unable to refresh your session. Please reload the page.

; } if (pageProps.fromSupertokens === "needs-refresh") { // in case the frontend needs to refresh, we show nothing. // Alternatively, you can show a spinner. return null; } // the below is already there by default return ; } export default MyApp; ```
## 3. Consume the `userId` returned by getServerSideProps in your component On success, `getServerSideProps` returns ```tsx check=false reason="Requires surrounding application context" { props: { userId: accessTokenPayload.sub, }, } ``` Therefore, the associated page can access the `userId` like: ```tsx interface HomeProps { userId: string; } export default function Home({ userId }: HomeProps) { return

Your user ID is: {userId}

; } ``` --- # 4. Protecting a website route Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/protecting-route :::warning This information only applies when using **SuperTokens Session Access Tokens**. When implementing [Unified Login](/authentication/unified-login/introduction), check the authentication state using your OAuth2/OIDC library. ::: Protecting a website route means that it cannot be accessed unless a user is signed in. A signed-out user is redirected to the login page. Let's say we want to protect the home page of your website (`/` route). In this case, we can edit the `/pages/index.tsx` file to add an auth wrapper around your `Home` component like so: ```tsx title="pages/index.tsx" check=false reason="Requires surrounding framework application context" import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import ProtectedPage from "./protectedPage"; export default function Home() { return ( // we protect ProtectedPage by wrapping it with SessionAuth ); } ``` :::tip[Test by navigating to `/`] You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. ::: Protecting a website route means that it cannot be accessed unless a user is signed in. A signed-out user is redirected to the login page. You can do this with the `doesSessionExist` function. This example assumes that your custom login page is at `/login`; change the path if your login page uses a different route. ```tsx title="pages/index.tsx" check=false reason="Requires surrounding framework application context" import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import Session from "supertokens-web-js/recipe/session"; import ProtectedPage from "./protectedPage"; type SessionStatus = "loading" | "authenticated" | "redirecting" | "error"; export default function Home() { const router = useRouter(); const [sessionStatus, setSessionStatus] = useState("loading"); useEffect(() => { let active = true; async function checkSession() { try { const sessionExists = await Session.doesSessionExist(); if (!active) { return; } if (!sessionExists) { setSessionStatus("redirecting"); const didNavigate = await router.replace("/login"); if (active && !didNavigate) { setSessionStatus("error"); } return; } setSessionStatus("authenticated"); } catch { if (active) { setSessionStatus("error"); } } } void checkSession(); return () => { active = false; }; }, [router]); if (sessionStatus === "error") { return
Unable to verify your session. Please try again.
; } if (sessionStatus === "redirecting") { return
Redirecting...
; } if (sessionStatus === "loading") { return
Loading...
; } return ; } ```
--- # 3. Adding auth APIs Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/setting-up-backend We will add all the backend APIs for auth on `/api/auth`. This can be changed by setting the `apiBasePath` property in the `appInfo` object in the `appInfo.ts` file. For the rest of this page, we will assume you are using `/api/auth`. ## 1. Create the `pages/api/auth/[[...path]].tsx` page - Be sure to create the `auth` folder in the `pages/api/` folder. - `[[...path]].tsx` will use the middleware exposed by `supertokens-node` which exposes all the APIs like sign in, sign up etc.. ## 2. Expose the SuperTokens APIs ```tsx title="pages/api/auth/[[...path]].ts" check=false reason="Requires surrounding framework application context" import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { middleware } from "supertokens-node/framework/express"; import { NextApiRequest, NextApiResponse } from "next"; import { Request, Response } from "express"; import supertokens from "supertokens-node"; import { backendConfig } from "../../../config/backendConfig"; import NextCors from "nextjs-cors"; supertokens.init(backendConfig()); export default async function superTokens(req: NextApiRequest & Request, res: NextApiResponse & Response) { await NextCors(req, res, { methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], origin: "", credentials: true, allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], }); await superTokensNextWrapper( async (next) => { res.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); await middleware()(req, res, next); }, req, res, ); if (!res.writableEnded) { res.status(404).send("Not found"); } } ``` :::note[In the snippet above we add the `Cache-Control` header to the responses for all auth APIs. This is required if you are deploying your app with Vercel because API responses are automatically cached for production deployments. This results in problems because APIs such as `/session/refresh` return older session tokens resulting in infinite calls to refresh if an API returns unauthorised status. Setting the header ensures that Vercel does not cache any of the auth API responses.] ::: ## 3. Use the login widget If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) --- # 2. Showing Login UI Source: https://supertokens.com/docs/integrations/nextjs/pages-directory/setting-up-frontend ## 1. Create the `pages/auth/[[...path]].tsx` page - Be sure to create the `auth` folder in the `pages` folder. - `[[...path]].tsx` will contain the component for showing SuperTokens UI ## 2. Create the `Auth` component: ```tsx title="pages/auth/[[...path]].tsx" import React, { useEffect } from "react"; import dynamic from "next/dynamic"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { redirectToAuth } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; const SuperTokensComponentNoSSR = dynamic<{}>( new Promise((res) => res(() => getRoutingComponent([EmailPasswordPreBuiltUI]))), { ssr: false }, ); export default function Auth() { // if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. useEffect(() => { if (canHandleRoute([EmailPasswordPreBuiltUI]) === false) { redirectToAuth(); } }, []); return ; } ``` ## 3. Visit `/auth` page on your website If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre-built UI, for each page on [this link](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/auth-page--playground). If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) You need to build your own UI. You will have to check each [authentication method tutorial](/authentication/overview) for detailed instructions how how to achieve this. --- # Overview Source: https://supertokens.com/docs/integrations/overview Explore different integration guides that present how to use SuperTokens with different platforms and frameworks. --- ## Frameworks ## Cloud Platforms --- # Supabase Source: https://supertokens.com/docs/integrations/supabase ## Overview The following guide shows you how to integrate a Next.js app with SuperTokens and Supabase. It includes instructions on how to: - Create a Supabase project with a table to store your user data - Create a Supabase JWT and store the user's session - Enable row level security policies in your Supabase table to ensure only authorized users can access their data In this example, the user's email is stored mapped to their SuperTokens userId in Supabase. You can also check an [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-supabase) for specific references. ## Before you start The guide does not include instructions on how to setup a Next.js app with SuperTokens. To do this you can follow the [app router](/integrations/nextjs/app-directory/about) or [pages router](/integrations/nextjs/pages-directory/about) instructions. ## Steps ### 1. Configure Supabase Supabase provides a database with authentication and authorization features. This guide uses Supabase to store the user's info mapped to their SuperTokens `userId`. #### 1.1 Create a new Supabase project 1. From your [Supabase dashboard](https://app.supabase.com/), click New project. 2. Enter a Name for your Supabase project. 3. Enter a secure Database Password. 4. Select the same Region you host your app's backend in. 5. Click Create new project. ![Supabase dashboard](/docs-assets/img/thirdpartyemailpassword/supabase/supabase_dashboard_create.png) #### 1.2 Create the user table in Supabase 1. From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), click Table editor, then New table. 2. Enter `users` as the `Name` field. 3. Select `Enable Row Level Security (RLS).` 4. Remove the default columns 5. Create two new columns: - `user_id` as `varchar` as primary key - email as `varchar` 6. Click `Save` to create the new table. ![Supabase table create](/docs-assets/img/thirdpartyemailpassword/supabase/supabase_table_create.png) ### 2. Setup JWT creation In this section, the SuperTokens backend is overridden to create a JWT signed with Supabase's secret which contains the user's `userId`. This token is used on the frontend and backend to read and write to Supabase's database. #### 2.1 Integrate your Next.js app with SuperTokens Follow either the [app router](/integrations/nextjs/app-directory/about) or [pages router](/integrations/nextjs/pages-directory/about) guides for instructions on how to configure your application. #### 2.2 Save the Supabase configuration values Retrieve the Supabase configuration values from the dashboard and add them to your `.env` file: ```bash // retrieve the following from your supabase dashboard NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_KEY= SUPABASE_SIGNING_SECRET= ``` #### 2.3 Create the Supabase JWT In the Next.js app when a user signs up, you'll want to store the user's email in Supabase. The email can then be retrieved from Supabase and displayed on the frontend. To use the Supabase client to query the database, you need to create a JWT signed with your Supabase app's signing secret. This JWT also needs to contain the user's `userId` so Supabase knows an authorized user is making the request. To create this flow, SuperTokens needs to be modified so that, when a user signs up or signs in, a JWT signed with Supabase's signing secret is created and attached to the user's session. Attaching the JWT to the user's session allows the Supabase JWT to be retrieved on the frontend and backend (post session verification), which can then be used to query Supabase. To create the JWT signed with Supabase's signing secret, the `jsonwebtoken` library is used. ```bash npm install jsonwebtoken ``` The JWT can be added to the user's session by overriding the `createNewSession` function and adding it to the `accessTokenPayload` ```ts // config/backendConfig.ts import EmailPassword from "supertokens-node/recipe/emailpassword"; import SessionNode from "supertokens-node/recipe/session"; import { TypeInput, AppInfo } from "supertokens-node/types"; import jwt from "jsonwebtoken"; let appInfo: AppInfo = { appName: "TODO: add your app name", apiDomain: "TODO: add your website domain", websiteDomain: "TODO: add your website domain", }; let supabase_signing_secret = process.env.SUPABASE_SIGNING_SECRET || "TODO: Your Supabase Signing Secret"; let backendConfig = (): TypeInput => { return { framework: "express", supertokens: { connectionURI: "https://try.supertokens.com", }, appInfo, recipeList: [ EmailPassword.init({ /*...*/ }), SessionNode.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { const payload = { userId: input.userId, exp: Math.floor(Date.now() / 1000) + 60 * 60, }; const supabase_jwt_token = jwt.sign(payload, supabase_signing_secret); input.accessTokenPayload = { ...input.accessTokenPayload, supabase_token: supabase_jwt_token, }; return await originalImplementation.createNewSession(input); }, }; }, }, }), ], isInServerlessEnv: true, }; }; ``` ### 3. Create a Supabase client A client is created to interact with Supabase using the `supabase-js` library. #### 3.1 Install the `supabase-js` library ```bash npm install @supabase/supabase-js ``` #### 3.2 Create a new file called `utils/supabase.ts` and add the following: ```ts check=false reason="Requires surrounding application context" // utils/supabase.ts import { createClient } from "@supabase/supabase-js"; let supabase_url = process.env.NEXT_PUBLIC_SUPABASE_URL || "TODO: Your Supabase URL"; let supabase_key = process.env.NEXT_PUBLIC_SUPABASE_KEY || "TODO: Your Supabase Key"; const getSupabase = (access_token: string) => { const supabase = createClient(supabase_url, supabase_key); supabase.auth.session = () => ({ access_token, token_type: "", user: null, }); return supabase; }; export { getSupabase }; ``` ### 4. Insert users into Supabase when they sign up In this example app, the user can sign up via Email-Password authentication. The API needs to be overridden such that when a user signs up, their email mapped to their userId is stored in Supabase. #### 4.1 Override the Email-Password sign up function ```ts check=false reason="This configuration fragment uses a recipe placeholder that must be replaced for the chosen authentication method." // config/backendConfig.ts let appInfo: AppInfo = { appName: "TODO: add your app name", apiDomain: "TODO: add your website domain", websiteDomain: "TODO: add your website domain" } // take a look at the Creating Supabase Client section to see how to define getSupabase let getSupabase: any; let backendConfig = (): TypeInput => { return { framework: "express", supertokens: { connectionURI: "https://try.supertokens.com", }, appInfo, recipeList: [ .init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // the signUpPOST function handles sign up signUpPOST: async function (input) { if (originalImplementation.signUpPOST === undefined) { throw Error("Should never come here"); } let response = await originalImplementation.signUpPOST(input); if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { // retrieve the accessTokenPayload from the user's session const accessTokenPayload = response.session.getAccessTokenPayload(); // create a supabase client with the supabase_token from the accessTokenPayload const supabase = getSupabase(accessTokenPayload.supabase_token); // store the user's email mapped to their userId in Supabase const { error } = await supabase .from("users") .insert({ email: response.user.emails[0], user_id: response.user.id }); if (error !== null) { throw error; } } return response; }, }; }, }, }), SessionNode.init({/*...*/}), ], isInServerlessEnv: true, }; }; ``` The Email-Password sign up flow is changed by overriding the `signUpPOST` API. When a user signs up, the `supabase_token` is retrieved from the user's `accessTokenPayload`(this was added in the previous step where the `createNewSession` function was changed) and used to query Supabase to insert the new user's information. ### 5. Retrieve the user email on the frontend With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. ```tsx // pages/index.tsx import React, { useState, useEffect } from "react"; import Head from "next/head"; import { SessionAuth, useSessionContext } from "supertokens-auth-react/recipe/session"; // take a look at the Creating Supabase Client section to see how to define getSupabase let getSupabase: any; export default function Home() { return ( // The ProtectedPage component is wrapped with the SessionAuth so only an // authenticated user can access it. ); } function ProtectedPage() { // retrieve the authenticated user's accessTokenPayload and userId from the sessionContext const session = useSessionContext(); const [userEmail, setEmail] = useState(""); useEffect(() => { async function getUserEmail() { if (session.loading) { return; } // retrieve the supabase client who's JWT contains users userId, this is // used by supabase to check that the user can only access table entries which contain their own userId const supabase = getSupabase(session.accessTokenPayload.supabase_token); // retrieve the user's name from the users table whose email matches the email in the JWT const { data } = await supabase.from("users").select("email").eq("user_id", session.userId); if (data.length > 0) { setEmail(data[0].email); } } getUserEmail(); }, [session]); if (session.loading) { return null; } return (
SuperTokens 💫

You are authenticated with SuperTokens! (UserId: {session.userId})
Your email retrieved from Supabase: {userEmail}

); } ```
With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. ```tsx import Session from "supertokens-web-js/recipe/session"; // take a look at the Creating Supabase Client section to see how to define getSupabase let getSupabase: any; async function getEmailFromSupabase() { if (await Session.doesSessionExist()) { let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); const supabase = getSupabase(accessTokenPayload.supabase_token); const { data } = await supabase .from("users") .select("email") .eq("user_id", await Session.getUserId()); if (data.length > 0) { return data[0].email; } return undefined; } throw new Error("Session does not exist"); } ``` As seen above, the access token payload is fetched from SuperTokens to retrieve the authenticated user's Supabase access token which can be used to fetch the user's email from Supabase. ### 6. Enforce row level security for select and insert requests To enforce Row Level Security for the Users table, you need to create policies for Select and Insert requests. These polices retrieve the `userId` from the JWT and check if it matches the `userId` in the Supabase table. A PostgreSQL function is needed to extract the `userId` from the JWT. The payload in the JWT has the following structure: ```bash { userId, exp } ``` #### 6.1 Create PostgreSQL function to retrieve `userId` from JWT To create the PostgreSQL function, navigate back to the Supabase dashboard, select `SQL` from the sidebar menu, and click `New query`. This creates a new query called `new sql snippet`, which allows you to run any SQL against the Postgres database. Write the following and click `Run`. ```bash create or replace function auth.user_id() returns text as $$ select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text; $$ language sql stable; ``` - This creates a function called `auth.user_id()`, which inspects the `userId` field of our JWT payload. #### 6.2 Create Policies for `SELECT` and `INSERT` queries: ##### `SELECT` query policy The first policy checks whether the user is the owner of the email being retrieved. - Select `Authentication` from the Supabase sidebar menu, click `Policies`, and then `New Policy` on the `Users` table. ![Create policy](/docs-assets/img/thirdpartyemailpassword/supabase/create_policy.png) - From the modal, select `Create a policy from scratch` and add the following. ![select policy](/docs-assets/img/thirdpartyemailpassword/supabase/policy_config_select.png) - This policy is calling the PostgreSQL function we just created to get the currently logged in user's ID `auth.user_id()` and checking whether this matches the `user_id` column for the current `email`. If it does, then it allows the user to select it, otherwise it continues to deny. - Click `Review` and then `Save policy`. ##### `INSERT` query policy The second policy checks whether the `user_id` being inserted is the same as the `userId` in the JWT. - Create another policy and add the following: ![insert policy](/docs-assets/img/thirdpartyemailpassword/supabase/policy_config_insert.png) Similar to the previous policy, the PostgreSQL function that was created is called to get the currently logged in user's ID and check whether this matches the `user_id` column for the row being inserted. If it does, then it allows the user to insert the row, otherwise it continues to deny. Click `Review` and then `Save policy`. ### 6.3 Test your changes You can now sign up and you should see the following screen: ![auth screen](/docs-assets/img/thirdpartyemailpassword/supabase/supabase_app_authenticated_screen.png) If you navigate to your table you should see a new row with the user's `user_id` and `email`. ![table with user](/docs-assets/img/thirdpartyemailpassword/supabase/table_with_user.png) --- # Vercel Source: https://supertokens.com/docs/integrations/vercel This page only talks about what environment variables to use when you are deploying an application on Vercel. For a full set of instructions on how to integrate **SuperTokens** in a **Next.js** project, please see either our [app router](/integrations/nextjs/app-directory/about) or [pages router](/integrations/nextjs/pages-directory/about) guides. ## Working with Vercel's inspect and production URL Vercel provides one production URL per app and one unique inspect URL per deployment. To get SuperTokens to work with dynamic URLs, you need to make the following changes to the [`appInfo` object](/references/frontend-sdks/reference#sdk-configuration): ### On the frontend ```text appInfo = { apiDomain: window.location.origin, websiteDomain: window.location.origin, ... } ``` ```text appInfo = { apiDomain: window.location.origin ... } ``` ### On the backend ```text appInfo = { apiDomain: process.env.VERCEL_URL, websiteDomain: process.env.VERCEL_URL, ... }, ``` Vercel adds an environment variable to the backend - `VERCEL_URL`, which points to the current URL that the app is deployed on. This allows SuperTokens to work on all inspect URLs generated by Vercel without you having to keep changing your code. :::note[The above setting works only if your backend and frontend are deployed on the same URL. If you are using a different backend and using Vercel only for your frontend, then:] - Set the `apiDomain` on the frontend and backend to point to your backend. - The `websiteDomain` on the frontend should be `window.location.origin`, but on the backend, it should be equal to your production deployment URL. This will break certain features of the app for inspect URL deployments, but it will work as expected for production deployments. ::: --- # Account Migration Source: https://supertokens.com/docs/migration/account-migration The following guide shows you how to move users from your current authentication solution to **SuperTokens**. --- ## Overview The process of migrating your accounts breaks down into two parts: ### Creating new users on the fly To ensure a smooth migration process, with no downtime, you need to be able to directly create new users from the legacy sign up flow. This is necessary since there is a time gap between when you export all your data for bulk import and when you go live with **SuperTokens**. New users might get created in that interval through your legacy authentication provider. Hence, you also need to create them in **SuperTokens** to keep the data in sync. ### Adding most of your users through a bulk import After you have set in place the lazy migration process you can move on to adding most of your users. This happens through the bulk import API. The process is asynchronous and can work with large amounts of data. ## Before you start This guide assumes that you have already integrated **SuperTokens** with your existing stack. If you have not, please check the [Quickstart Guide](/quickstart) and explore all the supported [authentication methods](/authentication/overview). Bulk import requires Core `10.0.0` or later and persistent database storage; the in-memory database does not support these APIs. Before importing: - create and configure every target tenant, role, recipe, and third-party provider referenced by the import; - enable account linking before importing a user with multiple login methods, and test your linking policy with a representative export; - decide how each legacy identity maps to a tenant and login method, and reject ambiguous or duplicate mappings; and - take a restorable source export and define retry, reconciliation, rollback, and cutover procedures. For email/password users, provide either a supported `passwordHash` with its `hashingAlgorithm`, or a `plainTextPassword`, as defined by the [bulk-import request schema](/references/cdi/bulk-import/addbulkimportusers). Prefer compatible bcrypt, Argon2, or Firebase `scrypt` hashes over plain-text passwords. Treat exports, password hashes, MFA secrets, API keys, and access tokens as credentials: encrypt them in transit and at rest, restrict access, never put them in logs or user metadata, and securely delete temporary copies after reconciliation. ## Steps ### 1. Update the legacy sign up flow Modify the legacy sign up flow logic to also create new users in **SuperTokens**. You can do this through the `Import User` endpoint that allows you to directly create accounts. Call the endpoint from the authentication flow used by your legacy provider. :::caution[Unverified mapping pseudocode] The following Action illustrates where a login-time direct import can run. Its identity fields, provider mapping, and `getPasswordHash` placeholder have not been validated against a current Auth0 password/MFA support export. Adapt and test it against a redacted export before use; do not deploy it as-is. ::: :::warning Auth0 does not expose password hashes or `TOTP` device information. You will have to contact their support separately if you need this type of data. ::: Create the Auth0 roles in SuperTokens before migrating users. The application endpoint must own an allowlisted mapping from Auth0 organization/connection/provider identifiers to SuperTokens tenants and providers. Do not let Action input select arbitrary tenant IDs or provider configuration. ##### 1. Access the Auth0 Dashboard ##### 2. From the navigation menu go to *Actions* > *Library* ##### 3. Click *Create Action* > *Create custom action* ##### 4. Specify a custom name for your action and then select *Login/Post Login* as the trigger ##### 5. Add `MIGRATION_ENDPOINT_URL` and `MIGRATION_ENDPOINT_TOKEN` Action secrets ##### 6. Paste the following code in the editor `MIGRATION_ENDPOINT_TOKEN` must authorize only this migration endpoint. The endpoint must authenticate every request, allow only the expected Auth0 tenant/issuer, rate-limit by credential and legacy user ID, enforce request-size limits, and use `externalUserId` as an idempotency key. Keep the Core URL and Core API key only in your backend secret store. The backend validates and maps the identity, retrieves any credential export through restricted storage, and then calls Core. ```typescript check=false reason="Requires application specific migration types" exports.onExecutePostLogin = async (event, api) => { const migrationEndpoint = event.secrets.MIGRATION_ENDPOINT_URL; const migrationToken = event.secrets.MIGRATION_ENDPOINT_TOKEN; try { if (event.user.app_metadata?.migrated_to_supertokens) { return; } const response = await fetch(migrationEndpoint, { method: "POST", headers: { Authorization: `Bearer ${migrationToken}`, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify({ externalUserId: event.user.user_id, auth0OrganizationId: event.organization?.id, identities: event.user.identities?.map(({ provider, connection, user_id }) => ({ provider, connection, userId: user_id, })), }), }); const result = await response.json(); if (response.ok && result.status === "OK") { api.user.setAppMetadata("migrated_to_supertokens", true); api.user.setAppMetadata("supertokens_user_id", result.userId); } else { console.error("Migration endpoint rejected the request"); } } catch (error) { console.error("Migration endpoint request failed"); } }; ```
:::info[If your application does not have a sign up process or if new users get created manually you can skip this step] ::: ### 2. Export the accounts from your legacy provider Export the users from your legacy authentication provider and adjust the data to match the request body schema used in the [**`Add Users for Bulk Import`**](/references/cdi/bulk-import/addbulkimportusers) endpoint. :::warning Auth0 does not export password hashes or `TOTP` device information. You will have to contact their support and request them. ::: #### 1. Create a management API application in Auth0 ##### 1.1 Navigate to Auth0 Dashboard and the select `Applications` > `APIs` ##### 1.2 Select `Auth0 Management API` ##### 1.3 Go to `Machine to Machine Applications` tab ##### 1.4 Authorize your application or create a new one ##### 1.5 Grant only `read:users` and `read:users_app_metadata` ##### 1.6 Save your `Domain`, `Client ID`, and `Client Secret` #### 2. Get the management API access token You need a valid Management API Access Token to export users. Use the following `cURL` command to get the token: ```bash curl --request POST \ --url 'https://YOUR_DOMAIN.auth0.com/oauth/token' \ --header 'content-type: application/json' \ --data '{ "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "audience": "https://YOUR_DOMAIN.auth0.com/api/v2/", "grant_type": "client_credentials" }' ``` #### 3. Create the export job Use the `POST /api/v2/jobs/users-exports` endpoint to create a job that exports all users. ```bash curl --request POST \ --url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/users-exports' \ --header 'authorization: Bearer YOUR_MGMT_API_TOKEN' \ --header 'content-type: application/json' \ --data '{ "format": "json", "fields": [ {"name": "user_id"}, {"name": "email"}, {"name": "email_verified"}, {"name": "name"}, {"name": "nickname"}, {"name": "picture"}, {"name": "created_at"}, {"name": "updated_at"}, {"name": "identities"}, {"name": "app_metadata"}, {"name": "user_metadata"}, {"name": "phone_number"}, {"name": "phone_verified"} ] }' ``` #### 4. Check the export job status Check if the export job has finalized with this request: ```bash curl --request GET \ --url 'https://YOUR_DOMAIN.auth0.com/api/v2/jobs/job_abc123xyz' \ --header 'authorization: Bearer YOUR_MGMT_API_TOKEN' ``` #### 5. Download the export file The previous request returns a `location` attribute in the response body if the export job has finalized. Use it do access your data. ```bash umask 077 cd /secure-migration-work # An encrypted, access-restricted filesystem curl --fail --location --proto '=https' -o auth0_users.json.gz "LOCATION_URL_FROM_RESPONSE" gzip --test auth0_users.json.gz sha256sum auth0_users.json.gz > auth0_users.json.gz.sha256 age --recipient "" --output auth0_users.json.gz.age auth0_users.json.gz ``` Keep the compressed download on that encrypted filesystem, move the encrypted archive and checksum to restricted migration storage, and verify that decryption succeeds. Auth0 exports NDJSON inside the gzip stream. Convert it without replacing the retained compressed archive: ```bash age --decrypt --identity /run/secrets/migration-archive-key auth0_users.json.gz.age \ | gzip -dc \ | jq -s '.' > /secure-migration-work/auth0_users_array.json ``` Keep the compressed export, encrypted copy, and checksum unchanged through transformation, import, failed-row retries, and source-to-target reconciliation. Keep derived plaintext only on encrypted restricted storage and delete it after each run. Delete all source-archive copies only after final reconciliation and rollback retention requirements are met; use your storage system's verified deletion/lifecycle mechanism rather than assuming `rm` securely erases every medium. #### 6. Transform the data to the SuperTokens format :::warning Auth0 does not expose password hashes or `TOTP` device information. You will have to contact their support separately if you need this type of data. ::: Create the Auth0 roles in SuperTokens before migrating users. This example assigns them to the default `public` tenant. :::caution[Unverified mapping pseudocode] The transformation below assumes application-specific Auth0 identity fields and an undefined `getPasswordHash` lookup. The relationship between ordinary user-export rows and a separately requested password/MFA export is not established here. Validate the mapping against a redacted current export and Core import validation before handling production data. ::: ```typescript check=false reason="Requires application specific migration types" const fs = require("fs"); const auth0Users = JSON.parse(fs.readFileSync("auth0_users_array.json", "utf8")); const superTokensUsers = auth0Users .map((auth0User) => { if (auth0User.app_metadata?.migrated_to_supertokens) { console.log(`User ${auth0User.user_id} already migrated`); return; } const userPayload = { externalUserId: auth0User.user_id, userMetadata: { auth0_user_id: auth0User.user_id, name: auth0User.name, nickname: auth0User.nickname, picture: auth0User.picture, auth0_user_metadata: auth0User.user_metadata, auth0_app_metadata: auth0User.app_metadata, }, userRoles: (auth0User.app_metadata?.roles || []).map((role) => ({ role, tenantIds: ["public"] })), loginMethods: [], }; const ThirdPartyProviders = ["google-oauth2", "facebook", "github", "apple"]; auth0User.identities.forEach((identity, index) => { if (ThirdPartyProviders.includes(identity.provider)) { userPayload.loginMethods.push({ recipeId: "thirdparty", thirdPartyId: mapProvider(identity.provider), thirdPartyUserId: identity.user_id, email: identity.profileData?.email ?? auth0User.email, isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, isPrimary: index === 0, timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), }); } else if (identity.provider === "auth0" || identity.provider === "Username-Password-Authentication") { // Auth0 does not export password hashes through the ordinary user export // You will have to contact their support and request them userPayload.loginMethods.push({ recipeId: "emailpassword", email: identity.profileData?.email ?? auth0User.email, // Request the password hash from Auth0 and then implement the function to retrieve the values passwordHash: getPasswordHash(identity.profileData?.email), hashingAlgorithm: "bcrypt", isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, isPrimary: index === 0, timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), }); } else if (identity.provider === "sms") { userPayload.loginMethods.push({ recipeId: "passwordless", phoneNumber: identity.profileData?.phone_number || auth0User.phone_number, isVerified: identity.profileData?.phone_verified ?? auth0User.phone_verified ?? false, isPrimary: index === 0, timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), }); } else if (identity.provider === "email") { userPayload.loginMethods.push({ recipeId: "passwordless", email: identity.profileData?.email || auth0User.email, isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, isPrimary: index === 0, timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), }); } else { throw new Error(`Unknown provider: ${identity.provider}`); } }); return userPayload; }) .filter(Boolean); fs.writeFileSync("supertokens_users.json", JSON.stringify({ users: superTokensUsers }, null, 2)); function mapProvider(auth0Provider) { const mapping = { "google-oauth2": "google", facebook: "facebook", github: "github", apple: "apple", }; return mapping[auth0Provider] || auth0Provider; } console.log(`Transformed ${superTokensUsers.length} users`); ``` ### 3. Perform the bulk migration process :::warning If your application has a sign up process please make sure that you have completed the [**first step**](#1-update-the-legacy-sign-up-flow). Otherwise, new accounts that get created after you have exported your users are not available in **SuperTokens**. ::: #### 3.1 Add the accounts to import Using the data that you have generated in the previous step, call the `Add Users for Bulk Import` endpoint. This step stages the data that the background job imports later. Keep in mind that the endpoint has a limit of **10000 users** per request. :::info[The Bulk Import Cron Job] Every 5 minutes the **SuperTokens** core service runs a cron job that goes through the staged users and tries to import them. If a user gets imported successfully it gets removed from the staged list. ::: #### 3.2 Monitor the progress of the job To determine if the import flow has processed all the users, call the [`Count Staged Users`](/references/cdi/bulk-import/countbulkimportusers) API. Before doing that, first understand the different states in which a staged user can be. During the import process, the user can have one of the following statuses: - **NEW (not yet started)**: The import process has not yet picked up the user. - **PROCESSING**: The import process has selected the user for import. - **FAILED**: The import process has failed for that user. If a user gets imported successfully it then gets removed from the staged list. Hence, no status exists for that state. With this new information, get back to the `count users` endpoint. The request counts the users that await import. Pass a status filter as a query parameter to count only users in that state: `status=NEW`, `status=PROCESSING`, or `status=FAILED`. Given that information, to check if your import is complete do the following: 1. Call the `count users` API once without any filters. If the count is 0, then the import process is complete. 2. If the count is not 0, then check if you still have rows that are getting processed (`status=PROCESSING`) or if there are rows that the import job has not yet picked up (`status=NEW`) 3. If the only rows that remain are the ones with the `FAILED` status, then proceed to step `3.3`. There you can see how to debug those issues. #### 3.3 Handle staged users that failed to import Go through this step only if you have staged users that failed to import. This can happen for a number of reasons. Some common ones: - `Email` / `phoneNumber` already exists - `externalUserId` is being already used by other user - A primary user already exists for the email but with a different login method If at the end of the previous step you have determined that you have staged users that failed to import, debug the issues with the [`Get Staged Users`](/references/cdi/bulk-import/getbulkimportusers) API. Filter the results with `status=FAILED`. The response includes the import error messages for each specific user. Use them to determine what you need to correct in your import data. Record the failed staged-row IDs and remove those exact rows before retrying. Verify that every requested ID appears in `deletedIds` and that `invalidIds` is empty; otherwise, stop and reconcile the discrepancy. After removal, fix the source records and repeat step `3.1` only for that corrected data. Re-run the status checks and reconcile every source identity to one successfully imported account. Never treat a zero count as sufficient if the source export, removed IDs, corrected retries, and final accounts do not reconcile. :::success[You have successfully migrated your accounts] If all your data has imported then you can consider the account migration process complete. Go on to the [session migration](/migration/session-migration) step to complete the entire migration flow. ::: ## See also --- # About Source: https://supertokens.com/docs/migration/legacy/about In this guide we will be going through the process of migrating users from an external Authentication provider to SuperTokens. User migration involves 3 steps: - Account Migration - User Creation - UserId Mapping - Mark email as verified - User Data Migration - Session Migration ## Step 1. Account Migration: ### User Creation - Our first step involves creating a SuperTokens user by importing their account credentials from the previous authentication provider. - You can learn more about how to implement these changes in the [User Creation](./account-creation/user-creation) section. ### User ID Mapping - If you have stored information against existing userIds in your application table, you can use UserId Mapping to map the existing userId to the user's SuperTokens userId. - Once the userIds are mapped you can use the existing userId to interact with all of SuperTokens APIs. - You can learn more about how to implement these changes in the [User Id Mapping](./account-creation/user-id-mapping) section. ### Mark email as verified - Once a SuperTokens user has been created and their userId has been mapped, you need to mark their email as verified (if applicable) so that they do not have to go through the email verification process again. - You can learn more about how to implement these changes in [this section](./account-creation/email-verification). ## Step 2. User Data Migration - Now that your user's account has been migrated over to SuperTokens we can associate additional information like roles and metadata with your user. - You can learn more about how to implement these changes in the [User Data Migration](./data-migration) section. ## Step 3. Session Migration - If you have users with an existing session, you can use [this guide](./session-migration) to migrate their external provider sessions to a SuperTokens session. - This will prevent users from having to re-authenticate. You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. ## Step 4. MFA migration If you are using MFA in your app, checkout the MFA migration section [here](/additional-verification/mfa/migration/legacy-to-new) after you have gone through the above migration steps. --- ## See also --- # Mark email as verified Source: https://supertokens.com/docs/migration/legacy/account-creation/email-verification Once a SuperTokens user has been created and their userId has been mapped, you need to mark their email as verified, if their email was verified in the old auth provider. ## Step 1. Generating the email verification token: For example with the email as `johnDoe@gmail.com` and userId as `056f4b02-c992-42ed-a8af-cb709669bbd` ```bash curl --location --request POST '/recipe/user/email/verify/token' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johnDoe@gmail.com", "userId": "056f4b02-c992-42ed-a8af-cb709669bbd" }' ``` Successfully generating an email verification token will result in the following response ```bash { "status":"OK", "token":"OWU2OGQyZWQ5MGFkMzM1M2Y4ZDMzNjE1NzA4ZGI0YWYyODEwMzg0NjJhNTcxNDZjYmY0NzJiOTZmYWE5OTJkMzRmOWVkYzBiODZkMWNmYTJkY2I5YWJkZDU2Yjg0NTU0" } ``` ## Step 2. Verifying the users email with the verification token Retrieve the token from the response of the previous request and set it in the body of the email verification request. ```bash curl --location --request POST '/recipe/user/email/verify' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "method": "token", "token": "OWU2OGQyZWQ5MGFkMzM1M2Y4ZDMzNjE1NzA4ZGI0YWYyODEwMzg0NjJhNTcxNDZjYmY0NzJiOTZmYWE5OTJkMzRmOWVkYzBiODZkMWNmYTJkY2I5YWJkZDU2Yjg0NTU0" }' ``` --- # User Creation without password hashes Source: https://supertokens.com/docs/migration/legacy/account-creation/ep-migration-without-password-hash :::warning The recommended method for migrating users to SuperTokens is by [importing users with their password hashes](./user-creation). You should only use the following method if you do not have access to your user's password hashes and still have access to your previous identity provider. ::: :::danger[Add concurrency and recovery controls before production use] The handlers below are partial examples; the external-provider functions and application locking are intentionally not implemented. Sign-up, user-ID mapping, email verification, and metadata updates are separate writes, not one transaction. For each normalized `(tenantId, email)` identity, serialize migration in a distributed lock, re-read the SuperTokens user and user-ID mapping after acquiring it, and make retries resume only missing steps. Treat an existing mapping as idempotent only when both its SuperTokens and external IDs match the intended user. Abort on every conflicting mapping, tenant mismatch, or response other than success, and reconcile partially created users before retrying. Initialize every recipe called by a handler in the same SDK initialization: Email Password, Email Verification in the mode used by your application, and User Metadata where metadata is read or written. Preserve the `RecipeUserId` returned by sign-up for recipe operations; user-ID mapping does not replace it with the external ID. Test concurrent requests, crashes after each write, retries, and mapping conflicts before enabling these overrides. ::: SuperTokens also supports the "**in time**" user migration strategy for when password hashes cannot be exported from your legacy provider. We need to make the following customizations to SuperTokens authentication flows to support this strategy: - **Step 1) Prevent sign ups from users who exist in the external provider.** - To prevent duplicate accounts from being created, we block sign ups from users who have existing accounts with the external provider. - **Step 2) Create a SuperTokens account for users trying to sign in if they have an account with the external provider.** - We modify the sign in flow to check if the user signing in has an existing account with the external provider and not with SuperTokens. If their input credentials are valid, we create a SuperTokens user and import their user data. - **Step 3) Create a SuperTokens account for users who have an account with the external provider but have forgotten their password.** - Some users who have an account with the external provider and not with SuperTokens may have forgotten their passwords and trigger the password reset flow. Since SuperTokens requires an existing account to send the reset password email to, we need to modify the password reset flow to check that if the user needs to be migrated. If they do, we create a SuperTokens account with a temporary password, import their user data and continue the password reset flow. - To ensure that users can only sign in once they successfully reset their passwords we add the `isUsingTemporaryPassword` flag to the account's metadata. We also modify the sign in flow to block sign ins from accounts with this metadata. - **Step 4) Remove the `isUsingTemporaryPassword` flag on successful password reset** - Once the password has been successfully reset we check if the user has the `isUsingTemporaryPassword` flag set in the metadata. If they do we clear the flag from the user's metadata. - **Step 5) Update the login flow to account for the `isUsingTemporaryPassword` flag** - We also update the login flow to prevent sign ins from accounts who have the `isUsingTemporaryPassword` flag and if their input password does not match the one in the legacy auth provider. This ensures that users who started the password reset flow are forced to finish it. ## Step 1) Prevent sign ups from users who exist in the external provider To implement this change we override the API that handles email-password login when initializing the recipe on the backend. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; const emailPasswordRecipe = EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { let email = input.formFields.find((field) => field.id === "email")!.value as string; // Check if the user signing in exists in the external provider if (await doesUserExistInExternalProvider(email)) { // Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider return { status: "EMAIL_ALREADY_EXISTS_ERROR", }; } return originalImplementation.signUpPOST!(input); }, }; }, }, }); SuperTokens.init({ framework: "express", supertokens: { connectionURI: "" }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", }, recipeList: [emailPasswordRecipe, Session.init()], }); async function doesUserExistInExternalProvider(email: string): Promise { // TODO: check if user with the input email exists in the external provider return false; } ``` ```python check=false reason="Partial configuration example" from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, EmailAlreadyExistsError, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.session.interfaces import SessionContainer def override_email_password_apis(original_implementation: APIInterface): original_sign_up = original_implementation.sign_up_post async def sign_up( 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], ): email = "" for field in form_fields: if field.id == "email": email = field.value # check if the user signing in exists in the external provider if await does_user_exist_in_external_provider(email): # Return SignUpEmailAlreadyExistsError since the user exists in the external provider return EmailAlreadyExistsError() return await original_sign_up( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_up_post = sign_up return original_implementation async def does_user_exist_in_external_provider(email: str): # TODO: Check if a user with the input email exists in the external provider return False init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=override_email_password_apis, ) ), session.init(), ], ) ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signUp function originalSignUp := *originalImplementation.SignUpPOST // Override the signUp function (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { email := "" for _, formField := range formFields { if formField.ID == "email" { valueAsString, asStrOk := formField.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = valueAsString } } // Check if the user signing in exists in the external provider if doesUserExistInExternalProvider(email) { // Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider return epmodels.SignUpPOSTResponse{ EmailAlreadyExistsError: &struct{}{}, }, nil } return originalSignUp(formFields, tenantId, options, userContext) } return originalImplementation }, }, }), session.Init(nil), }, }) } func doesUserExistInExternalProvider(email string) bool { // TODO: Check if user with the input email exists in the external provider return false } ``` We modify the `signUpPOST` API to first check if the user signing up has an account with the external provider. If they do we return a `EMAIL_ALREADY_EXISTS_ERROR` ## Step 2) Create a SuperTokens account for users trying to sign in if they have an account with the external provider To implement this flow we override the API that handles email-password login when initializing the recipe on the backend. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; const emailPasswordRecipe = EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signInPOST: async function (input) { // Check if an email-password user with the input email exists in SuperTokens let email = input.formFields.find((field) => field.id === "email")!.value as string; let password = input.formFields.find((field) => field.id === "password")!.value as string; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo( input.tenantId, { email: email, }, undefined, input.userContext, ); let emailPasswordUser = supertokensUsersWithSameEmail.find((u) => { return ( u.loginMethods.find((lM) => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined ); }); if (emailPasswordUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); if (legacyUserInfo === undefined) { // credentials are incorrect return { status: "WRONG_CREDENTIALS_ERROR", }; } // Call the signup function to create a new SuperTokens user. let signUpResponse = await EmailPassword.signUp( input.tenantId, email, password, undefined, input.userContext, ); if (signUpResponse.status !== "OK") { throw new Error("Sign-up failed; re-read and reconcile this identity before retrying"); } // Map the external provider's userId to the SuperTokens userId const mappingResponse = await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id, userContext: input.userContext, }); if (mappingResponse.status !== "OK") { throw new Error("Legacy user ID conflicts with an existing mapping"); } // We also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { // Generate an email verification token for the user let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken( input.tenantId, signUpResponse.recipeUserId, email, input.userContext, ); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email await EmailVerification.verifyEmailUsingToken( input.tenantId, generateEmailVerificationTokenResponse.token, undefined, input.userContext, ); } } } return originalImplementation.signInPOST!(input); }, }; }, }, }); SuperTokens.init({ framework: "express", supertokens: { connectionURI: "" }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", }, recipeList: [emailPasswordRecipe, Session.init(), EmailVerification.init({ mode: "OPTIONAL" })], }); async function validateAndGetUserInfoFromExternalProvider( email: string, password: string, ): Promise< | { user_id: string; isEmailVerified: boolean; } | undefined > { // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. return undefined; } ``` ```python check=false reason="Partial configuration example" from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) from supertokens_python.recipe import emailpassword, emailverification, session from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpOkResult, WrongCredentialsError, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.interfaces import CreateUserIdMappingOkResult from supertokens_python.types.base import AccountInfoInput def override_emailpassword_apis(original_implementation: APIInterface): original_emailpassword_sign_in = original_implementation.sign_in_post async def sign_in( 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], ): email = "" password = "" for field in form_fields: if field.id == "email": email = field.value if field.id == "password": password = field.value # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email), False, user_context ) emailpassword_user = next( ( user for user in supertokens_user_with_same_email if any( lm.recipe_id == "emailpassword" and lm.has_same_email_as(email) for lm in user.login_methods ) ), None, ) if emailpassword_user is None: # EmailPassword user with the input email does not exist in SuperTokens # Check if the input credentials valid in the external provider legacy_user_info = await validate_and_get_user_info_from_external_provider( email, password ) if legacy_user_info is None: # Credentials are incorrect return WrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(tenant_id, email, password, None, user_context) if not isinstance(response, SignUpOkResult): raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Map the external provider's userId to the SuperTokens userId mapping_response = await create_user_id_mapping( response.user.id, legacy_user_info.user_id, user_context=user_context ) if not isinstance(mapping_response, CreateUserIdMappingOkResult): raise Exception("Legacy user ID conflicts with an existing mapping") # We also need to set the email verification status of the user if legacy_user_info.isEmailVerified: # Generate an email verification token for the user generate_email_verification_response = ( await create_email_verification_token( tenant_id, response.recipe_user_id, email, user_context, ) ) if isinstance( generate_email_verification_response, CreateEmailVerificationTokenOkResult, ): await verify_email_using_token( tenant_id, generate_email_verification_response.token, True, user_context, ) return await original_emailpassword_sign_in( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_in_post = sign_in return original_implementation class ExternalUserInfo: def __init__(self, user_id: str, isEmailVerified: bool): self.user_id: str = user_id self.isEmailVerified: bool = isEmailVerified async def validate_and_get_user_info_from_external_provider( email: str, password: str ) -> Union[None, ExternalUserInfo]: return None init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ), emailverification.init("OPTIONAL"), session.init(), ], ) ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signIn function originalSignIn := *originalImplementation.SignInPOST // Override the function (*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) { email := "" password := "" for _, formField := range formFields { if formField.ID == "email" || formField.ID == "password" { valueAsString, asStrOk := formField.Value.(string) if !asStrOk { return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if formField.ID == "email" { email = valueAsString } else { password = valueAsString } } } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email) if err != nil { return epmodels.SignInPOSTResponse{}, err } if emailPasswordUser == nil { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo == nil { return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } // Call the email-password signup function to create a new SuperTokens user. response, err := emailpassword.SignUp(tenantId, email, password) if err != nil { return epmodels.SignInPOSTResponse{}, err } if response.OK == nil { return epmodels.SignInPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") } recipeUserId := response.OK.User.ID // Map the external provider's userId to the SuperTokens userId mapping, err := supertokens.CreateUserIdMapping(recipeUserId, legacyUserInfo.userId, nil, nil) if err != nil { return epmodels.SignInPOSTResponse{}, err } if mapping.OK == nil { return epmodels.SignInPOSTResponse{}, errors.New("legacy user ID mapping conflicts with an existing mapping") } // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { // Generate an email verification token for the user generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, recipeUserId, &email) if err != nil { return epmodels.SignInPOSTResponse{}, err } if generateEmailVerificationTokenResponse.OK != nil { // Verify the user's email emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token) } } } return originalSignIn(formFields, tenantId, options, userContext) } return originalImplementation }, }, }), emailverification.Init(evmodels.TypeInput{Mode: evmodels.ModeOptional}), session.Init(nil), }, }) } type ExternalUserInfo struct { userId string isEmailVerified bool } func validateAndGetUserInfoFromExternalProvider(email string, password string) *ExternalUserInfo { // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. return nil } ``` The code above overrides the `signInPOST` API with the following changes to achieve "**in time**" migration: - The first step is to determine if the user signing in needs to be migrated or not. We do this by checking if a user with the input email exists in the external auth provider and SuperTokens. If the user exists in the external auth provider and does not exist SuperTokens, we can determine that this user needs to be migrated. - The next step is to validate the input credentials against the external provider. If the credentials are invalid we throw a `WRONG_CREDENTIALS_ERROR`. If the credentials are valid we can call the SuperTokens login function with the input credentials to create a new SuperTokens user. - We now map the external `userId` to the SuperTokens `userId`. This allows SuperTokens functions to reference the user with the external `userId`. - Finally, depending on the email verification status of the user in the external provider we also verify the user's email in SuperTokens. ## Step 3) Create a SuperTokens account for users who have an account with the external provider but have forgotten their password. Some users who do not have an account with SuperTokens but have an existing account with the external provider may have forgotten their passwords and initiate a password reset. Since password resets require an existing SuperTokens account to send the password reset email to, the password reset flow needs to be modified to create a SuperTokens account if the user exists in the external provider. ```tsx import { randomBytes } from "node:crypto"; import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import EmailVerification from "supertokens-node/recipe/emailverification"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Session from "supertokens-node/recipe/session"; const emailPasswordRecipe = EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // Add overrides from the previous step generatePasswordResetTokenPOST: async (input) => { // Retrieve the email from the input let email = input.formFields.find((i) => i.id === "email")!.value as string; // check if user exists in SuperTokens let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo( input.tenantId, { email, }, undefined, input.userContext, ); let emailPasswordUser = supertokensUsersWithSameEmail.find((u) => { return ( u.loginMethods.find((lM) => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined ); }); if (emailPasswordUser === undefined) { // User does not exist in SuperTokens // Check if the user exists in the legacy provider and retrieve their data let legacyUserData = await retrieveUserDataFromExternalProvider(email); if (legacyUserData) { // create a SuperTokens account for the user with a temporary password let tempPassword = await generatePassword(); let signupResponse = await EmailPassword.signUp( input.tenantId, email, tempPassword, undefined, input.userContext, ); if (signupResponse.status === "OK") { // If the user is successfully created, map the legacy ID to the SuperTokens ID. const mappingResponse = await SuperTokens.createUserIdMapping({ superTokensUserId: signupResponse.user.id, externalUserId: legacyUserData.user_id, userContext: input.userContext, }); if (mappingResponse.status !== "OK") { throw new Error("Legacy user ID conflicts with an existing mapping"); } // We also need to set the email verification status of the user if (legacyUserData.isEmailVerified) { // Generate an email verification token for the user let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken( input.tenantId, signupResponse.recipeUserId, email, input.userContext, ); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email await EmailVerification.verifyEmailUsingToken( input.tenantId, generateEmailVerificationTokenResponse.token, undefined, input.userContext, ); } } // We also need to identify that the user is using a temporary password. We do through the userMetadata recipe await UserMetadata.updateUserMetadata(signupResponse.user.id, { isUsingTemporaryPassword: true }); } else { throw new Error("Sign-up failed; re-read and reconcile this identity before retrying"); } } } return await originalImplementation.generatePasswordResetTokenPOST!(input); }, }; }, }, }); SuperTokens.init({ framework: "express", supertokens: { connectionURI: "" }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", }, recipeList: [emailPasswordRecipe, Session.init(), EmailVerification.init({ mode: "OPTIONAL" }), UserMetadata.init()], }); async function generatePassword(): Promise { return randomBytes(32).toString("base64url"); } async function retrieveUserDataFromExternalProvider(email: string): Promise< | { user_id: string; isEmailVerified: boolean; } | undefined > { // TODO: retrieve user data if a user with the input email exists in the external provider. return undefined; } ``` ```python check=false reason="Partial configuration example" import secrets from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) from supertokens_python.recipe import emailpassword, emailverification, usermetadata, session from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata from supertokens_python.interfaces import CreateUserIdMappingOkResult from supertokens_python.types.base import AccountInfoInput def override_emailpassword_apis(original_implementation: APIInterface): original_generate_password_reset_token_post = ( original_implementation.generate_password_reset_token_post ) async def generate_password_reset_token_post( form_fields: List[FormField], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): # retrieve the email from the form fields email = None for field in form_fields: if field.id == "email": email = field.value if email is None: raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email), False, user_context ) emailpassword_user = next( ( user for user in supertokens_user_with_same_email if any( lm.recipe_id == "emailpassword" and lm.has_same_email_as(email) for lm in user.login_methods ) ), None, ) if emailpassword_user is None: # EmailPassword user with the input email does not exist in SuperTokens # Check if the user exists in the legacy provider and retrieve their data legacy_user_data = await retrieve_user_data_from_external_provider(email) if legacy_user_data is not None: # Create a SuperTokens account for the user with a temporary password tempPassword = await generate_password() response = await sign_up( tenant_id, email, tempPassword, None, user_context ) if not isinstance(response, SignUpOkResult): raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Map the SuperTokens userId to the legacy userId mapping_response = await create_user_id_mapping( response.user.id, legacy_user_data.user_id, user_context=user_context ) if not isinstance(mapping_response, CreateUserIdMappingOkResult): raise Exception("Legacy user ID conflicts with an existing mapping") # We also need to set the email verification status if legacy_user_data.isEmailVerified: # Generate an email verification token for the user generate_email_verification_token_response = ( await create_email_verification_token( tenant_id, response.recipe_user_id, email, user_context ) ) if isinstance( generate_email_verification_token_response, CreateEmailVerificationTokenOkResult, ): # Verify the user's email await verify_email_using_token( tenant_id, generate_email_verification_token_response.token, True, user_context, ) # We also need to identify that the user is using a temporary password. We do through the userMetadata recipe await update_user_metadata( response.user.id, {"isUsingTemporaryPassword": True}, user_context ) return await original_generate_password_reset_token_post( form_fields, tenant_id, api_options, user_context ) original_implementation.generate_password_reset_token_post = ( generate_password_reset_token_post ) return original_implementation class ExternalUserInfo: def __init__(self, user_id: str, isEmailVerified: bool): self.user_id: str = user_id self.isEmailVerified: bool = isEmailVerified async def retrieve_user_data_from_external_provider( email: str, ) -> Union[None, ExternalUserInfo]: # TODO: Retrieve user data if a user with the input email exists in the external provider. return None async def generate_password() -> str: return secrets.token_urlsafe(32) init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ), emailverification.init("OPTIONAL"), usermetadata.init(), session.init(), ], ) ``` ```go import ( "crypto/rand" "encoding/base64" "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/usermetadata" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the function originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST // Override the API (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { // Retrieve email from the form fields var email *string = nil for _, field := range formFields { if field.ID == "email" { valueAsString, asStrOk := field.Value.(string) if !asStrOk { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = &valueAsString } } if email == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, *email) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if emailPasswordUser == nil { // User does not exist in SuperTokens // Check if the user exists in the legacy provider and retrieve their data legacyUserInfo := retrieveUserDataFromExternalProvider(*email) if legacyUserInfo != nil { // Create a SuperTokens account for the user with a temporary password tempPassword, err := generatePassword() if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } response, err := emailpassword.SignUp(tenantId, *email, tempPassword) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if response.OK == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") } recipeUserId := response.OK.User.ID // Map the external provider's userId to the SuperTokens userId mapping, err := supertokens.CreateUserIdMapping(recipeUserId, legacyUserInfo.userId, nil, nil) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if mapping.OK == nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("legacy user ID mapping conflicts with an existing mapping") } // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, recipeUserId, &response.OK.User.Email) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if generateEmailVerificationTokenResponse.OK != nil { // Verify the user's email _, err := emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } } } // We also need to identify that the user is using a temporary password. We do through the UserMetadata recipe _, err = usermetadata.UpdateUserMetadata(legacyUserInfo.userId, map[string]interface{}{ "isUsingTemporaryPassword": true, }) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } } } return originalGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) } return originalImplementation }, }, }), usermetadata.Init(nil), emailverification.Init(evmodels.TypeInput{Mode: evmodels.ModeOptional}), session.Init(nil), }, }) } type ExternalUserInfo struct { userId string isEmailVerified bool } func retrieveUserDataFromExternalProvider(email string) *ExternalUserInfo { // TODO: Retrieve user info from external provider if account with input email exists. return nil } func generatePassword() (string, error) { password := make([]byte, 32) if _, err := rand.Read(password); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(password), nil } ``` The code above overrides the `generatePasswordResetTokenPOST` API. This is the first step in the password reset flow and is responsible for generating the password reset token to be sent with the reset password email. - Similar to the previous step, we need to determine whether to migrate the user or not. - The next step is to create a SuperTokens account with a temporary password, the password can be a random string since it is reset by the user when they complete the reset password flow. - We now map the external `userId`(the userId from the external provider) to the SuperTokens `userId`. This allows SuperTokens functions to reference the user with the external `userId`. - Depending on the email verification status of the user in the external provider we also verify the user's email in SuperTokens. - We assign the `isUsingTemporaryPassword` flag to user's metadata since the account was generated with a temporary password. This is done to prevent sign ins until the password is successfully reset. ## Step 4) Remove the `isUsingTemporaryPassword` flag on successful password reset If the password reset flow is successfully completed we need to check if the user has `isUsingTemporaryPassword` set in their metadata and remove it if it exists. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Session from "supertokens-node/recipe/session"; const emailPasswordRecipe = EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // TODO: implementation details in previous step passwordResetPOST: async function (input) { let response = await originalImplementation.passwordResetPOST!(input); if (response.status === "OK") { let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext); if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) { // Since the password reset we can remove the isUsingTemporaryPassword flag await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null }); } } return response; }, }; }, }, }); SuperTokens.init({ framework: "express", supertokens: { connectionURI: "" }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", }, recipeList: [emailPasswordRecipe, Session.init(), UserMetadata.init()], }); ``` ```python check=false reason="Partial configuration example" from typing import Any, Dict, List from supertokens_python import InputAppInfo, init from supertokens_python.recipe import emailpassword, usermetadata, session from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, PasswordResetPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) def override_emailpassword_apis(original_implementation: APIInterface): original_password_reset_post = original_implementation.password_reset_post async def password_reset_post( form_fields: List[FormField], token: str, tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): response = await original_password_reset_post( form_fields, token, tenant_id, api_options, user_context ) if ( isinstance(response, PasswordResetPostOkResult) ): # Check that the user has the isUsingTemporaryPassword flag set in their metadata metadata_result = await get_user_metadata(response.user.id, user_context) if ( "isUsingTemporaryPassword" in metadata_result.metadata and metadata_result.metadata["isUsingTemporaryPassword"] is True ): # Since the password has been successfully reset, we can remove the isUsingTemporaryPassword flag await update_user_metadata( response.user.id, {"isUsingTemporaryPassword": None} ) return response original_implementation.password_reset_post = password_reset_post return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ), usermetadata.init(), session.init(), ], ) ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/usermetadata" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the passwordReset originalPasswordResetPOST := *originalImplementation.PasswordResetPOST // Then we override the API (*originalImplementation.PasswordResetPOST) = func(formFields []epmodels.TypeFormField, token, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.ResetPasswordPOSTResponse, error) { response, err := originalPasswordResetPOST(formFields, token, tenantId, options, userContext) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } if response.OK != nil { metadata, err := usermetadata.GetUserMetadata(*response.OK.UserId) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } isUsingTemporaryPassword, ok := metadata["isUsingTemporaryPassword"] if ok && isUsingTemporaryPassword.(bool) { // Since the password is reset we can remove the isUsingTemporaryPassword flag _, err = usermetadata.UpdateUserMetadata(*response.OK.UserId, map[string]interface{}{ "isUsingTemporaryPassword": nil, }) if err != nil { return epmodels.ResetPasswordPOSTResponse{}, err } } } return response, nil } return originalImplementation }, }, }), usermetadata.Init(nil), session.Init(nil), }, }) } ``` The code above overrides the `passwordResetPOST` API and is a continuation of the password reset flow: - On a successful password reset we check if the user has the `isUsingTemporaryPassword` flag set in their metadata and remove it If it exists. ## Step 5) Update the login flow to account for the `isUsingTemporaryPassword` flag Apart from the changes we made in Step 1, we also need to account for users who have initiated a password reset but have not completed the flow. We need to handle two cases: - Prevent sign in from accounts that have temporary passwords. - If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked. - If a user initiates a password reset but remembers their password, they should be able to sign in. - In this case the user should be able to login and the database should be updated to reflect the new password. ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import EmailVerification from "supertokens-node/recipe/emailverification"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Session from "supertokens-node/recipe/session"; const emailPasswordRecipe = EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signInPOST: async function (input) { // Check if an email-password user with the input email exists in SuperTokens let email = input.formFields.find((field) => field.id === "email")!.value as string; let password = input.formFields.find((field) => field.id === "password")!.value as string; let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo( input.tenantId, { email: email, }, undefined, input.userContext, ); let emailPasswordUser = supertokensUsersWithSameEmail.find((u) => { return ( u.loginMethods.find((lM) => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined ); }); if (emailPasswordUser === undefined) { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); if (legacyUserInfo === undefined) { // credentials are incorrect return { status: "WRONG_CREDENTIALS_ERROR", }; } // Call the signup function to create a new SuperTokens user. let signUpResponse = await EmailPassword.signUp( input.tenantId, email, password, undefined, input.userContext, ); if (signUpResponse.status !== "OK") { throw new Error("Sign-up failed; re-read and reconcile this identity before retrying"); } // Map the external provider's userId to the SuperTokens userId const mappingResponse = await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id, userContext: input.userContext, }); if (mappingResponse.status !== "OK") { throw new Error("Legacy user ID conflicts with an existing mapping"); } // We also need to set the email verification status of the user if (legacyUserInfo.isEmailVerified) { // Generate an email verification token for the user let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken( input.tenantId, signUpResponse.recipeUserId, email, input.userContext, ); if (generateEmailVerificationTokenResponse.status === "OK") { // Verify the user's email await EmailVerification.verifyEmailUsingToken( input.tenantId, generateEmailVerificationTokenResponse.token, undefined, input.userContext, ); } } emailPasswordUser = signUpResponse.user; } // Check if the user signing in has a temporary password let userMetadata = await UserMetadata.getUserMetadata(emailPasswordUser.id, input.userContext); if (userMetadata.status === "OK" && userMetadata.metadata.isUsingTemporaryPassword) { // Check if the input credentials are valid in the external provider let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); if (legacyUserInfo) { let loginMethod = emailPasswordUser.loginMethods.find( (lM) => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email), ); // Update the user's password with the correct password await EmailPassword.updateEmailOrPassword({ recipeUserId: loginMethod!.recipeUserId, password: password, applyPasswordPolicy: false, }); // Update the user's metadata to remove the isUsingTemporaryPassword flag await UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null }); } else { return { status: "WRONG_CREDENTIALS_ERROR", }; } } return originalImplementation.signInPOST!(input); }, }; }, }, }); SuperTokens.init({ framework: "express", supertokens: { connectionURI: "" }, appInfo: { appName: "", apiDomain: "", websiteDomain: "", }, recipeList: [emailPasswordRecipe, Session.init(), EmailVerification.init({ mode: "OPTIONAL" }), UserMetadata.init()], }); async function validateAndGetUserInfoFromExternalProvider( email: string, password: string, ): Promise< | { user_id: string; isEmailVerified: boolean; } | undefined > { // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. return undefined; } ``` ```python check=false reason="Partial configuration example" from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) from supertokens_python.recipe import emailpassword, emailverification, usermetadata, session from supertokens_python.recipe.emailpassword.asyncio import ( sign_up, update_email_or_password, ) from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, SignUpOkResult, WrongCredentialsError, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.emailverification.asyncio import ( create_email_verification_token, verify_email_using_token, ) from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) from supertokens_python.interfaces import CreateUserIdMappingOkResult from supertokens_python.types.base import AccountInfoInput def override_emailpassword_apis(original_implementation: APIInterface): original_emailpassword_sign_in = original_implementation.sign_in_post async def sign_in( 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], ): email = "" password = "" for field in form_fields: if field.id == "email": email = field.value if field.id == "password": password = field.value # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email), False, user_context ) emailpassword_user = next( ( user for user in supertokens_user_with_same_email if any( lm.recipe_id == "emailpassword" and lm.has_same_email_as(email) for lm in user.login_methods ) ), None, ) if emailpassword_user is None: # EmailPassword user with the input email does not exist in SuperTokens # Check if the input credentials valid in the external provider legacy_user_info = await validate_and_get_user_info_from_external_provider( email, password ) if legacy_user_info is None: # Credentials are incorrect return WrongCredentialsError() # Call the sign_up function to create a new SuperTokens user. response = await sign_up(tenant_id, email, password, None, user_context) if not isinstance(response, SignUpOkResult): raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Map the external provider's userId to the SuperTokens userId mapping_response = await create_user_id_mapping( response.user.id, legacy_user_info.user_id, user_context=user_context ) if not isinstance(mapping_response, CreateUserIdMappingOkResult): raise Exception("Legacy user ID conflicts with an existing mapping") # We also need to set the email verification status of the user if legacy_user_info.isEmailVerified: # Generate an email verification token for the user generate_email_verification_response = ( await create_email_verification_token( tenant_id, response.recipe_user_id, email, user_context, ) ) if isinstance( generate_email_verification_response, CreateEmailVerificationTokenOkResult, ): await verify_email_using_token( tenant_id, generate_email_verification_response.token, True, user_context, ) emailpassword_user = response.user # Check if the user signing in has a temporary password metadata_result = await get_user_metadata(emailpassword_user.id) if ( "isUsingTemporaryPassword" in metadata_result.metadata and metadata_result.metadata["isUsingTemporaryPassword"] is True ): # Check if the input credentials are valid in the external provider legacy_user_info = await validate_and_get_user_info_from_external_provider( email, password ) if legacy_user_info is not None: # Find the emailpassword login method for the user login_method = next( ( lm for lm in emailpassword_user.login_methods if lm.recipe_id == "emailpassword" and lm.email == email ), None, ) assert login_method is not None # Update the user's password with the correct password await update_email_or_password( login_method.recipe_user_id, None, password, False, tenant_id, user_context, ) # Update the user's metadata to remove the isUsingTemporaryPassword flag await update_user_metadata( emailpassword_user.id, {"isUsingTemporaryPassword": None} ) else: return WrongCredentialsError() return await original_emailpassword_sign_in( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_in_post = sign_in return original_implementation class ExternalUserInfo: def __init__(self, user_id: str, isEmailVerified: bool): self.user_id: str = user_id self.isEmailVerified: bool = isEmailVerified async def validate_and_get_user_info_from_external_provider( email: str, password: str ) -> Union[None, ExternalUserInfo]: return None init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) ), emailverification.init("OPTIONAL"), usermetadata.init(), session.init(), ], ) ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/usermetadata" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // Copy the original implementation of the signIn function originalSignIn := *originalImplementation.SignInPOST // Override the function (*originalImplementation.SignInPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignInPOSTResponse, error) { email := "" password := "" for _, formField := range formFields { if formField.ID == "email" || formField.ID == "password" { valueAsString, asStrOk := formField.Value.(string) if !asStrOk { return epmodels.SignInPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } if formField.ID == "email" { email = valueAsString } else { password = valueAsString } } } // Check if an email-password user with the input email exists in SuperTokens emailPasswordUser, err := emailpassword.GetUserByEmail(tenantId, email) if err != nil { return epmodels.SignInPOSTResponse{}, err } if emailPasswordUser == nil { // EmailPassword user with the input email does not exist in SuperTokens // Check if the input credentials are valid in the external provider legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo == nil { return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } // Call the email-password signup function to create a new SuperTokens user. response, err := emailpassword.SignUp(tenantId, email, password) if err != nil { return epmodels.SignInPOSTResponse{}, err } if response.OK == nil { return epmodels.SignInPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") } recipeUserId := response.OK.User.ID // Map the external provider's userId to the SuperTokens userId mapping, err := supertokens.CreateUserIdMapping(recipeUserId, legacyUserInfo.userId, nil, nil) if err != nil { return epmodels.SignInPOSTResponse{}, err } if mapping.OK == nil { return epmodels.SignInPOSTResponse{}, errors.New("legacy user ID mapping conflicts with an existing mapping") } // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { // Generate an email verification token for the user generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, recipeUserId, &email) if err != nil { return epmodels.SignInPOSTResponse{}, err } if generateEmailVerificationTokenResponse.OK != nil { // Verify the user's email _, err = emailverification.VerifyEmailUsingToken(tenantId, generateEmailVerificationTokenResponse.OK.Token) if err != nil { return epmodels.SignInPOSTResponse{}, err } } } emailPasswordUser = &response.OK.User } // Check if the user signing in has a temporary password metadata, err := usermetadata.GetUserMetadata(emailPasswordUser.ID) if err != nil { return epmodels.SignInPOSTResponse{}, err } isUsingTemporaryPassword, ok := metadata["isUsingTemporaryPassword"] if ok && isUsingTemporaryPassword.(bool) { // Check that the input credentials are valid in the external provider legacyUserInfo := validateAndGetUserInfoFromExternalProvider(email, password) if legacyUserInfo != nil { // Replace the temporary password with the valid password usePasswordPolicy := false _, err = emailpassword.UpdateEmailOrPassword(emailPasswordUser.ID, nil, &password, &usePasswordPolicy, &tenantId) if err != nil { return epmodels.SignInPOSTResponse{}, err } // Update the user's metadata to remove the isUsingTemporaryPassword flag _, err = usermetadata.UpdateUserMetadata(emailPasswordUser.ID, map[string]interface{}{ "isUsingTemporaryPassword": nil, }) if err != nil { return epmodels.SignInPOSTResponse{}, err } } else { return epmodels.SignInPOSTResponse{ WrongCredentialsError: &struct{}{}, }, nil } } return originalSignIn(formFields, tenantId, options, userContext) } return originalImplementation }, }, }), usermetadata.Init(nil), emailverification.Init(evmodels.TypeInput{Mode: evmodels.ModeOptional}), session.Init(nil), }, }) } type ExternalUserInfo struct { userId string isEmailVerified bool } func validateAndGetUserInfoFromExternalProvider(email string, password string) *ExternalUserInfo { // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. return nil } ``` The code above adds the following changes to the `signInPOST` API: - Adds an additional check where if a user exists in SuperTokens, we check if they have the `isUsingTemporaryPassword` flag set in their metadata. - If the flag exists, we check if the input credentials are valid in the external provider. If they are we update the account with the new password and continue the login flow. - If the input credentials are invalid in the external provider, we return a `WRONG_CREDENTIALS_ERROR`. ## User migration edge cases that are addressed This strategy takes into account the following edge cases to ensure a smooth migration experience: - Users who have not been migrated over to SuperTokens forgets their passwords and tries the password reset flow - In this situation, the regular password reset flow does not work since password resets require an existing account. - The changes proposed in Step 3 and Step 4 resolve the this edge case. - User starts the password reset flow and attempts to sign in with a temporary password - If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked. - The changes proposed in Step 5 allows for this flow - User starts the password reset flow but they remember their password and try to login with the valid password. - In this scenario if the user starts the password reset flow, a new SuperTokens account with a temporary password is created. Instead of completing the password reset flow they remember their password and try to sign in. In this case the user should be able to successfully sign in and the account should be updated with the valid password. - The changes proposed in Step 5 allows for this flow. ## When can I stop using my legacy authentication provider? Your migration period could be decided by either of the following factors: - A time window (2-3 months) within which "in-time" migration is active. - A user migration threshold where, after a certain percentage of the user base is migrated, the migration period ends. After the migration period ends you have to make the following changes to stop automatic user migration: - Remove all migration related override changes in your backend. - Take the remaining users' emails and call the sign up function with a secure randomized password. - Email users encouraging them to go through the password reset flow. --- # User Creation Source: https://supertokens.com/docs/migration/legacy/account-creation/user-creation ## Email Password Migration :::caution[Legacy procedure] This page documents version-qualified CDI 2.16 behavior. For a new migration, use the current [account migration guide](/migration/account-migration), which supports staged bulk import and reconciliation. ::: :::danger[Secure the legacy Core before importing] Keep Core on a private network; for a same-host migration, bind the published port to the local interface only. Generate a high-entropy API key (for example, `openssl rand -hex 32`), store it in your secret manager, and configure the same key in Core and the migration client without putting it in source control, shell history, or logs. Pin the Core/database image to a tested immutable digest that implements the documented CDI/storage version. Never use an untagged or mutable image for a migration, and do not expose Core directly to the internet. The requests below read their API-key header from `/run/secrets/supertokens-curl.conf`. Create it with mode `0600` and the line `header = "api-key: "`; provision it from your secret manager and disable shell/curl tracing. ::: :::note If you do not have access to your user's password hashes, you can use our [guide for migrating them dynamically during login](./ep-migration-without-password-hash). ::: SuperTokens allows you to import users with password hashes generated with `BCrypt`, `Argon2` and `Firebase SCrypt` with our import user API. You can find the API spec [here](https://app.swaggerhub.com/apis/supertokens/CDI/2.16.0#/EmailPassword%20Recipe/userImport). ### Migrating users with Argon2 or `BCrypt` password hashes For users with `BCrypt` or `Argon2` password hashes you can use the following curl command to import your user. ```bash curl --location --request POST '/recipe/user/passwordhash/import' \ --config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@example.com", "passwordHash": "$argon2d$v=19$m=12,t=3,p=1$NWd0eGp4ZW91b3IwMDAwMA$57jcfXF19MyiUXSjkVBpEQ" }' ``` :::note[SuperTokens accepts `BCrypt` and `Argon2` hashes in standard format. When exporting password hashes from authentication providers the structure might be changed. For example, Auth0 adds an identifier to the exported password hashes which needs to be removed before importing into SuperTokens.] Sample password hashes for `BCrypt` and Argon2 in standard format: - `BCrypt`: `$2a$10$GzEm3vKoAqnJCTWesRARCe/ovjt/07qjvcH9jbLUg44Fn77gMZkmm` - Argon2: `$argon2id$v=19$m=16,t=2,p=1$VG1Oa1lMbzZLbzk5azQ2Qg$kjcNNtZ/b0t/8HgXUiQ76A` ::: ### Migrating users with Firebase `SCrypt` Password hashes Importing users from Firebase requires an update to your SuperTokens Core configuration and formatting the input password hash. #### Step 1: Retrieve your Firebase password hashing parameters from your dashboard. Firebase password hashing details modal #### Step 2: Update the SuperTokens core to use the `base64_signer_key` **For Managed Service** - Edit the core configuration on the **Configuration** page of the relevant deployment in the SuperTokens SaaS Dashboard. - Set the `firebase_password_hashing_signer_key` field in the config to the `base64_signer_key` retrieved from your firebase hashing parameters. **With Docker:** Create `/run/secrets/supertokens-migration.environment` with mode `0600`. It must contain `API_KEYS=` and `FIREBASE_PASSWORD_HASHING_SIGNER_KEY=`. Use deployment-managed secrets instead of an environment file where available. **Without Docker:** Restrict the Core listener to a private or local-only interface with host firewall/network policy. The exact process binding is deployment-specific; verify from another host that port `3567` is unreachable before importing. ```bash docker run \ --env-file /run/secrets/supertokens-migration.environment \ -p 127.0.0.1:3567:3567 \ -d supertokens/supertokens-@sha256: ``` ```yaml # Add your base64_signer_key to the following in the config.yaml file. # The file path can be found by running the "supertokens --help" command firebase_password_hashing_signer_key: "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXdWscZx0l2WbCJ1wbg==" api_keys: "" ``` #### Step 3: SuperTokens requires firebase password hashes to be in a specific format to be parsed. For example: Your exported firebase user has the following credentials: ```json { "users": [ { "localId": "userId", "email": "johnDoe@example.com", "passwordHash": "9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==", "salt": "/cj0jC1br5o4+w==" } ] } ``` The memory cost, rounds and salt separator retrieved from the password hashing config are: ```json { "mem_cost": 14, "rounds": 8, "base64_salt_separator": "Bw==" } ``` The password hash would be the following: `$f_scrypt$9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==$/cj0jC1br5o4+w==$m=14$r=8$s=Bw==` The example password hash is in the following format `$f_scrypt$$$m=$r=$s=` #### Step 4: Run the following `curl` command to import the user ```bash curl --location --request POST '/recipe/user/passwordhash/import' \ --config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "test@example.com", "passwordHash": "$f_scrypt$9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==$/cj0jC1br5o4+w==$m=14$r=8$s=Bw==", "hashingAlgorithm": "firebase_scrypt" }' ``` ## Passwordless Migration This legacy procedure is an active passwordless authentication flow, not a side-effect-free import. Generating a code creates temporary passwordless device/code records with an expiry. Consuming the link code consumes that credential and performs passwordless sign-in/up, creating the user if necessary. Repeating or racing these requests can produce used, expired, or duplicate-flow errors. Run them only in a controlled migration process, keep returned codes secret, and reconcile the resulting user before retrying. Use bulk import for new migrations. ### Generate passwordless code **With Email** ```bash curl --location --request POST '/recipe/signinup/code' \ --config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@example.com" }' ``` **With Phone Number** ```bash curl --location --request POST '/recipe/signinup/code' \ --config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+14155552671" }' ``` On successfully generating the passwordless code you should see the following response ```json { "status": "OK", "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", "codeId": "4fe93f8e-a5da-4588-82e2-314c6993b345", "deviceId": "+cWm1Y2EFxEPyHM7CAwYyAdkakBeoEDm6IOGT3xfa1U=", "userInputCode": "463152", "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=", "timeCreated": 1664283193059, "codeLifetime": 900000 } ``` ### Consume the passwordless code to create the passwordless user Retrieve the `preAuthSessionId` and `linkCode` from the previous response and set them as request body parameters for the consume code request. ```bash curl --location --request POST '/recipe/signinup/code/consume' \ --config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=" }' ``` If the imported passwordless login method should have both an email address and phone number, use its returned user ID to add the missing contact information: ```bash curl --location --request PUT '/recipe/user' \ --config /run/secrets/supertokens-curl.conf \ --header 'rid: passwordless' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "email": "johndoe@gmail.com", "phoneNumber": "+14155552671" }' ``` ## ThirdParty Migration To migrate users with social accounts we can simply call the SuperTokens Core's `signInUp` API with the provider Id and the user's third party userId. For example: If we were importing a user with Google as their provider with their third party userId being `106347997792363870000`, we can run the following curl command to import the user. ```bash curl --location --request POST '/recipe/signinup' \ --config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", "thirdPartyUserId": "106347997792363870000", "email": { "id": "johndoe@gmail.com", "isVerified": true } }' ``` --- ## See also --- # UserId Mapping Source: https://supertokens.com/docs/migration/legacy/account-creation/user-id-mapping UserId Mapping allows you to map existing userIds (from your old auth provider) to the SuperTokens userIds. This prevents you from having to update the existing `userIDs` in your application's table. As an example, if after creating the user in SuperTokens, their userId is `fa7a0841-b533-4478-95533-0fde890c3483` and the existing userId for that user is `customUserId`, then you can map these user IDs by calling the following API: ```bash curl --location --request POST '/recipe/userid/map' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "superTokensUserId": "fa7a0841-b533-4478-95533-0fde890c3483", "externalUserId": "customUserId" }' ``` Now whenever this user signs in, or if you fetch information about this user from SuperTokens, their userID will be `customUserId`. :::info[Note] The maximum allowed size of the `externalUserId` is 128 characters. ::: --- # Step 2. User Data Migration Source: https://supertokens.com/docs/migration/legacy/data-migration Once your user accounts have been migrated over to SuperTokens, additional information like metadata, roles and permissions can be associated with the user. ## User Metadata Migration SuperTokens allows you to store arbitrary data that is JSON serializable against a userId. In this example we want to store the following metadata against our user: ```json { "someKey": "someValue" } ``` ```bash curl --location --request PUT '/recipe/user/metadata' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "5acb2dbc-04f0-4c80-822a-7f06cd658f6f", "metadataUpdate": { "someKey": "someValue" } }' ``` ## User Roles Migration SuperTokens allows you to assign roles and permissions to a userId. In this example we will be assigning the `admin` role to a user: ```bash curl --location --request PUT '/recipe/user/role' \ --header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "admin", "userId": "5acb2dbc-04f0-4c80-822a-7f06cd658f6f" }' ``` :::info[Important] Roles and permissions must be created before they can be assigned to a user. You can follow this [guide](/additional-verification/user-roles/initial-setup) on creating roles and permissions in SuperTokens. ::: --- ## See also --- # Step 4. MFA migration Source: https://supertokens.com/docs/migration/legacy/mfa-migration If you are using MFA in your app, checkout the MFA migration section [here](/additional-verification/mfa/migration/legacy-to-new) after you have gone through the previous steps in migration. --- # Step 3. Session Migration Source: https://supertokens.com/docs/migration/legacy/session-migration This section explains how to migrate user sessions from your previous authentication provider to SuperTokens. This process involves two steps. - Adding a new `/migrate-session` API to your backend which will create a new SuperTokens session - Calling the `/migrate-session` API on your frontend to create a new SuperTokens session and revoke your old session. ## Flow Session migration flow chart ### Backend changes Create a rate-limited backend API that exchanges a valid legacy access token for a SuperTokens session. The following example uses the APIs released in SuperTokens Node SDK 24.0.3. ```tsx title="Backend changes" check=false reason="Requires surrounding framework application context" import express from "express"; import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; const app = express(); app.use(express.json()); interface VerifiedLegacyToken { issuer: string; subject: string; tenantId: string; } app.post("/migrate-session", migrationRateLimiter, async (req, res, next) => { const match = req.headers.authorization?.match(/^Bearer[ \t]+([^\s,]+)$/i); const idempotencyKey = req.header("Idempotency-Key"); if (match === null || match === undefined || !isValidIdempotencyKey(idempotencyKey)) { res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); return; } let verifiedToken: VerifiedLegacyToken; try { verifiedToken = await verifyLegacyAccessToken(match[1]); } catch { res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); return; } try { const identity = getNamespacedLegacyIdentity(verifiedToken); await enforceVerifiedIdentityRateLimit(identity.identityKey); const mapping = await SuperTokens.getUserIdMapping({ userId: identity.externalUserId, userIdType: "EXTERNAL", }); if (mapping.status !== "OK") { res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); return; } const recipeUserId = SuperTokens.convertToRecipeUserId(mapping.superTokensUserId); const result = await migrateSessionIdempotently( { idempotencyKey, identityKey: identity.identityKey, tenantId: verifiedToken.tenantId, recipeUserId: recipeUserId.getAsString(), }, () => Session.createNewSession(req, res, verifiedToken.tenantId, recipeUserId), ); if (result.status === "CONFLICT") { res.status(409).send({ status: "MIGRATION_CONFLICT" }); return; } if (result.status === "IN_PROGRESS") { res.set("Retry-After", "1").status(409).send({ status: "MIGRATION_IN_PROGRESS" }); return; } res.send({ status: result.status }); } catch (error) { next(error); } }); app.post("/confirm-session-migration", migrationRateLimiter, async (req, res, next) => { const idempotencyKey = req.body?.idempotencyKey; if (!isValidIdempotencyKey(idempotencyKey)) { res.status(400).send({ status: "INVALID_IDEMPOTENCY_KEY" }); return; } try { const session = await Session.getSession(req, res); const confirmed = await confirmMigrationOutcome({ idempotencyKey, tenantId: session.getTenantId(), recipeUserId: session.getRecipeUserId().getAsString(), }); res.status(confirmed ? 200 : 409).send({ status: confirmed ? "CONFIRMED" : "IDENTITY_MISMATCH" }); } catch (error) { next(error); } }); function isValidIdempotencyKey(value: unknown): value is string { return typeof value === "string" && /^[A-Za-z0-9_-]{32,128}$/.test(value); } declare function migrationRateLimiter(req: express.Request, res: express.Response, next: express.NextFunction): void; declare function enforceVerifiedIdentityRateLimit(identityKey: string): Promise; declare function getNamespacedLegacyIdentity(verifiedToken: VerifiedLegacyToken): { identityKey: string; externalUserId: string; }; declare function migrateSessionIdempotently( input: { idempotencyKey: string; identityKey: string; tenantId: string; recipeUserId: string; }, createSession: () => ReturnType, ): Promise<{ status: "CREATED" | "RECOVERED" | "CONFLICT" | "IN_PROGRESS" }>; declare function confirmMigrationOutcome(input: { idempotencyKey: string; tenantId: string; recipeUserId: string; }): Promise; // Implement this contract with your provider's supported SDK or a JWT library configured for that provider. declare function verifyLegacyAccessToken(accessToken: string): Promise; ``` :::info[Important] `verifyLegacyAccessToken` is an application-owned security boundary, not a provider-independent implementation. It must verify the signature with the provider's trusted algorithms and keys, and validate the exact issuer, audience, expiry, not-before time, token type or purpose, and required subject claims. If the provider uses JWKS, use HTTPS, cache keys, and refresh them safely when keys rotate. Derive `tenantId` from trusted application configuration and validated claims, never directly from request data that has not been verified. `getNamespacedLegacyIdentity` must use the same collision-resistant canonical encoding used during account import. Its external ID and identity key must include the validated issuer, trusted tenant, and subject. Do not concatenate ambiguous strings or map by subject alone: subjects are only unique within an issuer and can also overlap between tenants. ::: Configure CORS on the backend with the exact frontend origin, `credentials: true`, and the `Authorization`, `Content-Type`, and `Idempotency-Key` request headers explicitly allowed. Do not combine credentialed requests with `Access-Control-Allow-Origin: *`. The endpoint's error handler should return a generic 401 for verification failures without exposing token-validation details. Implement `migrateSessionIdempotently` with a distributed outcome store indexed uniquely by both the stable request key and the legacy identity scoped to its issuer and tenant. A completed outcome records the tenant, recipe user ID, and created session handle. A retry for the same key and identity must return or safely recover that logical outcome; if the original session was not delivered, revoke it before issuing a replacement. Reject a key bound to another identity, another key for an already migrated identity, and concurrent in-progress exchanges. Never store the raw legacy token. Keep completed outcomes at least until the legacy session and token can no longer be accepted. `confirmMigrationOutcome` must compare the current SuperTokens tenant and recipe user ID with that stored outcome and atomically mark it confirmed. ### Frontend changes On page load, obtain the legacy token and its stable request key. If a SuperTokens session exists, confirm that it matches the stored migration outcome before cleaning up the legacy session. Otherwise, perform the idempotent exchange, then confirm the identities. Never treat an HTTP success alone as proof that the current SuperTokens and legacy identities match. The example uses SuperTokens Web JS 0.16.0 and assumes that the SDK is initialized. ```tsx title="Frontend changes" import axios from "axios"; import Session from "supertokens-web-js/recipe/session"; // Call this function on page load async function migrateUserSessions() { const apiDomain = "..."; const accessToken = await getAccessTokenFromOldProvider(); if (accessToken === undefined) { return; } const idempotencyKey = await getOrCreateMigrationIdempotencyKey(); if (!(await Session.doesSessionExist())) { await axios.post( `${apiDomain}/migrate-session`, {}, { headers: { Authorization: `Bearer ${accessToken}`, "Idempotency-Key": idempotencyKey, }, withCredentials: true, }, ); } if (!(await confirmMigratedIdentity(apiDomain, idempotencyKey))) { return; } await revokeSessionFromOldProvider(); await clearMigrationIdempotencyKey(); } async function confirmMigratedIdentity(apiDomain: string, idempotencyKey: string): Promise { try { const response = await axios.post( `${apiDomain}/confirm-session-migration`, { idempotencyKey }, { withCredentials: true }, ); return response.data.status === "CONFIRMED"; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 409) { return false; } throw error; } } async function getAccessTokenFromOldProvider(): Promise { // Return the provider's access token when its session exists, or undefined otherwise. return "..."; } // Persist one random key for this specific legacy provider session until cleanup succeeds. declare function getOrCreateMigrationIdempotencyKey(): Promise; declare function clearMigrationIdempotencyKey(): Promise; async function revokeSessionFromOldProvider() { // Revoke the session associated with the previous provider } ``` --- ## See also --- # Overview Source: https://supertokens.com/docs/migration/overview The **Migration** section covers instructions on how you can move your authentication data from your current authentication solution to **SuperTokens** --- ## Before You Start The migration steps are intended to be executed after you have configured your [initial **SuperTokens** integration](/quickstart). Hence, to complete any of the next instructions you will need the following: - An existing application that has a working **SuperTokens** integration. You can follow the [quickstart guide](/quickstart) for instructions on how to achieve this. - A **SuperTokens** managed service account or a self-hosted **SuperTokens Core** instance. More specifically, you will have to make HTTP requests to your `CORE_API_ENDPOINT` using an `API_KEY`. ## Migration Steps The entire migration process can be broken down into two steps. Both are required to achieve a seamless transition. 1. [**Account Migration**](/migration/account-migration/) This is the main part of the migration flow. You will be importing your users, together with their account credentials, from the legacy authentication provider into **SuperTokens**. 2. [**Session Migration**](/migration/session-migration) This step prevents any users with active sessions from experiencing authentication issues. You will extend the **SuperTokens** functionality to create new sessions for users that have authenticated using your legacy provider. :::info[You need to keep using your legacy authentication provider until you have implemented all the migration steps.] After that, you can go ahead and switch to **SuperTokens** in your production application. ::: With that in mind, you can now move to the first step, the [**Account Migration**](/migration/account-migration/) process. --- # Migration Steps Source: https://supertokens.com/docs/migration/rownd/migration-steps Move supported Rownd users, sessions, and compatible authentication flows to **SuperTokens** with an assisted, project-specific migration. --- ## Overview Before going into the migration flow, you need to understand how the **Rownd** and **SuperTokens** architectures differ. Unlike with other providers, the **SuperTokens Client SDKs** never talk to the Authentication Service directly. All the requests target your existing backend where you integrate our server SDKs. Those in turn expose the authentication routes and communicate with the **SuperTokens Core** service which provides user and session storage. Compared to Rownd, in SuperTokens you have to: - Integrate a separate backend SDK into your application - Configure authentication methods and customizations through code configuration passed to the SDKs - Setup a separate backend plugin that ensures compatibility between the legacy Rownd APIs and the SuperTokens functionality ## Migration steps Each step of the migration process requires an existing SuperTokens backend integration in your application. The following steps give you an overview over the process. For detailed instructions on how to integrate SDKs you can read [the full guide](/migration/rownd/sdk-integration-guide). :::info The migration flow is not designed to be self service. Please get in touch with the [SuperTokens team](mailto:support@supertokens.com) for assistance during the whole process. ::: ## Checkpoints and rollback plan Agree on these gates with the SuperTokens team before changing production traffic. Record evidence and an owner for each gate; do not advance on partial success. 1. **Baseline checkpoint:** Export a time-stamped inventory of expected Rownd users and enabled identity providers. Record current client/plugin versions, routing, OAuth configuration, and session behavior. Keep the existing Rownd deployment and configuration available for rollback. 2. **Lazy-migration gate:** Deploy only to a controlled cohort. Verify successful and rejected legacy-token session bootstrap, sign-in/up, sign-out, refresh, profile reads/writes, and expected compatibility claims. Confirm failures do not create duplicate SuperTokens users or sessions before expanding the cohort. 3. **Bulk checkpoint:** Freeze or account for writes during the export boundary. Reconcile every expected Rownd user to exactly one intended SuperTokens identity, including tenant/provider mapping. Classify every missing, duplicate, failed, and retried record; a total count alone cannot pass this gate. 4. **Cutover gate:** Capture a final delta and reconciliation report, stop configuration changes, and define the exact traffic switch and rollback deadline. Test both newly created SuperTokens sessions and supported migrated legacy sessions before increasing traffic gradually. 5. **Post-cutover checkpoint:** Monitor authentication errors, session bootstrap/refresh failures, duplicate identities, and reconciliation drift. Keep Rownd authoritative and reversible until the agreed observation window and all acceptance checks pass. If any gate fails, stop the migration, route traffic back to the previously verified Rownd clients/backend path, and stop new writes to the SuperTokens migration path while investigating. Do not copy SuperTokens-only writes back into Rownd without a separately tested reverse-data contract. Instead, preserve the failed-state evidence, identify writes made after the checkpoint, and have the migration owners decide whether to replay them after correction. Revoke migration credentials and retire Rownd only after final identity, provider, session, and failed-record reconciliation is approved. ### 1. Lazy migration For released lazy-migration behavior, update supported Rownd React, iOS, and Android clients to the versions selected with the SuperTokens team. Do not assume that every Rownd client or version implements this behavior. After a successful sign-up, a supported SDK calls the migration endpoint exposed by the compatibility plugin. This phase reduces the export-to-cutover gap, but does not prove that every user was migrated. Reconcile it with the bulk snapshot and failed records because users can be created between export and cutover. ### 2. Bulk migration This step is performed by the SuperTokens team. Once you have deployed the updated Rownd client libraries and confirmed that the migration works, you can start the next step. The plugin provides a released paginated Rownd-user migration path. Your migration plan must still prove that the project-specific export snapshot covered every expected user and that every failed or retried record was reconciled; deployment of the plugin alone does not prove completeness. ### 3. Cutover Replace the Rownd client SDKs with the SuperTokens Rownd-compatible clients. The [SDK integration guide](/migration/rownd/sdk-integration-guide) goes into detail on how to do this. After cutover, authentication traffic goes through SuperTokens while your application uses Rownd-compatible APIs. #### Compatibility model Released compatibility clients preserve parts of the Rownd-facing integration surface, depending on platform and version. Verified implementations expose APIs including `requestSignIn()`, `signOut()`, `getAccessToken()`, profile management, and signed-in state; released backend plugins preserve Rownd identity metadata and compatibility session claims. This is not a no-change guarantee: cutover requires backend/plugin deployment, client package changes, and may require platform upgrades or OAuth reauthorization. Confirm every API, UI, claim, and platform your application uses in a staging environment before cutover. ## Next steps Continue with the [SDK Integration Guide](/migration/rownd/sdk-integration-guide). The backend plugin must be deployed before clients can migrate sessions or use SuperTokens-backed Rownd flows. After the backend is working, use the same guide to configure your frontend or mobile client platform. --- # SDK Integration Guide Source: https://supertokens.com/docs/migration/rownd/sdk-integration-guide Configure the SuperTokens Rownd backend plugin and frontend SDKs to migrate users, create SuperTokens sessions, and keep using Rownd-style APIs. --- ## Overview This tutorial configures the backend and client SDKs used during the Rownd migration. By the end, your backend exposes Rownd-compatible plugin routes, your frontend or mobile app uses the SuperTokens Rownd-compatible Hub, and OAuth/OIDC clients can be migrated if your Rownd app uses them. ## Before you start These instructions assume that you have already created an account in [the SuperTokens SaaS Dashboard](https://supertokens.com/dashboard) and have deployed a SuperTokens Core service. After you have done that, select the relevant **Managed** deployment, enable **Account Linking** from **Features**, and copy the core connection information from **Overview**. :::info The Rownd compatibility plugin is only available with NodeJS or Python at the moment. If your main backend uses another language or unsupported framework, deploy the NodeJS or Python backend as an authentication sidecar and route Rownd/SuperTokens auth traffic to it. Read [the complete guide](/references/backend-sdks/other-frameworks) for more information on how to set it up. ::: ## Steps ### 1. Configure the backend SDK #### 1.1 Install the SuperTokens SDK and Rownd plugin Install the base SuperTokens backend SDK together with the Rownd migration plugin. The SuperTokens SDK adds the auth middleware, recipe APIs, and session handling. The Rownd plugin adds the Rownd-compatible migration, Hub, profile, and OAuth compatibility routes. #### 1.1 Install the SuperTokens SDK and Rownd plugin Install the base SuperTokens Python SDK together with the Rownd migration plugin from [PyPI](https://pypi.org/project/supertokens-rownd/). ```bash npm install supertokens-node @supertokens-plugins/rownd-nodejs ``` ```bash yarn add supertokens-node @supertokens-plugins/rownd-nodejs ``` ```bash pnpm add supertokens-node @supertokens-plugins/rownd-nodejs ``` ```bash bun add supertokens-node @supertokens-plugins/rownd-nodejs ``` ```bash pip install supertokens-python supertokens-rownd ``` ```bash uv add supertokens-python supertokens-rownd ``` #### 1.2 Initialize SuperTokens Initialize the recipes that map to your Rownd auth methods, then add the Rownd plugin under `experimental.plugins`. :::info[Contact the SuperTokens team before finalizing this setup for a complete plugin `appConfig` object based on your existing Rownd configuration.] ::: The setup has four parts: - `supertokens`: connects the backend SDK to SuperTokens Core. - `appInfo`: defines the public API and website domains used by SuperTokens and the Rownd Hub. - `recipeList`: enables the SuperTokens recipes used to replace Rownd auth behavior. - `experimental.plugins`: mounts the Rownd migration plugin routes under `apiBasePath`. #### 1.2 Initialize SuperTokens Python plugin configuration must include `api_base_path`, `api_domain`, `website_domain`, and `app_name` explicitly. Keep these values in sync with `InputAppInfo`. :::info Contact the SuperTokens team before finalizing this setup for a complete plugin `app_config` object based on your existing Rownd configuration. ::: The setup has four parts: - `supertokens_config`: connects the backend SDK to SuperTokens Core. - `app_info`: defines the public API and website domains used by SuperTokens and the Rownd Hub. - `recipe_list`: enables the SuperTokens recipes used to replace Rownd auth behavior. - `experimental.plugins`: mounts the Rownd migration plugin routes under `api_base_path`. ```ts import SuperTokens from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import EmailVerification from "supertokens-node/recipe/emailverification"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import RowndMigrationPlugin from "@supertokens-plugins/rownd-nodejs"; SuperTokens.init({ supertokens: { connectionURI: process.env.SUPERTOKENS_CONNECTION_URI!, apiKey: process.env.SUPERTOKENS_API_KEY, }, appInfo: { appName: "My App", apiDomain: "", websiteDomain: process.env.WEBSITE_DOMAIN!, apiBasePath: "", }, recipeList: [ AccountLinking.init({}), Session.init(), OAuth2Provider.init(), UserMetadata.init(), Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", flowType: "MAGIC_LINK", }), EmailVerification.init({ mode: "OPTIONAL" }), ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "google", clients: [ { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, ], }, }, { config: { thirdPartyId: "apple", clients: [ { // Browser/Hub Apple login uses the Apple Services ID. clientType: "web", clientId: process.env.APPLE_WEB_CLIENT_ID!, clientSecret: process.env.APPLE_CLIENT_SECRET!, }, { // Native iOS Apple login returns authorization codes for the app bundle ID. clientType: "ios", clientId: process.env.APPLE_IOS_BUNDLE_ID!, clientSecret: process.env.APPLE_CLIENT_SECRET!, }, ], }, }, ], }, }), ], experimental: { plugins: [ RowndMigrationPlugin.init({ rowndAppKey: process.env.ROWND_APP_KEY!, rowndAppSecret: process.env.ROWND_APP_SECRET!, enableDebugLogs: process.env.ROWND_ENABLE_DEBUG_LOGS === "true", clientDomains: { browser: process.env.WEBSITE_DOMAIN!, browser_local: "http://localhost:3000", mobile: "https://my-app.rownd-hub.supertokens.com", }, appConfig: { id: process.env.ROWND_APP_KEY!, name: "My App", signInMethods: [ { method: "email" }, { method: "phone" }, { method: "google", clientId: process.env.GOOGLE_CLIENT_ID }, { method: "apple", clientId: process.env.APPLE_WEB_CLIENT_ID, // These map Rownd platforms to the SuperTokens Apple clients above. webClientType: "web", iosClientType: "ios", }, { method: "anonymous", type: "guest", displayName: "Continue as guest" }, ], profile: { accountInformation: { methods: { email: { enabled: true }, phone: { enabled: true }, google: { enabled: true }, apple: { enabled: true }, }, }, personalInformation: { enabled: true }, preferences: { enabled: true }, signOutButton: { enabled: true }, deleteAccountButton: { enabled: true }, }, }, }), ], }, }); ``` ```python from supertokens_python import ( InputAppInfo, SupertokensConfig, SupertokensExperimentalConfig, init, ) from supertokens_python.recipe import ( accountlinking, emailverification, oauth2provider, passwordless, session, thirdparty, usermetadata, ) from supertokens_python.recipe.thirdparty import ProviderClientConfig, ProviderConfig, ProviderInput from supertokens_rownd import init as rownd_init from supertokens_rownd.types import RowndPluginConfig API_BASE_PATH = "" API_DOMAIN = "" WEBSITE_DOMAIN = "https://app.example.com" init( app_info=InputAppInfo( app_name="My App", api_domain=API_DOMAIN, website_domain=WEBSITE_DOMAIN, api_base_path=API_BASE_PATH, ), framework="fastapi", mode="asgi", supertokens_config=SupertokensConfig( connection_uri="", api_key="", ), recipe_list=[ accountlinking.init(), session.init(), oauth2provider.init(), usermetadata.init(), passwordless.init( contact_config=passwordless.ContactEmailOrPhoneConfig(), flow_type="MAGIC_LINK", ), emailverification.init(mode="OPTIONAL"), thirdparty.init( sign_in_and_up_feature=thirdparty.SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="google", clients=[ ProviderClientConfig( client_id="", client_secret="", ) ], ) ), ProviderInput( config=ProviderConfig( third_party_id="apple", clients=[ # Browser/Hub Apple login uses the Apple Services ID. ProviderClientConfig( client_type="web", client_id="", client_secret="", ), # Native iOS Apple login returns authorization codes for the app bundle ID. ProviderClientConfig( client_type="ios", client_id="", client_secret="", ), ], ) ) ] ) ), ], experimental=SupertokensExperimentalConfig( plugins=[ rownd_init( RowndPluginConfig( rownd_app_key="", rownd_app_secret="", api_base_path=API_BASE_PATH, api_domain=API_DOMAIN, website_domain=WEBSITE_DOMAIN, app_name="My App", client_domains={ "browser": WEBSITE_DOMAIN, "browser_local": "http://localhost:3000", "mobile": "https://my-app.rownd-hub.supertokens.com", }, app_config={ "id": "", "name": "My App", "signInMethods": [ {"method": "email"}, {"method": "phone"}, {"method": "google", "clientId": ""}, { "method": "apple", "clientId": "", # These map Rownd platforms to the SuperTokens Apple clients above. "webClientType": "web", "iosClientType": "ios", }, {"method": "anonymous", "type": "guest", "displayName": "Continue as guest"}, ], }, ) ) ] ), ) ``` #### 1.3 Add CORS and middleware Install SuperTokens middleware after CORS handling. :::note[- Add the `middleware` BEFORE all your routes.] - Add the `cors` middleware BEFORE the SuperTokens middleware as shown below. ::: #### 1.3 Add CORS and middleware For FastAPI, use the `get_middleware()` and `get_all_cors_headers()` functions as shown below. ```ts import express from "express"; import cors from "cors"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/express"; const app = express(); app.use( cors({ origin: process.env.WEBSITE_DOMAIN, allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }), ); // IMPORTANT: CORS should be before this line. app.use(middleware()); // ...your API routes ``` ```python from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from supertokens_python import get_all_cors_headers from supertokens_python.framework.fastapi import get_middleware app = FastAPI() app.add_middleware(get_middleware()) # TODO: Add APIs app.add_middleware( CORSMiddleware, allow_origins=[ "https://app.example.com" ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) # TODO: start server ``` #### 1.4 Configure client domains By default, magic links are constructed using the `websiteDomain` that you pass in the SDK configuration. To test locally or to route the user to a mobile deep linked domain you can use the `clientDomains` plugin option. #### 1.4 Configure client domains By default, magic links are constructed using the `website_domain` that you pass in the SDK configuration. To test locally or to route the user to a mobile deep linked domain you can use the `client_domains` plugin option. ```ts check=false reason="Requires earlier Rownd setup context" RowndMigrationPlugin.init({ rowndAppKey: process.env.ROWND_APP_KEY!, rowndAppSecret: process.env.ROWND_APP_SECRET!, clientDomains: { browser: "https://app.example.com", browser_local: "http://localhost:3000", mobile: "https://my-app.rownd-hub.supertokens.com", }, }); ``` ```python check=false reason="Requires earlier Rownd setup context" RowndPluginConfig( rownd_app_key="", rownd_app_secret="", client_domains={ "browser": "https://app.example.com", "browser_local": "http://localhost:3000", "mobile": "https://my-app.rownd-hub.supertokens.com", }, ) ``` The client sends a `clientDomain` key, not a URL. The plugin looks up the key in `clientDomains` and rewrites links to that base URL. If no explicit key is sent: - Mobile Hub flows use `clientDomains.mobile`. - Browser Hub flows use `clientDomains.browser`. - If the selected key is missing, the plugin keeps the link on the Hub URL and only rewrites the path. The client sends a `clientDomain` key, not a URL. The plugin looks up the key in `client_domains` and rewrites links to that base URL. If no explicit key is sent: - Mobile Hub flows use `client_domains["mobile"]`. - Browser Hub flows use `client_domains["browser"]`. - If the selected key is missing, the plugin keeps the link on the Hub URL and only rewrites the path. #### 1.5 Configure Apple login for iOS (optional) If your iOS app uses native Sign in with Apple, configure Apple as multiple SuperTokens clients. Browser and Hub Apple login use the Apple Services ID, but native iOS Apple login returns authorization codes for your app bundle ID. The iOS bundle ID must therefore be configured as a separate Apple client in the SuperTokens backend SDK. The Rownd plugin maps the Apple sign-in method to those SuperTokens client types. The plugin setting is `iosClientType`, which becomes `ios_client_type` in the Rownd app config. The iOS SDK reads that value and sends it as `clientType` when it exchanges the Apple authorization code with `/signinup`. ```ts check=false reason="Requires earlier Rownd setup context" ThirdParty.init({ signInAndUpFeature: { providers: [ { config: { thirdPartyId: "apple", clients: [ { // Browser/Hub Apple login uses the Apple Services ID. clientType: "web", clientId: process.env.APPLE_WEB_CLIENT_ID!, clientSecret: process.env.APPLE_CLIENT_SECRET!, }, { // Native iOS Apple login returns authorization codes for the app bundle ID. clientType: "ios", clientId: process.env.APPLE_IOS_BUNDLE_ID!, clientSecret: process.env.APPLE_CLIENT_SECRET!, }, ], }, }, ], }, }); RowndMigrationPlugin.init({ rowndAppKey: process.env.ROWND_APP_KEY!, rowndAppSecret: process.env.ROWND_APP_SECRET!, appConfig: { signInMethods: [ { method: "apple", clientId: process.env.APPLE_WEB_CLIENT_ID, // These map Rownd platforms to the SuperTokens Apple clients above. webClientType: "web", iosClientType: "ios", }, ], }, }); ``` ```python check=false reason="Requires earlier Rownd setup context" thirdparty.init( sign_in_and_up_feature=thirdparty.SignInAndUpFeature( providers=[ ProviderInput( config=ProviderConfig( third_party_id="apple", clients=[ # Browser/Hub Apple login uses the Apple Services ID. ProviderClientConfig( client_type="web", client_id="", client_secret="", ), # Native iOS Apple login returns authorization codes for the app bundle ID. ProviderClientConfig( client_type="ios", client_id="", client_secret="", ), ], ) ) ] ) ) RowndPluginConfig( rownd_app_key="", rownd_app_secret="", app_config={ "signInMethods": [ { "method": "apple", "clientId": "", # These map Rownd platforms to the SuperTokens Apple clients above. "webClientType": "web", "iosClientType": "ios", } ], }, ) ``` If Android uses a separate Apple client, add another SuperTokens Apple client with `clientType: "android"` and set `androidClientType: "android"` on the Rownd Apple sign-in method. ### 2. Configure the frontend SDK After the backend plugin is deployed and reachable, configure each client application to use the SuperTokens Rownd-compatible Hub. Every client needs the same values configured on the backend: - `appKey`: the Rownd app key used by the backend plugin. - `apiDomain`: the public backend origin that hosts SuperTokens and the Rownd plugin routes. - `apiBasePath`: the SuperTokens API base path, for example ``. - `clientDomain`: optional key from the backend `clientDomains` map. #### 2.1 Install the React SDK #### 2.1 Load the hosted Hub script Use this option when you do not use a package-based frontend framework. #### 2.1 Add the Android SDK The Android SDK is published through JitPack. #### 2.1 Add the iOS SDK In Xcode, add this Swift Package dependency: #### 2.1 Install the Flutter SDK Add the SuperTokens Rownd Flutter package to `pubspec.yaml`: #### 2.1 Install the React Native SDK ```bash npm install @supertokens/rownd-react ``` ```bash yarn add @supertokens/rownd-react ``` ```bash pnpm add @supertokens/rownd-react ``` ```bash bun add @supertokens/rownd-react ``` ```html ``` ```gradle dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url "https://jitpack.io" } } } ``` ```text https://github.com/supertokens/supertokens-rownd-ios.git ``` ```yaml dependencies: supertokens_rownd_flutter: ^0.1.0 provider: ^6.1.2 ``` ```bash npm install @supertokens/rownd-react-native ``` ```bash yarn add @supertokens/rownd-react-native ``` ```bash pnpm add @supertokens/rownd-react-native ``` ```bash bun add @supertokens/rownd-react-native ``` #### 2.2 Add the provider Replace imports from `@rownd/react` with `@supertokens/rownd-react`, then add `RowndProvider` near the root of your application. The script URL supports these query parameters: | Parameter | Required | Description | | --- | --- | --- | | `appKey` | Yes | Rownd app key used by the backend plugin. | | `apiDomain` | Yes | Public backend origin that hosts the plugin routes. | | `apiBasePath` | No | SuperTokens API base path. Defaults to ``. | | `appVariantId` | No | Rownd app variant or sub-brand ID. | | `clientDomain` | No | Key from the backend plugin `clientDomains` map. | | `displayContext` | No | Usually `browser` for direct web integrations. | #### 2.2 Use runtime config Use `window._rphConfig` for optional settings that are easier to set in JavaScript than in the script URL. Select the `Rownd` package product and add it to your app target. If you use CocoaPods instead, install the `RowndSupertokens` pod. The pod exposes the same Swift module, so app code still imports `Rownd`. #### 2.2 Configure Rownd Then fetch dependencies: React Native apps must use React Native `0.61` or newer. Native builds also need Android `minSdkVersion` `26` or newer and iOS deployment target `14.0` or newer. #### 2.2 Expo setup For Expo apps, add the plugin, a URL scheme, and native platform versions to `app.json`. Use a development build or prebuild so native URL scheme and platform configuration is generated. ```tsx check=false reason="Requires earlier Rownd setup context" import React from "react"; import ReactDOM from "react-dom/client"; import { RowndProvider } from "@supertokens/rownd-react"; import { App } from "./App"; ReactDOM.createRoot(document.getElementById("root")!).render( ", apiBasePath: "", }, }} > , ); ``` ```html ``` ```gradle dependencies { implementation 'com.github.supertokens:supertokens-rownd-android:0.1.1' } ``` ```swift import Rownd import UIKit func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil ) -> Bool { Task { await Rownd.configure( launchOptions: launchOptions, appKey: "", supertokens: RowndSuperTokensConfig( appName: "My App", apiDomain: "", apiBasePath: "" ) ) } return true } ``` ```bash flutter pub get ``` ```json { "expo": { "scheme": "rowndsupertokens", "plugins": [ "@supertokens/rownd-react-native", [ "expo-build-properties", { "android": { "minSdkVersion": 26 }, "ios": { "deploymentTarget": "14.0" } } ] ] } } ``` Do not manually include the Hub script in your HTML when using the React SDK. The provider injects the Hub script for you. #### 2.3 Use Rownd-compatible APIs Add `_rphConfig` entries before the Hub script loads. The SDK requires `compileSdk 35` or newer, Kotlin Gradle plugin `2.1.0` or newer, and `minSdk 26` or newer. #### 2.2 Add configuration values #### 2.3 Configure links :::note[Contact the SuperTokens team before configuring production Universal Links. We need to set up the link asset files for your Hub domain, including the Apple App Site Association file.] ::: Add an Associated Domains entitlement for the Hub domain used by the app. #### 2.2 Configure Rownd Import the Flutter package and configure it before using any Rownd APIs. Install the Expo build properties plugin before running prebuild: ```tsx import { RequireSignIn, SignedIn, SignedOut, useRownd } from "@supertokens/rownd-react"; export function AuthControls() { const { requestSignIn, signOut, user } = useRownd(); return (

{user.data?.email || user.data?.phone_number || user.data?.user_id}

Protected content

); } ```
```gradle android { defaultConfig { manifestPlaceholders = [rowndDeepLinkScheme: "rowndsupertokens"] buildConfigField "String", "ROWND_APP_KEY", '""' buildConfigField "String", "ROWND_API_DOMAIN", '""' buildConfigField "String", "ROWND_API_BASE_PATH", '""' buildConfigField "String", "ROWND_DEEP_LINK_SCHEME", '"rowndsupertokens"' } } ``` ```xml com.apple.developer.associated-domains applinks:my-app.rownd-hub.supertokens.com ``` ```dart import 'package:supertokens_rownd_flutter/rownd.dart'; import 'package:supertokens_rownd_flutter/rownd_platform_interface.dart'; final rowndPlugin = RowndPlugin(); void configureRownd() { rowndPlugin.configure(RowndConfig( appKey: '', supertokens: RowndSuperTokensConfig( appInfo: RowndSuperTokensAppInfo( appName: 'My Flutter App', apiDomain: '', apiBasePath: '', ), ), )); } ``` ```bash npx expo install expo-build-properties ```
`requestSignIn()` supports Rownd-style options such as `identifier`, `auto_sign_in`, `init_data`, `post_login_redirect`, `include_user_data`, `redirect`, `intent`, `group_to_join`, `prevent_closing`, `method`, and `method_options`. #### 2.3 Configure deep links :::note[Contact the SuperTokens team before configuring production deep links. We need to set up the link asset files for your Hub domain, including Android App Links metadata.] ::: Add one custom-scheme fallback filter and one verified HTTPS App Link filter. Register the custom URL scheme fallback. The Flutter package is published as `supertokens_rownd_flutter`. Existing Rownd-style APIs remain available through `RowndPlugin`, but the package import and SuperTokens config are required for the migrated SDK. #### 2.3 Use Rownd-compatible APIs The SDK exposes Rownd state through a `ChangeNotifier`. Provide `rowndPlugin.state()` to your widget tree and use the plugin methods for sign-in, sign-out, account management, user profile calls, and access tokens. #### 2.3 Add the provider ```xml ``` ```xml CFBundleURLTypes CFBundleURLSchemes rowndsupertokens ``` ```dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:supertokens_rownd_flutter/rownd.dart'; import 'package:supertokens_rownd_flutter/rownd_platform_interface.dart'; import 'package:supertokens_rownd_flutter/state/global_state.dart'; final rowndPlugin = RowndPlugin(); void main() { WidgetsFlutterBinding.ensureInitialized(); rowndPlugin.configure(RowndConfig( appKey: '', supertokens: RowndSuperTokensConfig( appInfo: RowndSuperTokensAppInfo( appName: 'My Flutter App', apiDomain: '', apiBasePath: '', ), ), )); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => rowndPlugin.state()), Provider.value(value: rowndPlugin), ], child: const MaterialApp(home: AuthControls()), ); } } class AuthControls extends StatelessWidget { const AuthControls({super.key}); @override Widget build(BuildContext context) { return Consumer( builder: (_, rownd, __) { final isAuthenticated = rownd.state.auth?.isAuthenticated ?? false; return Scaffold( body: Center( child: ElevatedButton( onPressed: () { final plugin = context.read(); if (isAuthenticated) { plugin.signOut(); } else { plugin.requestSignIn(); } }, child: Text(isAuthenticated ? 'Sign out' : 'Sign in'), ), ), ); }, ); } } ``` ```tsx check=false reason="Requires earlier Rownd setup context" import { RowndProvider } from "@supertokens/rownd-react-native"; export default function Root() { return ( ", supertokens: { appInfo: { appName: "My App", apiDomain: "", apiBasePath: "", }, }, deepLinkScheme: "rowndsupertokens", }} > ); } ``` The HTTPS App Link domain should match `clientDomains.mobile` on the backend. #### 2.4 Initialize Rownd Forward custom URL scheme links and Universal Links to Rownd. `requestSignIn()` accepts an optional `RowndSignInOptions` object. The migrated Flutter SDK currently exposes `postSignInRedirect` as the sign-in option. #### 2.4 Configure Android Flutter Android apps need JitPack because the native SuperTokens Rownd Android SDK is resolved from JitPack. The React Native provider accepts `appKey`, `supertokens.appInfo`, `deepLinkScheme`, and optional `hubUrlOverride`. Use `hubUrlOverride` only for staging or local Hub testing. React Native does not send a `clientDomain` prop; mobile Hub flows use the backend `clientDomains.mobile` default. #### 2.4 Register native links :::note[Contact the SuperTokens team before configuring production deep links. We need to set up the link asset files for your Hub domain for the native platforms your React Native app supports.] ::: For bare React Native iOS apps, install pods after adding the package: ```kotlin import android.app.Application import io.rownd.android.Rownd import io.rownd.android.RowndConfigureOptions class MyApplication : Application() { override fun onCreate() { super.onCreate() Rownd.configure( this, RowndConfigureOptions( appKey = BuildConfig.ROWND_APP_KEY, apiDomain = BuildConfig.ROWND_API_DOMAIN, apiBasePath = BuildConfig.ROWND_API_BASE_PATH, deepLinkScheme = BuildConfig.ROWND_DEEP_LINK_SCHEME, ) ) } } ``` ```swift func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { return Rownd.handleSmartLink(url: url) } func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } return Rownd.handleSmartLink(url: url) } ``` ```gradle dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url "https://jitpack.io" } } } ``` ```bash cd ios && pod install ``` #### 2.5 Call protected APIs Rownd manages the SuperTokens session after sign-in. For `OkHttp`, add the SuperTokens interceptor to clients that call protected backend APIs. The Universal Link domain should match `clientDomains.mobile` on the backend. Set Android platform versions and Kotlin metadata support: Register the same scheme in `Info.plist` and forward URL opens to React Native `Linking`. The React Native Rownd provider listens for `Linking` events and passes matching links to the native SDK. ```kotlin import com.supertokens.session.SuperTokensInterceptor import okhttp3.OkHttpClient val client = OkHttpClient.Builder() .addInterceptor(SuperTokensInterceptor()) .build() ``` ```gradle android { compileSdk 35 defaultConfig { minSdk 26 targetSdk 35 } } ``` ```xml CFBundleURLTypes CFBundleURLSchemes rowndsupertokens ``` Use Kotlin Gradle plugin `2.1.0` or newer. Also make your main activity extend `FlutterFragmentActivity` instead of `FlutterActivity`: Objective-C app delegate: ```kotlin import io.flutter.embedding.android.FlutterFragmentActivity class MainActivity : FlutterFragmentActivity() ``` ```objc #import - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { return [RCTLinkingManager application:application openURL:url options:options]; } ``` #### 2.5 Configure iOS The Flutter plugin depends on the `RowndSupertokens` CocoaPod. The pod exposes the Swift module as `Rownd`, so Flutter apps do not need app-level Swift import changes. Install pods after adding the package: Swift app delegate: ```bash cd ios && pod install ``` ```swift import React override func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { return RCTLinkingManager.application(app, open: url, options: options) } ``` If an existing lockfile pins an older `lottie-ios` version, update pods: For Android, register the scheme on the activity that hosts React Native and use `singleTask`. The scheme must match `config.deepLinkScheme`; `singleTask` is required so links opened while the app is running are delivered to the existing React Native activity. ```bash cd ios && pod update lottie-ios --repo-update ``` ```xml ``` #### 2.6 Configure mobile links :::note[Contact the SuperTokens team before configuring production deep links or Universal Links. We need to set up the link asset files for your Hub domain for the native platforms your Flutter app supports.] ::: Configure the same native link handling described in the Android and iOS tabs for Flutter's Android and iOS host apps. The HTTPS App Link or Universal Link domain should match `clientDomains.mobile` on the backend. If your bare React Native Android app uses Google Sign-In, initialize the Rownd package from `MainActivity` before calling auth APIs: ```kotlin import android.os.Bundle import com.facebook.react.ReactActivity import com.reactnativerowndplugin.RowndPluginPackage class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) RowndPluginPackage.preInit(this) } } ``` #### 2.5 Use Rownd-compatible APIs ```tsx import { Pressable, Text, View } from "react-native"; import { useRownd } from "@supertokens/rownd-react-native"; export function AuthControls() { const { is_authenticated, requestSignIn, signOut, user, getAccessToken } = useRownd(); async function callProtectedApi() { const accessToken = await getAccessToken(); // Use accessToken in the Authorization header for your protected API call. } if (is_authenticated) { return ( Welcome {user.data?.email ?? user.data?.first_name} Get access token signOut()}> Sign out ); } return ( requestSignIn()}> Sign in ); } ``` `requestSignIn()` accepts Rownd-style options such as `method`, `postSignInRedirect`, and `intent`. The `guest` method is treated as `anonymous`. On Android, forcing `email` or `phone` currently opens the Hub default flow rather than bypassing the method selector. ### 3. Validate client flows Test the following flows to validate your client integration: - Existing users can authenticate - User logins and sign ups are migrated to SuperTokens - Existing Rownd sessions migrate without forcing users to sign in again. - Deep links work as expected on mobile ### 4. Migrate OAuth/OIDC clients (optional) If your Rownd application acts as an OAuth/OIDC provider, update clients to use SuperTokens discovery and endpoints after the SuperTokens team migrates your Rownd OAuth clients into SuperTokens Core. #### 4.1 Replace the discovery URL Replace the Rownd discovery URL: ```text https://api.rownd.io/oidc/{rowndAppId}/.well-known/openid-configuration ``` with your SuperTokens discovery URL: ```text //.well-known/openid-configuration ``` #### 4.2 Replace hardcoded endpoints If a client hardcodes endpoints, update them like this: | Rownd endpoint | SuperTokens endpoint | | --- | --- | | `/oidc/{appId}/.well-known/openid-configuration` | `/.well-known/openid-configuration` | | `/oidc/{appId}/auth` | `/oauth/auth` | | `/oidc/{appId}/token` | `/oauth/token` | | `/oidc/{appId}/me` | `/oauth/userinfo` | | `/oidc/{appId}/jwks` | `/jwt/jwks.json` | | `/oidc/{appId}/token/introspection` | `/oauth/introspect` | | `/oidc/{appId}/token/revocation` | `/oauth/revoke` | | `/oidc/{appId}/session/end` | `/oauth/end_session` | Replace `` with your configured backend API base path. #### 4.3 Confirm client IDs and tokens Continue using the OAuth credential `client_id` and `client_secret` that Rownd issued and the SuperTokens team migrated. Do not use the Rownd OIDC client configuration `id` as the OAuth `client_id`. Existing Rownd-issued OAuth tokens are not SuperTokens-issued tokens. After cutover, users should complete a new authorization flow against SuperTokens unless a separate token migration path is explicitly enabled for your project. #### 4.4 Validate OAuth Check discovery and JWKS: ```bash curl //.well-known/openid-configuration curl //jwt/jwks.json ``` Start an authorization-code flow: ```text //oauth/auth?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&scope=openid%20profile%20email%20phone%20offline_access ``` Exchange the code: ```bash curl -X POST //oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "CLIENT_ID:CLIENT_SECRET" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE" \ -d "redirect_uri=REDIRECT_URI" ``` Fetch userinfo: ```bash curl //oauth/userinfo \ -H "Authorization: Bearer ACCESS_TOKEN" ``` --- # Session Migration Source: https://supertokens.com/docs/migration/session-migration This guide shows you how to migrate user sessions from your previous authentication provider to **SuperTokens**. --- ## Overview To achieve a seamless transition process, you will also have to migrate the active sessions that use your previous authentication provider to **SuperTokens**. To do this you should create a new flow that will determine if an existing user session needs to be migrated, and call the migration API if necessary. You can see a detailed illustration of the process below. Session migration flow chart ## Steps ### 1. Add the session migration endpoint Create a rate-limited backend endpoint that exchanges a valid legacy access token for a **SuperTokens Session**. The following example uses the APIs released in SuperTokens Node SDK 24.0.3. ```tsx title="Backend changes" check=false reason="Requires surrounding framework application context" import express from "express"; import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; const app = express(); app.use(express.json()); interface VerifiedLegacyToken { issuer: string; subject: string; tenantId: string; } app.post("/migrate-session", migrationRateLimiter, async (req, res, next) => { const match = req.headers.authorization?.match(/^Bearer[ \t]+([^\s,]+)$/i); const idempotencyKey = req.header("Idempotency-Key"); if (match === null || match === undefined || !isValidIdempotencyKey(idempotencyKey)) { res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); return; } let verifiedToken: VerifiedLegacyToken; try { verifiedToken = await verifyLegacyAccessToken(match[1]); } catch { res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); return; } try { const identity = getNamespacedLegacyIdentity(verifiedToken); await enforceVerifiedIdentityRateLimit(identity.identityKey); const mapping = await SuperTokens.getUserIdMapping({ userId: identity.externalUserId, userIdType: "EXTERNAL", }); if (mapping.status !== "OK") { res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); return; } const recipeUserId = SuperTokens.convertToRecipeUserId(mapping.superTokensUserId); const result = await migrateSessionIdempotently( { idempotencyKey, identityKey: identity.identityKey, tenantId: verifiedToken.tenantId, recipeUserId: recipeUserId.getAsString(), }, () => Session.createNewSession(req, res, verifiedToken.tenantId, recipeUserId), ); if (result.status === "CONFLICT") { res.status(409).send({ status: "MIGRATION_CONFLICT" }); return; } if (result.status === "IN_PROGRESS") { res.set("Retry-After", "1").status(409).send({ status: "MIGRATION_IN_PROGRESS" }); return; } res.send({ status: result.status }); } catch (error) { next(error); } }); app.post("/confirm-session-migration", migrationRateLimiter, async (req, res, next) => { const idempotencyKey = req.body?.idempotencyKey; if (!isValidIdempotencyKey(idempotencyKey)) { res.status(400).send({ status: "INVALID_IDEMPOTENCY_KEY" }); return; } try { const session = await Session.getSession(req, res); const confirmed = await confirmMigrationOutcome({ idempotencyKey, tenantId: session.getTenantId(), recipeUserId: session.getRecipeUserId().getAsString(), }); res.status(confirmed ? 200 : 409).send({ status: confirmed ? "CONFIRMED" : "IDENTITY_MISMATCH" }); } catch (error) { next(error); } }); function isValidIdempotencyKey(value: unknown): value is string { return typeof value === "string" && /^[A-Za-z0-9_-]{32,128}$/.test(value); } declare function migrationRateLimiter(req: express.Request, res: express.Response, next: express.NextFunction): void; declare function enforceVerifiedIdentityRateLimit(identityKey: string): Promise; declare function getNamespacedLegacyIdentity(verifiedToken: VerifiedLegacyToken): { identityKey: string; externalUserId: string; }; declare function migrateSessionIdempotently( input: { idempotencyKey: string; identityKey: string; tenantId: string; recipeUserId: string; }, createSession: () => ReturnType, ): Promise<{ status: "CREATED" | "RECOVERED" | "CONFLICT" | "IN_PROGRESS" }>; declare function confirmMigrationOutcome(input: { idempotencyKey: string; tenantId: string; recipeUserId: string; }): Promise; // Implement this contract with your provider's supported SDK or a JWT library configured for that provider. declare function verifyLegacyAccessToken(accessToken: string): Promise; ``` :::info[Important] `verifyLegacyAccessToken` is an application-owned security boundary, not a provider-independent implementation. It must verify the signature with the provider's trusted algorithms and keys, and validate the exact issuer, audience, expiry, not-before time, token type or purpose, and required subject claims. If the provider uses JWKS, use HTTPS, cache keys, and refresh them safely when keys rotate. Derive `tenantId` from trusted application configuration and validated claims, never directly from request data that has not been verified. `getNamespacedLegacyIdentity` must use the same collision-resistant canonical encoding used during account import. Its external ID and identity key must include the validated issuer, trusted tenant, and subject. Do not concatenate ambiguous strings or map by subject alone: subjects are only unique within an issuer and can also overlap between tenants. ::: Configure CORS on the backend with the exact frontend origin, `credentials: true`, and the `Authorization`, `Content-Type`, and `Idempotency-Key` request headers explicitly allowed. Do not combine credentialed requests with `Access-Control-Allow-Origin: *`. The endpoint's error handler should return a generic 401 for verification failures without exposing token-validation details. Implement `migrateSessionIdempotently` with a distributed outcome store indexed uniquely by both the stable request key and the legacy identity scoped to its issuer and tenant. A completed outcome records the tenant, recipe user ID, and created session handle. A retry for the same key and identity must return or safely recover that logical outcome; if the original session was not delivered, revoke it before issuing a replacement. Reject a key bound to another identity, another key for an already migrated identity, and concurrent in-progress exchanges. Never store the raw legacy token. Keep completed outcomes at least until the legacy session and token can no longer be accepted. `confirmMigrationOutcome` must compare the current SuperTokens tenant and recipe user ID with that stored outcome and atomically mark it confirmed. ### 2. Call the migration endpoint from your frontend app On page load, obtain the legacy token and its stable request key. If a SuperTokens session exists, confirm that it matches the stored migration outcome before cleaning up the legacy session. Otherwise, perform the idempotent exchange, then confirm the identities. Never treat an HTTP success alone as proof that the current SuperTokens and legacy identities match. The example uses SuperTokens Web JS 0.16.0 and assumes that the SDK is initialized. ```tsx title="Frontend changes" import axios from "axios"; import Session from "supertokens-web-js/recipe/session"; // Call this function on page load async function migrateUserSessions() { const apiDomain = "..."; const accessToken = await getAccessTokenFromOldProvider(); if (accessToken === undefined) { return; } const idempotencyKey = await getOrCreateMigrationIdempotencyKey(); if (!(await Session.doesSessionExist())) { await axios.post( `${apiDomain}/migrate-session`, {}, { headers: { Authorization: `Bearer ${accessToken}`, "Idempotency-Key": idempotencyKey, }, withCredentials: true, }, ); } if (!(await confirmMigratedIdentity(apiDomain, idempotencyKey))) { return; } await revokeSessionFromOldProvider(); await clearMigrationIdempotencyKey(); } async function confirmMigratedIdentity(apiDomain: string, idempotencyKey: string): Promise { try { const response = await axios.post( `${apiDomain}/confirm-session-migration`, { idempotencyKey }, { withCredentials: true }, ); return response.data.status === "CONFIRMED"; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 409) { return false; } throw error; } } async function getAccessTokenFromOldProvider(): Promise { // Return the provider's access token when its session exists, or undefined otherwise. return "..."; } // Persist one random key for this specific legacy provider session until cleanup succeeds. declare function getOrCreateMigrationIdempotencyKey(): Promise; declare function clearMigrationIdempotencyKey(): Promise; async function revokeSessionFromOldProvider() { // Revoke the session associated with the previous provider } ``` --- ## See also --- # Configure Email Delivery Source: https://supertokens.com/docs/platform-configuration/email-delivery ## Email delivery summary - Email delivery is owned by the `EmailPassword`, `EmailVerification`, `Passwordless`, and `WebAuthn` recipes. `AccountLinking` does not configure delivery. - Without configuration, each recipe uses its built-in delivery service. The endpoint and failure behavior differ by recipe and SDK; this service does not support template customization. - Configure your own SMTP server to send from your domain and optionally customize the subject and template. - For complete control, provide a custom delivery implementation or override `sendEmail`. ## Overview SuperTokens sends emails in different authentication scenarios. Email delivery is configured on the recipe that generates the message: `EmailPassword` for password resets, `EmailVerification`, `Passwordless` for email codes and links, and `WebAuthn` for account-recovery emails. The `AccountLinking` recipe does not own an email-delivery configuration. The following page shows you how to configure the email delivery method and adjust the content that gets sent to your users. ## Delivery methods ### Default service If you provide no email-delivery configuration, the recipe uses the backend SDK's built-in delivery service. This applies whether the Core is self-hosted or managed. Do not allow the external delivery endpoints from a single hostname: released recipes use both `api.supertokens.io` and `api.supertokens.com`. :::note The built-in service does not support template customization. Configure SMTP or a custom delivery implementation when you need to control the sender, content, delivery guarantees, or data-processing terms. ::: :::caution[Failure behavior differs] Do not treat the built-in service as a durable queue. In Node.js 24.0.3, Passwordless awaits delivery, while several other email flows suppress built-in-service failures outside serverless environments. If delivery is security-critical, provide your own service, await its result, monitor failures, and make retries idempotent. ::: --- ### SMTP service Using this method, you can provide your own SMTP server configuration and the system sends emails through it. Use this method if you want to: - Send emails using your own domain. - Optionally customize the default email template and subject. ```tsx import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import { SMTPService } from "supertokens-node/recipe/emailpassword/emaildelivery"; import EmailVerification from "supertokens-node/recipe/emailverification"; import { SMTPService as EmailVerificationSMTPService } from "supertokens-node/recipe/emailverification/emaildelivery"; import Passwordless from "supertokens-node/recipe/passwordless"; import { SMTPService as PasswordlessSMTPService } from "supertokens-node/recipe/passwordless/emaildelivery"; import WebAuthn from "supertokens-node/recipe/webauthn"; import { SMTPService as WebAuthnSMTPService } from "supertokens-node/recipe/webauthn/emaildelivery"; const smtpSettings = { host: "...", authUsername: "...", // this is optional. In case not given, from.email will be used password: "...", port: 465, from: { name: "...", email: "...", }, secure: true, }; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ emailDelivery: { service: new SMTPService({ smtpSettings }), }, }), // if email verification is enabled.. EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { service: new EmailVerificationSMTPService({ smtpSettings }), }, }), Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE", emailDelivery: { service: new PasswordlessSMTPService({ smtpSettings }), }, }), WebAuthn.init({ emailDelivery: { service: new WebAuthnSMTPService({ smtpSettings }), }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { smtpUsername := "..." smtpSettings := emaildelivery.SMTPSettings{ Host: "...", From: emaildelivery.SMTPFrom{ Name: "...", Email: "...", }, Port: 465, Username: &smtpUsername, // this is optional. In case not given, from.email will be used Password: "...", Secure: true, } supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Service: emailpassword.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: smtpSettings, }), }, }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Service: emailverification.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: smtpSettings, }), }, }), passwordless.Init(plessmodels.TypeInput{ ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true}, FlowType: "USER_INPUT_CODE", EmailDelivery: &emaildelivery.TypeInput{ Service: passwordless.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: smtpSettings, }), }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig, SMTPSettingsFrom, SMTPSettings from supertokens_python.recipe import emailverification from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig smtp_settings = SMTPSettings( host="...", port=465, from_=SMTPSettingsFrom( name="...", email="..." ), password="...", secure=True, username="..." # this is optional. In case not given, from_.email will be used ) init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( email_delivery=EmailDeliveryConfig( service=emailpassword.SMTPService( smtp_settings=smtp_settings ) ) ), # If email verification is enabled emailverification.init( mode="OPTIONAL", email_delivery=EmailDeliveryConfig( service=emailverification.SMTPService( smtp_settings=smtp_settings ) ) ), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", email_delivery=EmailDeliveryConfig( service=passwordless.SMTPService(smtp_settings=smtp_settings) ) ) ] ) ``` Port 465 conventionally uses implicit TLS, so the examples set `secure`/`Secure` to `true`. For STARTTLS, use the port specified by your provider (commonly 587) and set `secure`/`Secure` to `false`; the connection starts without encryption and is then upgraded. Never disable certificate or hostname verification. Node.js 24.0.3 exports a WebAuthn SMTP template service. Python 0.31.3 and Go 0.26.0 accept WebAuthn email-delivery implementations but do not export a public, recipe-specific WebAuthn SMTP template service; configure a WebAuthn delivery override in those SDKs instead of importing an internal module or reusing another recipe's template service. ### Custom method This method allows you to define your own email sending abstraction. ```tsx import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import EmailVerification from "supertokens-node/recipe/emailverification"; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: create and send password reset email // Or use the original implementation which calls the default service, // or a service that you may have specified in the emailDelivery object. return originalImplementation.sendEmail(input); }, }; }, }, }), // if email verification is enabled EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: create and send email verification email // Or use the original implementation which calls the default service, // or a service that you may have specified in the emailDelivery object. return originalImplementation.sendEmail(input); }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // TODO: create and send password reset email // Or use the original implementation which calls the default service, // or a service that you may have specified in the EmailDelivery object. return originalSendEmail(input, userContext) } return originalImplementation }, }, }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // TODO: create and email verification email // Or use the original implementation which calls the default service, // or a service that you may have specified in the EmailDelivery object. return originalSendEmail(input, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import emailpassword from typing import Dict, Any from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig from supertokens_python.recipe.emailverification.types import EmailDeliveryOverrideInput as EVEmailDeliveryOverrideInput, EmailTemplateVars as EVEmailTemplateVars from supertokens_python.recipe import emailverification def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: # TODO: create and send password reset email # Or use the original implementation which calls the default service, # or a service that you may have specified in the email_delivery object. return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation def custom_emailverification_delivery(original_implementation: EVEmailDeliveryOverrideInput) -> EVEmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EVEmailTemplateVars, user_context: Dict[str, Any]) -> None: # TODO: create and send email verification email # Or use the original implementation which calls the default service, # or a service that you may have specified in the email_delivery object. return await original_send_email(template_vars, user_context) original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ), # If email verification is enabled emailverification.init( mode="OPTIONAL", email_delivery=EmailDeliveryConfig(override=custom_emailverification_delivery)) ] ) ``` If you call the original implementation function for `sendEmail`, it uses the service that you have configured. If you have not configured any service, it uses the default service. Using this method, you can, for example, have your custom way of sending email verification emails, but use the default or SMTP service to send the reset password emails. :::note[Error management] Throw or return an error from your custom `sendEmail` implementation when delivery fails. API-triggered delivery can propagate the error through the SDK's error handler; non-API calls may log it. This does not describe every recipe's built-in service behavior; see the warning under [Default service](#default-service). ::: --- ## Email content customization You can access the default email UI through the following links: - Default [email verification template](/references/frontend-sdks/prebuilt-ui/ui-showcase#email-verification) and its [source code](https://github.com/supertokens/email-sms-templates/blob/master/email-html/email-verification.html). - Default [password reset template](/references/frontend-sdks/prebuilt-ui/ui-showcase#password-reset) and its [source code](https://github.com/supertokens/email-sms-templates/blob/master/email-html/password-reset.html). To change the content you can create a custom `SMTPService` like and update the property which builds the content. The method allows you to return an object that has the following properties: - `body`: This is the email's body. This can be HTML or text as well. - `isHtml`: If the body is HTML, then this should be `true`. - `subject`: This is the subject of the email to send. - `toEmail`: The system sends the email to this email. Other information like which email address to send from appears in the `smtpSettings` object. ```tsx check=false reason="Requires surrounding application context" import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import { SMTPService } from "supertokens-node/recipe/emailpassword/emaildelivery"; import EmailVerification from "supertokens-node/recipe/emailverification"; import { SMTPService as EmailVerificationSMTPService } from "supertokens-node/recipe/emailverification/emaildelivery"; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ emailDelivery: { service: new SMTPService({ smtpSettings: { /*...*/ }, override: (originalImplementation) => { return { ...originalImplementation, getContent: async function (input) { // password reset content let { passwordResetLink, user } = input; // you can even call the original implementation and modify that let originalContent = await originalImplementation.getContent(input); originalContent.subject = "My custom subject"; return originalContent; }, }; }, }), }, }), // if email verification is enabled EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { service: new EmailVerificationSMTPService({ smtpSettings: { /*...*/ }, override: (originalImplementation) => { return { ...originalImplementation, getContent: async function (input) { // email verification content let { emailVerifyLink, user } = input; // you can even call the original implementation and modify that let originalContent = await originalImplementation.getContent(input); originalContent.subject = "My custom subject"; return originalContent; }, }; }, }), }, }), Session.init(), ], }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Service: emailpassword.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: emaildelivery.SMTPSettings{ /* ... */ }, Override: func(originalImplementation emaildelivery.SMTPInterface) emaildelivery.SMTPInterface { originalGetContent := *originalImplementation.GetContent (*originalImplementation.GetContent) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) (emaildelivery.EmailContent, error) { // password reset content passwordResetLink := input.PasswordReset.PasswordResetLink user := input.PasswordReset.User fmt.Println(passwordResetLink) fmt.Println(user) // you can even call the original implementation and modify that originalContent, err := originalGetContent(input, userContext) if err != nil { return emaildelivery.EmailContent{}, err } originalContent.Subject = "My custom subject" return originalContent, nil } return originalImplementation }, }), }, }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Service: emailverification.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: emaildelivery.SMTPSettings{ /* ... */ }, Override: func(originalImplementation emaildelivery.SMTPInterface) emaildelivery.SMTPInterface { originalGetContent := *originalImplementation.GetContent (*originalImplementation.GetContent) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) (emaildelivery.EmailContent, error) { // email verification email content emailVerificationLink := input.EmailVerification.EmailVerifyLink user := input.EmailVerification.User fmt.Println(emailVerificationLink) fmt.Println(user) // you can even call the original implementation and modify that originalContent, err := originalGetContent(input, userContext) if err != nil { return emaildelivery.EmailContent{}, err } originalContent.Subject = "My custom subject" return originalContent, nil } return originalImplementation }, }), }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig, EmailContent, SMTPSettings from supertokens_python.recipe.emailpassword.types import SMTPOverrideInput, EmailTemplateVars from typing import Dict, Any from supertokens_python.recipe.emailverification.types import SMTPOverrideInput as EVSMTPOverrideInput, EmailTemplateVars as EVEmailTemplateVars from supertokens_python.recipe import emailverification def custom_smtp_content_override(original_implementation: SMTPOverrideInput) -> SMTPOverrideInput: original_get_content = original_implementation.get_content async def get_content(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> EmailContent: # password reset content _ = template_vars.password_reset_link __ = template_vars.user # you can even call the original implementation and modify that original_content = await original_get_content(template_vars, user_context) original_content.subject = "My custom subject" return original_content original_implementation.get_content = get_content return original_implementation def custom_smtp_email_verification_content_override(original_implementation: EVSMTPOverrideInput) -> EVSMTPOverrideInput: original_get_content = original_implementation.get_content async def get_content(template_vars: EVEmailTemplateVars, user_context: Dict[str, Any]) -> EmailContent: # email verification content _ = template_vars.email_verify_link __ = template_vars.user # you can even call the original implementation and modify that original_content = await original_get_content(template_vars, user_context) original_content.subject = "My custom subject" return original_content original_implementation.get_content = get_content return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( email_delivery=EmailDeliveryConfig( service=emailpassword.SMTPService( smtp_settings=SMTPSettings(...), override=custom_smtp_content_override ) ) ), # If email verification is enabled emailverification.init( mode="OPTIONAL", email_delivery=EmailDeliveryConfig( service=emailverification.SMTPService( smtp_settings=SMTPSettings(...), override=custom_smtp_email_verification_content_override ) ) ) ] ) ``` ## Overrides You can use the override functionality to trigger any kind of behavior before and after email sending. This can include things like: - Logging - Spam protection actions - Modifying the email template variables before sending the emails ```tsx import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import EmailVerification from "supertokens-node/recipe/emailverification"; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: run some logic before sending the email await originalImplementation.sendEmail(input); // TODO: run some logic post sending the email }, }; }, }, }), // if email verification is enabled EmailVerification.init({ mode: "OPTIONAL", emailDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendEmail: async function (input) { // TODO: run some logic before sending the email await originalImplementation.sendEmail(input); // TODO: run some logic post sending the email }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // TODO: run some logic before sending the email err := originalSendEmail(input, userContext) if err != nil { return err } // TODO: run some logic post sending the email return nil } return originalImplementation }, }, }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { // TODO: run some logic before sending the email err := originalSendEmail(input, userContext) if err != nil { return err } // TODO: run some logic post sending the email return nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import emailpassword from typing import Dict, Any from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig from supertokens_python.recipe.emailverification.types import EmailDeliveryOverrideInput as EVEmailDeliveryOverrideInput, EmailTemplateVars as EVEmailTemplateVars from supertokens_python.recipe import emailverification def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: # TODO: run some logic before sending the email resp = await original_send_email(template_vars, user_context) # TODO: run some logic after sending the email return resp original_implementation.send_email = send_email return original_implementation def custom_emailverification_delivery(original_implementation: EVEmailDeliveryOverrideInput) -> EVEmailDeliveryOverrideInput: original_send_email = original_implementation.send_email async def send_email(template_vars: EVEmailTemplateVars, user_context: Dict[str, Any]) -> None: # TODO: run some logic before sending the email resp = await original_send_email(template_vars, user_context) # TODO: run some logic after sending the email return resp original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ emailpassword.init( email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ), # If email verification is enabled emailverification.init( mode="OPTIONAL", email_delivery=EmailDeliveryConfig(override=custom_emailverification_delivery)) ] ) ``` --- # SMS delivery Source: https://supertokens.com/docs/platform-configuration/sms-delivery ## Overview SuperTokens sends SMS in different authentication scenarios. SMS delivery is configured by the `Passwordless` recipe. Phone OTP can be used as an MFA factor, but the `MFA` recipe does not expose a separate SMS-delivery configuration. The following page shows you how to configure the SMS delivery method and adjust the content that gets sent to your users. ## Delivery methods ### Default method If you provide no configuration for SMS delivery, the Passwordless recipe uses the backend SDK's built-in service at `https://api.supertokens.com/0/services/sms`. This applies whether the Core is self-hosted or managed. :::info[Important] - Do not depend on the built-in service for production delivery. Its quota and availability are service policy, not an SDK contract. - When the service returns HTTP 429, released SDK fallback implementations treat that response as terminal and print the message input. This can include the phone number, OTP, magic link, and code lifetime. - You cannot customize the SMS content when using this method. If you want to customize the content, please see one of the other methods in this section. ::: :::caution[Sensitive fallback logs] OTP codes and magic links are authentication secrets. Prevent production fallback logs from reaching shared consoles or third-party log pipelines. If you must retain them for testing, restrict access, redact the phone number and secret values, set a short retention period, and verify deletion. Prefer configuring Twilio or a custom service before production so a quota response cannot expose message content through this fallback. ::: ### Twilio Using this method, you can provide your own Twilio account details to the backend SDK, and the SMS is sent using those. ```tsx import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import { TwilioService } from "supertokens-node/recipe/passwordless/smsdelivery"; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", smsDelivery: { service: new TwilioService({ twilioSettings: { accountSid: "...", authToken: "...", opts: { // optionally extra config to pass to Twilio client }, // Use exactly one sender option. This example uses from. from: "...", }, }), }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { smsService, err := passwordless.MakeTwilioService(smsdelivery.TwilioServiceConfig{ Settings: smsdelivery.TwilioSettings{ AccountSid: "...", AuthToken: "...", // Use exactly one sender option. This example uses From. From: "...", }, }) if err != nil { panic(err) } supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Service: smsService, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig, TwilioSettings init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( contact_config=ContactPhoneOnlyConfig(), flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig( service=passwordless.TwilioService( twilio_settings=TwilioSettings( account_sid="...", auth_token="...", opts={ # Optional configs to pass to twilio client }, # Use exactly one sender option. This example uses from_. from_="...", ) ) ) ) ] ) ``` To learn about how to customize the SMS templates, please see the next section. ### SuperTokens SMS service The backend SDKs also expose an API-key-based `SuperTokensSMSService`. It calls an external SMS endpoint directly and can be used whether your Core is self-hosted or managed. Availability, pricing, credits, quotas, sender identity, key issuance, and the Dashboard workflow are mutable service policy. Confirm them in your current Dashboard or contract before adopting this option; they are not guaranteed by the released SDK interface. If you have been issued an SMS API key, set it in the backend SDK configuration: ```tsx import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import { SupertokensService } from "supertokens-node/recipe/passwordless/smsdelivery"; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", smsDelivery: { service: new SupertokensService(""), }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" "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{ ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Service: passwordless.MakeSupertokensSMSService(""), }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( contact_config=ContactPhoneOnlyConfig(), flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig( service=passwordless.SuperTokensSMSService("")) ) ] ) ``` ### Custom method This method allows you to send messages however you like. The input to the send function consists of SMS template variables, allowing you to create the content of the SMS as well. Use this method if you are: - Using a third-party SMS service that is **not** Twilio. - You want to use another delivery method like WhatsApp or Facebook Messenger. - You want to do some custom spam protection before sending the SMS. - You already have an SMS sending infrastructure and want to use that. ```tsx 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: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", smsDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendSms: async function ({ codeLifetime, // amount of time the code is alive for (in MS) phoneNumber, urlWithLinkCode, // magic link userInputCode, // OTP }) { // TODO: create and send SMS }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" "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{ ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Override: func(originalImplementation smsdelivery.SmsDeliveryInterface) smsdelivery.SmsDeliveryInterface { (*originalImplementation.SendSms) = func(input smsdelivery.SmsType, userContext supertokens.UserContext) error { // amount of time the code is alive for (in MS) codeLifetime := input.PasswordlessLogin.CodeLifetime phoneNumber := input.PasswordlessLogin.PhoneNumber // magic link urlWithLinkCode := input.PasswordlessLogin.UrlWithLinkCode // OTP userInputCode := input.PasswordlessLogin.UserInputCode fmt.Println(codeLifetime) fmt.Println(phoneNumber) fmt.Println(urlWithLinkCode) fmt.Println(userInputCode) // TODO: create and send SMS return nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.passwordless.types import SMSDeliveryOverrideInput, SMSTemplateVars from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from typing import Dict, Any from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMSDeliveryOverrideInput: async def send_sms(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> None: # amount of time the code is alive for (in MS) _ = template_vars.code_life_time __ = template_vars.phone_number ___ = template_vars.url_with_link_code # magic link ____ = template_vars.user_input_code # OTP # TODO: create and send SMS... original_implementation.send_sms = send_sms return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( contact_config=ContactPhoneOnlyConfig(), flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig(override=custom_sms_deliver) ) ] ) ``` If you call the original implementation function for `sendSms`, it uses the service that you have configured. If you have not configured any service, it uses the default service. :::info[Important] When using this callback, you must manage sending the SMS yourself. ::: :::note[Error Management] Throw or return an error from `sendSms` when delivery fails. API-triggered delivery can propagate it through the SDK's error handler; non-API calls may log it. Do not include phone numbers, OTPs, magic links, provider credentials, or complete provider responses in exceptions or logs. ::: ## SMS Customization You can see the default SMS content: - Default [passwordless login with OTP template](https://github.com/supertokens/email-sms-templates#otp-login-1). - Default [passwordless login with magic link template](https://github.com/supertokens/email-sms-templates#magic-link-login-1). - Default [passwordless login with magic link and OTP template](https://github.com/supertokens/email-sms-templates#magic-link--otp-login-1). To change the content of the default SMS templates, you can override the `getContent` function in the `smsDelivery` object. It allows you to return an object that has the following properties: - `body`: The SMS message body. - `toPhoneNumber`: The phone number where the SMS is sent to. ```tsx check=false reason="Requires surrounding application context" import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import { TwilioService } from "supertokens-node/recipe/passwordless/smsdelivery"; supertokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", smsDelivery: { service: new TwilioService({ twilioSettings: { /*...*/ }, override: (originalImplementation) => { return { ...originalImplementation, getContent: async function ({ isFirstFactor, codeLifetime, // amount of time the code is alive for (in MS) phoneNumber, urlWithLinkCode, // magic link userInputCode, // OTP }) { if (isFirstFactor) { // send some custom SMS content return { toPhoneNumber: phoneNumber, body: "SMS BODY", }; } else { // for second factor, urlWithLinkCode will always be // undefined since we only support OTP based for second factor return { toPhoneNumber: phoneNumber, body: "SMS BODY", }; } // You can even call the original implementation and // modify its content: /*let originalContent = await originalImplementation.getContent(input) originalContent.body = "My custom body"; return originalContent;*/ }, }; }, }), }, }), Session.init(), ], }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { smsService, err := passwordless.MakeTwilioService(smsdelivery.TwilioServiceConfig{ Settings: smsdelivery.TwilioSettings{ /* ... */ }, Override: func(originalImplementation smsdelivery.TwilioInterface) smsdelivery.TwilioInterface { // originalGetContent := *originalImplementation.GetContent (*originalImplementation.GetContent) = func(input smsdelivery.SmsType, userContext supertokens.UserContext) (smsdelivery.SMSContent, error) { // amount of time the code is alive for (in MS) codeLifetime := input.PasswordlessLogin.CodeLifetime phoneNumber := input.PasswordlessLogin.PhoneNumber // magic link urlWithLinkCode := input.PasswordlessLogin.UrlWithLinkCode // OTP userInputCode := input.PasswordlessLogin.UserInputCode fmt.Println(codeLifetime) fmt.Println(phoneNumber) fmt.Println(urlWithLinkCode) fmt.Println(userInputCode) // send custom SMS content return smsdelivery.SMSContent{ Body: "SMS BODY", ToPhoneNumber: phoneNumber, }, nil // Or call the original implementation and change its content: /* originalResponse, err := originalGetContent(input, userContext) if err != nil { return smsdelivery.SMSContent{}, nil } originalResponse.body = "SMS Body" return originalResponse */ } return originalImplementation }, }) if err != nil { panic(err) } supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Service: smsService, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from supertokens_python.recipe.passwordless.types import TwilioOverrideInput, SMSTemplateVars from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig, SMSContent, TwilioSettings from typing import Dict, Any def custom_sms_content_override(original_implementation: TwilioOverrideInput) -> TwilioOverrideInput: # original_get_content = original_implementation.get_content async def get_content(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> SMSContent: # amount of time the code is alive for (in MS) _ = template_vars.code_life_time phone_number = template_vars.phone_number __ = template_vars.url_with_link_code # magic link ___ = template_vars.user_input_code # OTP # send custom SMS content return SMSContent(body="SMS BODY", to_phone=phone_number) # you can even call the original implementation and modify that # original_content = await original_get_content(template_vars, user_context) # original_content.body = "My custom body" # return original_content original_implementation.get_content = get_content return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( contact_config=ContactPhoneOnlyConfig(), flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig( service=passwordless.TwilioService( twilio_settings=TwilioSettings(...), override=custom_sms_content_override ) ) ) ] ) ``` ## Overrides You can use the override functionality to trigger any kind of behavior before and after SMS sending. This can include things like: - Logging - Spam protection actions - Modifying the SMS template variables before sending messages ```tsx 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: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE", contactMethod: "PHONE", smsDelivery: { override: (originalImplementation) => { return { ...originalImplementation, sendSms: async function (input) { // TODO: before sending SMS await originalImplementation.sendSms(input); // TODO: after sending SMS }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" "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{ ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Override: func(originalImplementation smsdelivery.SmsDeliveryInterface) smsdelivery.SmsDeliveryInterface { originalSendSms := *originalImplementation.SendSms (*originalImplementation.SendSms) = func(input smsdelivery.SmsType, userContext supertokens.UserContext) error { // TODO: before sending SMS err := originalSendSms(input, userContext) if err != nil { return err } // TODO: after sending SMS return nil } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.passwordless.types import SMSDeliveryOverrideInput, SMSTemplateVars from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from typing import Dict, Any from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMSDeliveryOverrideInput: original_send_sms = original_implementation.send_sms async def send_sms(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> None: # TODO: before sending SMS await original_send_sms(template_vars, user_context) # TODO: after sending SMS original_implementation.send_sms = send_sms return original_implementation init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ passwordless.init( contact_config=ContactPhoneOnlyConfig(), flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig(override=custom_sms_deliver) ) ] ) ``` --- # Add SSL via NGINX Source: https://supertokens.com/docs/platform-configuration/supertokens-core/add-ssl-via-nginx ## Overview SuperTokens Core does not terminate TLS. This guide configures NGINX as a trusted TLS edge and redirects HTTP traffic to HTTPS. ## Before you start :::warning This page is only relevant if you are self-hosting SuperTokens. ::: This guide assumes NGINX is installed and Core listens on `127.0.0.1:3567`. :::danger Bind Core's port 3567 to `127.0.0.1` or a private network and block it from public ingress. If clients can connect to Core directly, they can bypass TLS and controls enforced by NGINX. Core is a trusted backend component and must never be directly reachable by browsers or clients you do not trust. ::: ## Steps ### 1. Obtain a certificate For a local test only, create a self-signed certificate: ```bash sudo install -d -m 700 /etc/nginx/ssl sudo openssl req -x509 -nodes -newkey rsa:2048 \ -keyout /etc/nginx/ssl/server.key \ -out /etc/nginx/ssl/server.crt \ -subj "/CN=localhost" ``` Use a CA-issued certificate valid for the production hostname in production. ### 2. Configure NGINX Add separate HTTP and HTTPS server blocks. Replace `localhost` and certificate paths with production values when needed. ```text title="/etc/nginx/sites-available/default" server { listen 80; server_name localhost; return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name localhost; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location / { proxy_pass http://127.0.0.1:3567; proxy_http_version 1.1; proxy_set_header Host $host; } } ``` Test and apply the configuration: ```bash sudo nginx -t sudo service nginx reload ``` Verify that `http://localhost/hello` redirects to HTTPS and `https://localhost/hello` reaches Core. `/hello` is unauthenticated and is only a basic process/storage signal; success does not prove API-key enforcement or that direct port 3567 is private. Test externally that port 3567 is unreachable, and test a protected Core API with no, wrong, and current API keys. --- # API Keys Source: https://supertokens.com/docs/platform-configuration/supertokens-core/api-keys ## API key summary - SuperTokens Core requires no API key by default. After you configure one, every backend SDK request must provide a matching key or Core returns HTTP 401. - Configure multiple keys as a comma-separated value to rotate keys gradually across backend systems. - Every key must be at least 20 characters and contain only alphanumeric characters, `=`, or `-`. ## Overview The backend SDK uses API keys to authenticate requests to SuperTokens Core. By default, there is no API key required. After you configure one, every backend SDK must send a matching key or Core responds with HTTP 401. :::danger Core is a trusted backend component with APIs that can administer users and sessions. Keep Core on a private network that is reachable only by your backend services. Never expose it directly to browsers or clients you do not trust. An API key is defense in depth, not a replacement for network isolation. Use TLS if the key crosses a network you do not trust. Without an API key, any caller that can reach Core can perform administrative operations on your users' data. Configure an API key, restrict access by [IP address](/platform-configuration/supertokens-core/ip-allow-deny), and serve traffic over [TLS/SSL](/platform-configuration/supertokens-core/add-ssl-via-nginx). See [Secure the core](/deployment/self-host-supertokens#secure-the-core). ::: ## Before you start :::warning This page is only relevant if you are self-hosting SuperTokens. ::: ## Steps ### 1. Add the key to the core instance Generate a high-entropy key and store it in your deployment's secret manager. For example: ```bash openssl rand -hex 32 ``` The command prints a 64-character key that satisfies Core's character restrictions. Store it as `SUPERTOKENS_API_KEY` in your deployment's secret manager. Do not commit it to source control, logs, shell history, or an image layer. Set `SUPERTOKENS_IMAGE` to an immutable, verified image reference rather than an untagged image or `latest`. ```bash : "${SUPERTOKENS_IMAGE:?Set an immutable Core image reference}" : "${SUPERTOKENS_API_KEY:?Load a generated Core API key from secret storage}" if [[ ! "$SUPERTOKENS_API_KEY" =~ ^[A-Za-z0-9=-]{20,}(,[A-Za-z0-9=-]{20,})*$ ]]; then echo "SUPERTOKENS_API_KEY must contain one or more valid comma-separated Core API keys" >&2 exit 1 fi docker run \ --network app-network \ -e API_KEYS="$SUPERTOKENS_API_KEY" \ -d "$SUPERTOKENS_IMAGE" ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command # Replace this entire placeholder from secret storage before starting Core. # Core rejects the literal placeholder because underscores are not valid API-key characters. api_keys: "" ``` - The format of the value is `key1,key2,key3`. - Keys can only contain `=`, `-`, and alphanumeric characters. - Each key must have a minimum length of 20 characters. This is a syntax requirement, not an entropy recommendation. - Each backend sends only one key. Core can accept multiple independently generated keys separated by commas. Keep the Core and backend secret records separate, even if both workloads expose their value as `SUPERTOKENS_API_KEY`. During rotation, the Core record contains `old-key,new-key`; each backend secret record contains exactly one of those keys. ### 2. Add the key to your backend code Inject one key currently accepted by Core into each backend as `SUPERTOKENS_API_KEY`. ```tsx import supertokens from "supertokens-node"; const apiKey = process.env.SUPERTOKENS_API_KEY; if (apiKey === undefined || apiKey.length === 0) { throw new Error("SUPERTOKENS_API_KEY is required"); } supertokens.init({ supertokens: { connectionURI: "", apiKey, }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [], }); ``` ```go import ( "os" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiKey := os.Getenv("SUPERTOKENS_API_KEY") if apiKey == "" { panic("SUPERTOKENS_API_KEY is required") } supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "", APIKey: apiKey, }, }) } ``` ```python check=false reason="Partial configuration example" import os from supertokens_python import init, InputAppInfo, SupertokensConfig api_key = os.environ["SUPERTOKENS_API_KEY"] if not api_key: raise RuntimeError("SUPERTOKENS_API_KEY is required") init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='', api_key=api_key ), framework='...', recipe_list=[ #... ] ) ``` ### 3. Rotate a key safely Use an overlap period so that Core never rejects a backend that has not been updated yet: 1. Generate a new independent key and store it in secret storage. Keep the old key active. 2. Set Core's `API_KEYS` value to `old-key,new-key`, deploy or restart every Core instance, and explicitly test a protected API with each key from a trusted network. Do not continue unless both work. 3. Rotate all backends to the new key. Use a staged deployment where possible. Monitor backend request failures, the HTTP 401 rate in Core or edge telemetry, and deployment health throughout the change. Core does not identify which matching key was used, so use controlled old-key and new-key probes to verify both paths during the overlap. 4. Confirm every backend is healthy on the new key and that no planned rollback still depends on the old key. Then remove the old key from Core and deploy every Core instance. Verify the new key works and the old key now receives HTTP 401. 5. Retain the old key securely for a defined rollback window, but do not leave it active in Core. A rollback must first re-add the old key to Core, verify both keys, and only then roll back a backend. Destroy the old key after the window. Never replace the old key in Core before all Core instances accept the new key, and never remove it while any backend still uses it. --- # Add a base path Source: https://supertokens.com/docs/platform-configuration/supertokens-core/base-path ## Overview If you cannot add a dedicated subdomain for Core, you can add a base path to all Core APIs. To do this, you have to make changes to the core's configuration as well as to the backend SDK's `init` function call. :::danger A base path changes routing only. It does not authenticate requests, hide Core, or provide access control. Keep Core on a private network reachable only by trusted backend services. If broader network reachability is unavoidable, also require an API key, terminate TLS at a trusted edge, and restrict ingress with firewall or security-group rules. ::: Consider an example where the core resides on `http://localhost:3567/some-prefix`. This implies that all APIs exposed by the core are on `http://localhost:3567/some-prefix/*`. ## Before you start :::warning This page is only relevant if you are self-hosting SuperTokens. ::: The feature is only available for Core versions `>= 3.9`. ## Steps ### 1. Change the core configuration ```bash docker run \ -p 127.0.0.1:3567:3567 \ -e BASE_PATH="/some-prefix" \ -d "$SUPERTOKENS_IMAGE" ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command base_path: "/some-prefix" ``` ### 2. Change the backend SDK initialization ```tsx import supertokens from "supertokens-node"; supertokens.init({ supertokens: { connectionURI: "http://localhost:3567/some-prefix", // ... }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ /* ... */ ], }); ``` ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:3567/some-prefix", }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo, SupertokensConfig init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='http://localhost:3567/some-prefix', ), framework='...', recipe_list=[ #... ] ) ``` :::note[You can even set different base paths for different core instances:] - For each of the core's configs you need to supply their base path as mentioned in step 1 - The connection URI should be something like `"/;/"`. For example, a valid connection URI is `"http://localhost:3567/some-prefix;http://localhost:3567/some-prefix-2"`. ::: --- # CLI Source: https://supertokens.com/docs/platform-configuration/supertokens-core/cli ## Overview The SuperTokens CLI has the allows you to manage your core instance from the command line. ```bash supertokens [command] [--help] [--version] ``` :::note[If you are using Windows, you can only use the SuperTokens CLI using a terminal with Administrator privilege.] ::: ## Commands ### Start Start an instance of SuperTokens. By default, the process starts as a daemon. ```bash supertokens start [options] ``` #### Options | Option | Description | Example | |--------|-------------|---------| | `--with-space` | Sets the amount of space, in MB, to allocate to the `JVM`. | `supertokens start --with-space=200` allocates 200MB for the `JVM` | | `--with-config` | Specify the location of the configuration file to load. Can be either relative or absolute. | `supertokens start --with-config=/usr/config.yaml` | | `--port` | Sets the port on which this instance of SuperTokens should run. | `supertokens start --port=8080` | | `--host` | Sets the host on which this instance of SuperTokens should run. | `supertokens start --host=192.168.0.1` | | `--foreground` | Runs this instance of SuperTokens in the foreground (not as a daemon). | `supertokens start --foreground` | | `--help` | Help for this command. | `supertokens start --help` | ### List List information about all running SuperTokens instances. ```bash supertokens list [options] ``` ### Stop ```bash supertokens stop [options] ``` If you do not provide options, the command stops all instances, or it stops one specific instance of SuperTokens. #### Options | Option | Description | Example | |--------|-------------|---------| | `--id` | Stop an instance of SuperTokens that has a specific `PID`. You can obtain an instance's `PID` via the `supertokens list` command. | `supertokens stop --id=7634` | | `--help` | Help for this command. | `supertokens stop --help` | ### Uninstall Uninstalls SuperTokens ```bash supertokens uninstall [options] ``` #### Manual uninstall ##### 1. Stop or kill all SuperTokens processes ```bash supertokens stop ``` ##### 2. Delete the installation directory You can find out the installation directory by running ```supertokens --help```. ##### 3. Delete the SuperTokens script - Linux: ```/usr/bin/supertokens``` - Mac: ```/usr/local/bin/supertokens``` - Windows: ```C:\Windows\System32\supertokens.bat``` --- # Filter requests based on IP address Source: https://supertokens.com/docs/platform-configuration/supertokens-core/ip-allow-deny ## Overview You can configure SuperTokens Core to allow or deny requests from specific directly connected peer addresses. :::warning Core evaluates the address of the peer connected to it. It does not establish trust in `X-Forwarded-For`. Behind a reverse proxy, Core normally sees the proxy address, not the original client address. Filter client IPs at a trusted proxy or firewall; use Core's filter only for the backend or proxy addresses that connect directly to Core. Keep Core private and use API-key authentication as defense in depth. ::: ## Before you start :::warning This page is only relevant if you are self-hosting SuperTokens. The option is not available if you are using the managed version of SuperTokens due to security reasons. In this case, you have to configure the filtering mechanism in your backend server. ::: --- ## Allow requests ```bash docker run \ --network app-network \ -e IP_ALLOW_REGEX="^10\.0\.0\.12$" \ -d "$SUPERTOKENS_IMAGE" ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command ip_allow_regex: '^10\.0\.0\.12$' ``` The example allows only a backend whose directly connected private address is exactly `10.0.0.12`. Replace it with a stable private address assigned to your backend or trusted proxy. The anchors prevent partial matches and each dot is escaped so that it means a literal dot. To allow exact backend addresses, escape IPv4 dots and anchor the alternatives. For example: `^(100\.12\.12\.3|192\.167\.4\.3|50\.32\.5\.1)$`. If this value is not set, then the core allows requests from any IP address. --- ## Deny requests This is the opposite of the above configuration. If you only set this, the core allows requests from any IP other than the one that matches the regular expression corresponding to this setting. ```bash docker run \ --network app-network \ -e IP_DENY_REGEX="^10\.0\.0\.99$" \ -d "$SUPERTOKENS_IMAGE" ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command ip_deny_regex: '^10\.0\.0\.99$' ``` The above setting makes Core accept requests from any directly connected peer other than exactly `10.0.0.99`. For that address, it returns a `403`. :::info[What if you set both the configurations?] In this case, Core allows a request only if it matches `ip_allow_regex` and does not match `ip_deny_regex`. ::: --- # Add passwords to an existing account Source: https://supertokens.com/docs/post-authentication/account-linking/add-passwords-to-an-existing-account ## Overview There may be scenarios in which you want to add a password to an account created using a social provider or passwordless login. This guide walks you through how to do this. The idea here is to reuse the existing sign up APIs, but call them with a session's access token. The APIs then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and this guide goes through them as well. ## Before you start We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses on which APIs to call from your own UI. The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. ## Steps ### 1. Enable account linking and `emailpassword` on the backend SDK :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init(), AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any, ) => { if (user === undefined) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } if (session !== undefined && session.getUserId() === user.id && session.getTenantId() === tenantId) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), ], }); ``` ```python from typing import Any, Dict, Optional, Union from supertokens_python.recipe import accountlinking, emailpassword from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldAutomaticallyLink, ShouldNotAutomaticallyLink, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import User async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if user is None: return ShouldAutomaticallyLink(should_require_verification=True) if ( session is not None and session.get_user_id() == user.id and session.get_tenant_id() == tenant_id ): return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() recipe_list = [ emailpassword.init(), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), ] ``` The callback allows a new user to become a primary user when `user` is absent. It links to an existing user only when the session user and tenant match the proposed primary user and current tenant. It therefore does not enable linking between existing users during first-factor authentication. To enable that behavior, see the [automatic account linking documentation](./automatic-account-linking). ### 2. Create a UI to show a password input to the user and handle the submit event :::note If you want to use password based auth as a second factor, or for step up auth, see the docs in the [MFA recipe](/additional-verification/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. ::: First, you need to detect if there already exists a password for the user. You can do this by inspecting the [user object](/references/backend-sdks/user-object) on the backend and checking if there is an `emailpassword` login method. Then, if no such login method exists, you have to show a UI in which the user can add a password to their account. The [password validation documentation](/authentication/email-password/customize-the-sign-up-form#change-field-validators) contains the default password validation rules. You also need to fetch a verified email for the current tenant before you call the email-password sign-up API. Fetch it on the backend from a login method on the user object whose `tenantIds` contains the session tenant. Do not accept an email from the client as proof of ownership. If no tenant-scoped, verified email exists, first complete an email OTP flow through the passwordless recipe and link that login method to the same session user. Once you have the email on the frontend, you should call the sign up API. The two big differences in the implementation are: - When you call the sign up API, you need to provide the session's access token in the request. If you are using the frontend SDK, this process happens automatically via the frontend network interceptors. The access token enables the backend to get a session and then link the email password account to session user. - New types of failure scenarios exist when calling the sign up API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#error-status-codes) (> `ERR_CODE_008`). ### 3. Check for email match in the backend sign up API Since the frontend specifies the email, verify its ownership on the backend before using it. The email must belong to a verified login method for the session user in the request tenant. You can enforce this by overriding the email-password sign-up API: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signUpPOST: async function (input) { if (input.session !== undefined) { // this means that we are trying to add a password to the session user const inputEmail = input.formFields.find((field) => field.id === "email")?.value; if (typeof inputEmail !== "string") { return { status: "GENERAL_ERROR", message: "A valid email is required", }; } const sessionUserId = input.session.getUserId(); const tenantId = input.tenantId; if (input.session.getTenantId() !== tenantId) { return { status: "GENERAL_ERROR", message: "Cannot add a password across tenants", }; } const userObject = await SuperTokens.getUser(sessionUserId); const ownsVerifiedEmail = userObject?.loginMethods.some( (loginMethod) => loginMethod.tenantIds.includes(tenantId) && loginMethod.verified && loginMethod.hasSameEmailAs(inputEmail), ); if (!ownsVerifiedEmail) { return { status: "GENERAL_ERROR", message: "Cannot use this email to add a password for this user", }; } } return await originalImplementation.signUpPOST!(input); }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```python from typing import Any, Dict, List, Optional, Union from supertokens_python.asyncio import get_user from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, EmailAlreadyExistsError, SignUpPostNotAllowedResponse, SignUpPostOkResult, ) from supertokens_python.recipe.emailpassword.types import FormField from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import GeneralErrorResponse def override_emailpassword_apis(original_implementation: APIInterface) -> APIInterface: original_sign_up_post = original_implementation.sign_up_post async def sign_up_post( form_fields: List[FormField], tenant_id: str, session: Optional[SessionContainer], should_try_linking_with_session_user: Optional[bool], api_options: APIOptions, user_context: Dict[str, Any], ) -> Union[ SignUpPostOkResult, EmailAlreadyExistsError, SignUpPostNotAllowedResponse, GeneralErrorResponse, ]: if session is not None: input_email = next(field.value for field in form_fields if field.id == "email") user = await get_user(session.get_user_id(), user_context) owns_verified_email = user is not None and any( tenant_id in login_method.tenant_ids and login_method.verified and login_method.has_same_email_as(input_email) for login_method in user.login_methods ) if session.get_tenant_id() != tenant_id or not owns_verified_email: return GeneralErrorResponse( message="Cannot use this email to add a password for this user" ) 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 emailpassword.init( override=emailpassword.EmailPasswordOverrideConfig( apis=override_emailpassword_apis ) ) ``` --- ## See also --- # Automatic account linking Source: https://supertokens.com/docs/post-authentication/account-linking/automatic-account-linking ## Overview Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. At a high level, SuperTokens can automatically link accounts for different login methods when: - Their emails or phone numbers are the same. - The new login method has a verified identifier when your callback returns `shouldRequireVerification: true`. SuperTokens applies account-takeover checks before linking. Your callback remains part of that security boundary, especially if you disable verification. ## Before you start ## Steps ### 1. Enable the recipe You can enable this feature by providing the following callback implementation on the backend SDK: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; // Prevent account linking if the user already exists in your database function checkIfUserHasAssociatedInformation( accountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, ): boolean { if (!accountInfo.recipeUserId || !user) return false; const userId = accountInfo.recipeUserId.getAsString(); const hasAssociatedInformation = false; return hasAssociatedInformation; } supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any, ) => { if ( session !== undefined && (user === undefined || session.getUserId() !== user.id || session.getTenantId() !== tenantId) ) { return { shouldAutomaticallyLink: false, }; } // This step is required if you are saving user information in your own database. const hasAssociatedInformation = checkIfUserHasAssociatedInformation(newAccountInfo, user); if (hasAssociatedInformation) { return { shouldAutomaticallyLink: false, }; } return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; }, }), ], }); ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking from supertokens_python.types import User from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import AccountInfoWithRecipeIdAndUserId, ShouldNotAutomaticallyLink, ShouldAutomaticallyLink from typing import Dict, Any, Optional, Union # Prevent account linking if the user already exists in your database async def check_if_user_has_associated_information(account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User]) -> bool: if not account_info.recipe_user_id or not user: return False _user_id = account_info.recipe_user_id.get_as_string() # Add your own implementation here has_associated_information = False return has_associated_information async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if session is not None and ( user is None or session.get_user_id() != user.id or session.get_tenant_id() != tenant_id ): return ShouldNotAutomaticallyLink() has_associated_information = await check_if_user_has_associated_information(new_account_info, user) # This step is required if you are saving user information in your own database. if has_associated_information: return ShouldNotAutomaticallyLink() return ShouldAutomaticallyLink(should_require_verification=True) init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework="fastapi", recipe_list=[ accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` | Argument | Type | Description | |----------|------|-------------| | `newAccountInfo` | `AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }` | Contains information about the user whose account is going to link or become a primary user. Includes email, social login info, phone number, WebAuthn credential IDs, and login method (`emailpassword`, `thirdparty`, `passwordless`, or `webauthn`). May contain `recipeUserId` during account linking. When `newAccountInfo.recipeUserId !== undefined && user !== undefined`, check whether that recipe user ID has associated data in your application database to prevent data loss. | | `user` | `User \| undefined` | If not `undefined`, indicates `newAccountInfo` user links to this user. If `undefined`, `newAccountInfo` user becomes a primary user. | | `session` | `SessionContainerInterface \| undefined` | Session object of the user who is linking. `undefined` for first factor login. Defined if user completed first factor and calls sign up/in API again (MFA or social login linking). | | `tenantId` | `string` | ID of the tenant the user is signing in or signing up to. Account matching and linking are scoped to this tenant. | | `userContext` | `any` | User-defined context object. | | Argument | Type | Description | |----------|------|-------------| | `shouldAutomaticallyLink` | `boolean` | If `true`, `newAccountInfo` links or becomes primary user during API call (subject to security checks). If `false`, no account linking operation occurs. | | `shouldRequireVerification` | `boolean` | If `true`, account linking only happens after the new login method's matching identifier is verified. Keep this `true` unless your backend has independently verified ownership of that identifier; client-provided values are not ownership evidence. |
:::note If you return `shouldRequireVerification: true`, enable the [email verification recipe](/additional-verification/email-verification/initial-setup). `REQUIRED` mode prevents access until a login method that does not inherently verify its email, such as email-password, completes verification; linking is then retried. A provider login whose backend-validated response marks the email verified can link immediately. If you enable email verification in `OPTIONAL` mode, the user can access the account after email password login. However, account linking only occurs after they verify their email later on. This is risky because while the user had access to their email password account after sign up, they could lose access after verification and account linking completes due to the change in the primary user ID. A callback is available to help migrate data from one user ID to another. ::: You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. Do not use a client-provided email, phone number, provider user ID, or WebAuthn credential ID to authorize linking. Derive identifiers from the authenticated provider, WebAuthn ceremony, or backend user record. If `session` is present, only authorize a session-driven link when `session.getUserId()` equals `user.id` and `session.getTenantId()` equals `tenantId`. Returning `false` preserves both existing users; conflict statuses never transfer a login method between primary users. ## References ### Automatic account linking scenarios #### During sign up If there exists another account with the same email or phone number within the current tenant, the new account links to the existing account if: - The existing account is a primary user - If `shouldRequireVerification` is `true`, the new account needs creation via a method that has the email as verified (for example via passwordless or google login). If the new method doesn't inherently verify the email (like in email password login), the accounts link post email verification. - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. #### During sign in If the current user is not already linked and if there exists another user with the same email or phone number within the current tenant, the accounts link if: - The user signing into is not a primary user, and the other user with the same email / phone number is a primary user - If `shouldRequireVerification` is `true`, the current account (that's signing into) has its email as verified. - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. #### After email verification If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then the two accounts link if: - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. :::info For a primary user, if two login methods (L1 & L2) share the same email, but L1's email verifies and L2's does not, SuperTokens automatically verifies L2's email under these conditions: - The user logs in with L2. - The `updateEmailOrPassword` (email password recipe) or `updateUser` (passwordless recipe) function calls to update L2's email to match L1's. ::: #### During the password reset flow If there already exists a user with the same email in a non email password recipe (social login for example), and the user is doing a password reset flow, a new email password user creates and links to the existing account if: - The non email password user is a primary user. - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. :::info[Email update implications] When updating a user's login email, SuperTokens ensures account linking conditions remain valid. A primary user's email cannot update to match another primary user's email. User A has login methods `AL1` (email `e1`) and `AL2` (email `e1`). User B has login methods `BL1` (email `e2`) and `BL2` (email `e3`). Updating `AL1`'s email to `e2` or `e3` is not allowed, as it would create two primary users with the same email. **Email updates occur in these scenarios:** * `updateEmailOrPassword` function (email password recipe) * `updateUser` function (passwordless recipe) * Social login (if email from provider has changed) If the update violates account linking rules, the operation fails: * Function calls return a status indicating the update was impossible. * Social login API calls return a status prompting the user to contact support. ::: ### User data changes during account linking When two accounts link, the primary user ID of the non primary user changes. For example, if User A has a primary user ID `p1` and user B, which is a non primary user, has a user ID of `p2`, and they link, then the primary user ID of User B changes to `p1`. This has an effect that if the user logs in with login method from User B, the `session.getUserId()` returns `p1`. If there was any older data associated with User B (against user ID `p2`), in your database, that data essentially becomes "lost". To prevent this scenario, you should: - Make sure that you return `false` for `shouldAutomaticallyLink` boolean in the `shouldDoAutomaticAccountLinking` function implementation if there exists a `recipeUserId` in the `newAccountInfo` object, and if you have some information related to that user ID in your own database. This appears in the [code snippet above](#1-enable-the-recipe). - If you do not want to return `false` in this case, and want the accounts to link, then make sure to implement the `onAccountLinked` callback:
```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any, ) => { return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; }, onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { let olderUserId = newAccountInfo.recipeUserId.getAsString(); let newUserId = user.id; // TODO: migrate data from olderUserId to newUserId in your database... }, }), ], }); ``` :::warning[`onAccountLinked` does not provide rollback] SuperTokens calls `onAccountLinked` after the Core has linked the accounts. If the callback throws, the link remains, the callback is not retried automatically, and the API returns `500`. Make data migration idempotent and record enough state to reconcile failures asynchronously. Retrying login does not rerun this callback for an already-linked account. ::: ### Error status codes The following codes can appear in general errors shown by the pre-built UI. Released Node.js 24.0.3 uses these families: | Recipe or operation | Codes | |---|---| | Password reset/recovery protection | `ERR_CODE_001` | | Passwordless sign-in/up and session linking | `ERR_CODE_002`, `ERR_CODE_003`, `ERR_CODE_017`–`ERR_CODE_019` | | Third-party sign-in/up and session linking | `ERR_CODE_004`–`ERR_CODE_006`, `ERR_CODE_020`–`ERR_CODE_024` | | Email-password sign-in/up and session linking | `ERR_CODE_007`–`ERR_CODE_016` | | WebAuthn sign-up and session linking | `ERR_CODE_025`–`ERR_CODE_029` | | WebAuthn sign-in and session linking | `ERR_CODE_030`–`ERR_CODE_034` | For session-linking families, the consecutive codes distinguish verification required, a recipe user already linked to another primary user, account information already associated with another primary user, and session-user account information already associated with another primary user. Do not parse the message text; handle the API status and show the reason as a support-safe error. - This can happen during creating a password reset code in the email password flow: - API path and method: `/user/password/reset/token POST` - Output JSON: ```json { "status": "PASSWORD_RESET_NOT_ALLOWED", "reason": "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" } ``` - The pre-built UI on the frontend displays this error in the following way: pre-built UI screenshot showing error message for ERR_CODE_001. - Below is the scenario for when this status returns: A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (`emailpassword` login). If user A changes their `emailpassword` email to `e2` (which is in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If this happens, and the victim resets the password (since they are the real owner of the email), then they can login to the account, and the attacker can spy on what the user is doing via their third party login method. To prevent this scenario, enforcement ensures that the password link is only generated if the primary user has at least one login method that has the input email ID and verifies it, or if not, checks that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then the system returns the error code `ERR_CODE_001`. - To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **You can do these actions using the user management dashboard.** - This can happen during the passwordless recipe's create or consume code API (during sign up): - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)" } ``` - The pre-built UI on the frontend displays this error in the following way: pre-built UI screenshot showing error message for ERR_CODE_002. - Below is an example scenario for when this status returns (one amongst many): A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which remains unverified (owned by an attacker). If this scenario occurs, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they signed up, they do not get suspicious), and then the attacker's login method links to the passwordless login method. This way, the attacker gains access to the user's account. To prevent this, sign up with passwordless login is not allowed in case there exists another account with the same email and remains unverified. - To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using the user management dashboard.** - This can happen during passwordless code consumption when sign-in is blocked to prevent unsafe account linking: - API path and method: `/signinup/code/consume POST` - Output status: `SIGN_IN_UP_NOT_ALLOWED` - Ask the user to use another login method that is already associated with the account or contact support. Do not bypass the check based on client-provided account information. - This can happen during the third party recipe's `/signinup` API (during sign in): - API path and method: `/signinup POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error for message ERR_CODE_004. - Below is an example scenario for when this status returns (one amongst many): There exists a `thirdparty` user with email `e1`, sign in with Google (owned by the victim, and the email is verified). There exists another `thirdparty` login method with email, `e2` (owned by an attacker), such as login with GitHub. The attacker then goes to their GitHub and changes their email to `e1` (which is in unverified state). The next time the attacker tries to login, via GitHub, they see this error code. Login is prevented, because if it wasn't, then the attacker might send an email verification link to `e1`, and if the victim clicks on it, then the attacker's account will link to the victim's account. - To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using the user management dashboard.** - This can happen during the third party recipe's `signinup` API (during sign in): - API path and method: `/signinup POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_005. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, third party user with email `e1`, sign in with Google. There exists another email password user with email `e2`, which is a primary user. If the user changes their email on Google to `e2`, and then try logging in via Google, they see this error code. This occurs because if it wasn't, then it would result in two primary users having the same email, which violates one of the account linking rules. - To resolve this issue, you can make one of the primary users as non primary (use the unlink button against the login method on the user management dashboard). Once the user is not a primary user, you can ask the user to re-login with that method, and it should auto link that account with the existing primary user. - This can happen during the third party recipe's `signinup` API (during sign up): - API path and method: `/signinup POST` - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_006)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_006. - Below is as example scenario for when this status returns (one amongst many): A user is trying to sign up using third party login method with email `e1`. There exists an email password login method with `e1`, which remains unverified (owned by an attacker). If the third party sign up is allowed, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they signed up, they do not get suspicious), and then the attacker's login method links to the third party login method. This way, the attacker has access to the user's account. To prevent this, sign up with third party login is not allowed in case there exists another account with the same email and remains unverified. - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using the user management dashboard.** - This can happen during the email password sign up API: - API path and method: `/signup POST` - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_007. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, social login account with email `e1`, sign in with Google. If an attacker tries to sign up with email password with email `e1`, the system sends an email verification email to the victim, and they may click it since they had previously signed up with Google. This links the attacker's account to the victim's account. - To resolve this issue, you can ask the user to try and login, or go through the reset password flow. - This can happen during the email password sign in API: - API path and method: `/signin POST` - Output JSON: ```json { "status": "SIGN_IN_NOT_ALLOWED", "reason": "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)" } ``` - The pre-built UI on the frontend displays this error in the following way: Pre-built UI screenshot showing error message for ERR_CODE_008. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, social login account with email `e1`, sign in with Google. There also exists an email password account (owned by the attacker) that remains unverified with the same email `e1` (this is not a primary user). If the attacker tries to sign in with email password, they see this error. This occurs because if it wasn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the system links the attacker's account to the victim's account. - To resolve this issue, you can ask the user to try the reset password flow. - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" } ``` - An example scenario of when in the following scenario: - Let's say that the app configures to not have automatic account linking during the first factor. - A user creates an email password account with email `e1`, verifies it, and links social login account to it with email `e2`. - The user logs out, and then creates a social login account with email `e1`. Then, they receive a request to add a password to this account. Since an email password account with `e1` already exists, SuperTokens tries and links that to this new account, but fails, since the email password account with `e1` is already a primary user. - To resolve this, it is recommended to manually link the `e1` social login account with the `e1` email password account. Alternatively, enable automatic account linking for first factor to prevent the above scenario. - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" } ``` - An example scenario of when in the following scenario: - A user creates a social login account with email `e1` which becomes a primary user. - The user logs out, and creates another social login account with email `e2`, which also becomes a primary user. - The user receives a request to add a password for the new account with an option to also specify an email with it (this is strange, but theoretically possible). They enter the email `e1` for the email password account. - This causes this type of error since the linking of the new social login and email account fails since there already exists another primary user with the same (`e1`) email. - To resolve this, it is recommended not allowing users to specify an email when asking them to add a password for their account. - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { "status": "SIGN_UP_NOT_ALLOWED", "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is configured to not have automatic account linking during the first factor. - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. - The user logs out and creates another social login account with email `e1` (say GitHub), and then tries and adds a password to this account with email `e1`. Here, SuperTokens tries and makes the GitHub login a primary user, but fails, since the email `e1` is already a primary user (with Google login). - To resolve this, it is recommended that you manually link the `e1` GitHub social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" } ``` - This can happen when the third party account that is trying to link to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). - Only allow users to link provider accounts whose identifiers the provider marks as verified. Return `shouldRequireVerification: false` only if your backend has independently verified ownership; client input is not sufficient evidence. - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" } ``` - This can happen when the third party account that is trying to link to the session's account is already linked with another primary user. - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" } ``` - This can happen when the third party account that is trying to link to the session's account has the same email as another primary user. - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" } ``` - To link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error returns to the frontend. - This happens during third party sign in, when the user is trying to sign in with a non-primary user, and the third party provider does not verify their email, and their exists a primary user with the same email. This can also happen the other way around wherein the user is trying to sign in with the primary user (unverified email), and there exists a non-primary user with the same email. - API Path is `/signinup POST`. - Output JSON: ```json { "status": "SIGN_IN_UP_NOT_ALLOWED", "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_024)" } ``` - You can resolve this by deleting the (non primary) user that has the same email ID, or by manually marking the email of the user as verified for the login method that they are trying to sign in with. #### Changing the error message on the frontend If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](/references/frontend-sdks/prebuilt-ui/translations). --- ## See also --- # Important concepts Source: https://supertokens.com/docs/post-authentication/account-linking/important-concepts ## Overview The following page describes concepts that are relevant towards understanding how account linking works in **SuperTokens**. ## References Each authentication recipe has a unique *user* object. ### Primary and non-primary users The system identifies a primary or a non-primary user by the `isPrimaryUser` boolean within the user object. The primary user ID remains constant when accounts link to it. A user can become a primary user only if no other primary users share the same email, third-party information, or phone number across all tenants. This applies to all tenants to which the user belongs. Hence, two primary users with the same email address, one using email/password login and the other using social login. :::info[Multi-tenancy] Additionally, the following scenario is not permitted in a multi-tenant context: - User A is a primary user with email `test@example.com` and belongs to tenants `t1` and `t2`. - User B is a primary user with email `test@example.com` and belongs to tenant `t2`. This is not allowed because there are two primary users with the same email in tenant `t2`. ::: For accounts to link, one user must be a primary user. The resulting user ID of the linked accounts becomes the primary user's ID. For example, if User A is a primary user with user ID `u1` and links to User B (a non-primary user) with user ID `u2`, the resulting user's primary ID becomes `u1`. The recipe ID remains `u1` for User A and `u2` for User B. **Tenant Considerations:** * When designating a user as a primary user, the system verifies the primary user condition (as defined above) across all tenants to which the user belongs. * When linking two accounts, the primary user condition and account linking condition must satisfy across the union of all tenants to which the primary and non-primary users belong. For example, if User A (tenant `t1`, `t2`) links to User B (tenant `t3`), the system checks the conditions across `t1`, `t2`, and `t3`. ### Primary user ID and recipe user ID For most purposes, you care about the user's primary user ID. For example, when a user with two login methods, email password and social login, logs in, you get back the same primary user ID when you get their user ID from the session (or read the `sub` claim in the JWT). However, if you want to identify what the login method used for the current session is, you can use the session's `recipeUserId`. Then you can compare its value to the `recipeUserId` in each of the `loginMethods` in the user object. Some functions from the backend SDK also accept a `recipeUserId` as a parameter. For example, the `updateEmailOrPassword` function from the `emailpassword` recipe takes in a `recipeUserId` to determine which login method needs the update. If it took a `string` user ID instead, and you passed it a user's primary user ID, it may unintentionally lead to updating the wrong login method's email or password. It may also throw an error if the primary user is not an email password user. ### User unlinking User unlinking is the process of removing a login method from a user. For example, if a user has both email password and social login, and they want to remove their social login, you can use the unlinking function from the backend SDK. A few scenarios exist here: 1. If unlinking a login method that **is not** associated with the primary user, it results in two users: one as the primary user and the other as the non-primary user. For example, if User A (primary user, with email password login) links with User B (social login), and then you unlink User B, this results in two separate users: User A (primary user), with one login method (email password) and User B (non-primary user) with social login. The primary user ID of user B changes to be equal to their recipe user ID. 2. If you unlink a login method associated with the primary user, it deletes the login method of the primary user ID. For example, if User A (primary user, with email password login) links with User B (social login), and then you unlink User A, this results in the deletion of the email password user. Only User B remains, which is a social login user, and its primary user ID equals User A's primary user ID (even though the system deleted the login method for A). Any metadata, role, sessions info continues to exist. 3. If unlinking a User A which is a primary user ID, but it has not linked users, it results in this user becoming a non-primary user. :::note[All the above checks happen automatically. You don't need to worry about them. But it is important to understand what's happening.] ::: ## Security Below is the list of all points in time when account linking occurs, and for each point, you can see the list of security checks that happen: ### During sign up #### First case - Email is `e1` - Email verification: `false` (is the case with email password sign up or social login with a provider that does not require email verification) ##### Checks done: - If there is no primary user with the same email, then sign up is not allowed if there exists any other non-primary account with the same email and that account is not verified. This occurs because if not, there is a risk that if this user signs up and becomes a primary user, the other account (which could be malicious), might resend a verification email. The user might click on it (since they signed up) and verify the malicious account, thereby linking it to their account. This way, the malicious user gains access to the victim's account. - If there exists a primary user with the same email, then the system rejects the sign up of this new user. This occurs because if allowed, and this sign up is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the new, malicious account to link, thereby giving the malicious user access to the victim's account. ###### What users see: - In case of email password login, users see an account already exists error. In this case, they can try logging in with another method, or go through the password reset flow, which creates a new email password account for them as well as verify it. - In case of social login, users see that they should try a different login method for security reasons. #### Second case - Email is `e1` - Email verified: `true` (this is the case with social login with a provider that requires email verification, like Google, or it could be a passwordless sign up) ##### Checks done: - Same as in the first case. - If there exists a primary user with the same email, then the system allows sign up only if there exists at least one login method in the primary user with email `e1` which the system has verified. This occurs because if not, then the following account takeover is possible: - Malicious user signs up with email password, with email `e2`, verifies it, and becomes a primary user. - They then change their email to `e1`, and keep it in an unverified state. - The actual user (victim) does a Google sign in with email `e1`. - If this sign up is not stopped, then the new sign up links to the primary user, and the malicious user gains access to the victim's account. ##### What users see: - Users see that they should try a different login method for security reasons. ### During sign in #### First case - Email is `e1` - Email verified: `false` - User is not a primary user ##### Checks done: - If there exists another user with the same email, and they are not a primary user, but their email remains unverified, the system disallows this sign in because if it allowed it, and this user verifies their email, it results in this user becoming a primary user. If the other account then sends an email verification email, this user may click on it (they may not get too suspicious), and verify the other account, thereby linking it to their account. This way, the other account, which may belong to a malicious user, gains access to the victim's account. - If there exists another user with the same email, and they are not a primary user, the system disallows signing in. This occurs because if allowed, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the new, malicious account to link, thereby giving the malicious user access to the victim's account. ##### What users see: - For email password sign in, users see a wrong credentials error message. This prompts them to go through the password reset flow, which also marks the email as verified, thereby allowing them to sign in. This also blocks sign ins from malicious users, since the new password is only known to the actual owner of the email. - For passwordless or social login, users see that they should try a different login method for security reasons. #### Second case - The user first does a social login sign up with email `e1`. - They then change their email to `e2` on the provider and tries signing in again. - The third party provider does not verify emails, resulting in `e2` remaining unverified. ##### Checks done: - If the social login user is **not** a primary user, and there exists another primary user with email `e2`, then the system disallows sign in here. This occurs because if allowed, and this sign in is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the new, malicious account to link, thereby giving the malicious user access to the victim's account. - If this user is a primary user, and there exists another primary user with email `e2`, the system disallows sign in because there can't be two primary users with the same email. The system rejects the email update which happens during sign in for social login. ##### What users see: - For the first point, users see a message asking them to try a different login method, or contact support for security reasons. - For the second case, users see that email update is not allowed and to contact support. ### During the password reset flow - Malicious user has email password and a social login account with email `e1`, and the system links them both. - They then change their email to `e2` for the email password login, which belongs to the victim. - Actual owner of `e2` tries to sign up, but sees that their account already exists (Case 1), they then try to sign in, but can't cause they don't know the password. They try the password reset flow. #### Checks done: - In this case, we deny generating the password reset token because if we did, then the real user would change the password of the email password account, and also mark it as verified. They would have access to the account, however, the malicious user could also then login using social login (with email `e1`) to access the same account. During password reset, the system does not generate a token if the email password account for that email associates with a primary account that also has other emails / phone numbers. If the email for which the password is being reset is not verified for any of the login methods in that primary user, the token is not generated. #### What users see: - They see a message telling them that the reset password link was not generated because of account takeover risk, and to contact support. ### During the email update flow: - A user has email `e1`, and they want to change it to email `e2` #### Checks done: - If the user's account is not a primary user, and there exists another primary user with email `e2`, then the system disallows email update here. This occurs because if allowed, and this email update is from a malicious user, then the actual user (the primary user owner) may get an email for verification, and might click it (since they did sign up previously). This causes the system to link the new, malicious account, thereby giving the malicious user access to the victim's account. - If this user is a primary user, and there exists another primary user with email `e2`, the system disallows email update because there can't be two primary users with the same email. #### What users see: - If the email update is happening during sign in of social login, users see a message that email update is not allowed and to contact support. - If this is happening post login (from a settings page), then you can send any message you want to the user, since this would be your custom API. --- ## See also --- # Introduction Source: https://supertokens.com/docs/post-authentication/account-linking/introduction ## Overview Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account. They may want to link both of these accounts to the same user account in your application. Account linking can occur either automatically or manually. The first method happens during user sign up. If a user signs up with a second login method with the same email or phone, the two accounts are automatically linked. The other one involves you setting up the linking process yourself. ## Getting started Before you going into the tutorials, read through the **Important concepts** page. It teaches you about the main things that you need to know in the context of account linking. After that, based on your use case, check either the automatic or manual account linking guides. Learn about the main concepts that you need to know in the context of account linking. See how to enable and use the automatic account linking feature. Implement the account linking feature manually. ## Customization See how you can manually link multiple social accounts under the same user. See how you can add passwords to an existing account. --- # Link social accounts Source: https://supertokens.com/docs/post-authentication/account-linking/link-social-accounts ## Overview The following guide shows you how to link a social account to an existing user account. The idea here is to reuse the existing sign up APIs, but call them with a session's access token. The APIs then create a new recipe user for that login method based on the input, and then link that to the session user. Of course, there are security checks done to ensure there is no account takeover risk, and this guide goes through them as well. ## Before you start We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses on which APIs to call from your own UI. The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. ## Steps ### 1. Enable account linking on the backend SDK :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ AccountLinking.init({ shouldDoAutomaticAccountLinking: async ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any, ) => { if (user === undefined) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } if (session !== undefined && session.getUserId() === user.id && session.getTenantId() === tenantId) { return { shouldAutomaticallyLink: true, shouldRequireVerification: true, }; } return { shouldAutomaticallyLink: false, }; }, }), ], }); ``` ```python from typing import Any, Dict, Optional, Union from supertokens_python.recipe import accountlinking from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, ShouldAutomaticallyLink, ShouldNotAutomaticallyLink, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.types import User async def should_do_automatic_account_linking( new_account_info: AccountInfoWithRecipeIdAndUserId, user: Optional[User], session: Optional[SessionContainer], tenant_id: str, user_context: Dict[str, Any], ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: if user is None: return ShouldAutomaticallyLink(should_require_verification=True) if ( session is not None and session.get_user_id() == user.id and session.get_tenant_id() == tenant_id ): return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ) ``` The callback allows a new user to become a primary user when `user` is absent. It links to an existing user only when the session user and tenant match the proposed primary user and current tenant. It therefore does not enable linking between existing users during first-factor authentication. To enable that behavior, see [the automatic account linking page](./automatic-account-linking). ### 2. Create a UI to show social login buttons and handle login First, you need to detect which social login methods are already linked to the user. You can do this by inspecting the [user object](/references/backend-sdks/user-object) on the backend and checking the `thirdParty.id` property (the values are like `google`, `facebook` etc). Then you have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, redirect them to that provider's page. After login, the provider redirects the user back to your application (on the same path as the first factor login). You then call the APIs to consume the OAuth tokens and link the user. The exact implementation of the above is available [in the initial setup documentation](/authentication/social/initial-setup). The two big differences in the implementation are: - When you call the `signinup` API, you need to provide the session's access token in the request. If you are using the frontend SDK, the frontend network interceptors automatically handle this. The access token enables the backend to get a session and then link the social login account to session user. - New types of failure scenarios exist when calling the `signinup` API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#error-status-codes) (> `ERR_CODE_008`). ### 3. Access the social login access token and user profile on the backend Once you call the `signinup` API from the frontend, SuperTokens verifies the OAuth tokens and fetches the user's profile info from the third party provider. SuperTokens also links the newly created recipe user to the session user. To fetch the new user object and also the third party profile, you can override the `signinup` recipe function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import SuperTokens, { User } from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // override the thirdparty sign in / up function signInUp: async function (input) { let existingUser: User | undefined; if (input.session !== undefined && input.session.getTenantId() === input.tenantId) { existingUser = await SuperTokens.getUser(input.session.getUserId()); } let response = await originalImplementation.signInUp(input); if (response.status === "OK") { let accessToken = response.oAuthTokens["access_token"]; let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; if ( input.session !== undefined && input.session.getTenantId() === input.tenantId && response.user.id === input.session.getUserId() && existingUser !== undefined ) { if (response.user.loginMethods.length === existingUser.loginMethods.length + 1) { // new social account was linked to session user } else { // social account was already linked to the session // user from before } } } return response; }, }; }, }, }), Session.init({ /* ... */ }), ], }); ``` ```python from typing import Any, Dict, Optional from supertokens_python.recipe import thirdparty from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.thirdparty.interfaces import ( RecipeInterface, SignInUpOkResult, ) from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider def override_thirdparty_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: 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: Optional[bool], tenant_id: str, user_context: Dict[str, Any], ): existing_login_method_count = None if session is not None and session.get_tenant_id() == tenant_id: from supertokens_python.asyncio import get_user existing_user = await get_user(session.get_user_id(), user_context) if existing_user is not None: existing_login_method_count = len(existing_user.login_methods) result = 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, ) if ( isinstance(result, SignInUpOkResult) and session is not None and session.get_tenant_id() == tenant_id and result.user.id == session.get_user_id() and existing_login_method_count is not None ): _access_token = result.oauth_tokens.get("access_token") _provider_profile = result.raw_user_info_from_provider.from_user_info_api if len(result.user.login_methods) == existing_login_method_count + 1: pass # The provider account was linked to this session user. else: pass # The provider account was already linked to this session user. return result original_implementation.sign_in_up = sign_in_up return original_implementation thirdparty.init( override=thirdparty.ThirdPartyOverrideConfig( functions=override_thirdparty_functions ) ) ``` The checks bind custom logic to the same session user and tenant. The provider identifiers, tokens, and profile in these function results come from the backend-verified OAuth exchange; never use client-submitted provider identifiers as proof that the current user owns a social account. A conflict leaves the provider login method linked to its existing primary user and returns a linking error instead of moving it to the session user. --- ## See also --- # Manual account linking Source: https://supertokens.com/docs/post-authentication/account-linking/manual-account-linking ## Overview Manual account linking allows you to take control of when and which accounts link. With this, you can implement flows like: - Connecting social login accounts to an existing account post login. - Adding a password to an account that a social or passwordless login created. - Linking accounts which don't have the same email or phone number, or have a different identifier altogether. ## Steps ### 1. Initialize the account linking recipe :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ supertokens: { connectionURI: "...", apiKey: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [AccountLinking.init()], }); ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", ), framework='...', recipe_list=[ accountlinking.init() ], ) ``` In the above, SuperTokens does not automatically link accounts (during sign up or sign in APIs) by returning `shouldAutomaticallyLink: false`. Initializing the recipe is still important to use the functions from the SDK as shown below. It is of course possible to [enable auto account linking](./automatic-account-linking) and still use the functions for manual account linking below. ### 2. Create a primary user To link two accounts, you first need to make one of them a primary user: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import AccountLinking from "supertokens-node/recipe/accountlinking"; import { RecipeUserId } from "supertokens-node"; async function makeUserPrimary(recipeUserId: RecipeUserId) { let response = await AccountLinking.createPrimaryUser(recipeUserId); if (response.status === "OK") { if (response.wasAlreadyAPrimaryUser) { // The input user was already a primary user and accounts can be linked to it. } else { // User is now primary and accounts can be linked to it. } let modifiedUser = response.user; console.log(modifiedUser.isPrimaryUser); // will print true } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // This happens if there already exists another primary user with the same email or phone number // in at least one of the tenants that this user belongs to. } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { // This happens if this user is already linked to another primary user. } } ``` ```python from supertokens_python.recipe.accountlinking.asyncio import create_primary_user from supertokens_python.types import RecipeUserId async def make_user_primary(recipe_user_id: RecipeUserId): response = await create_primary_user(recipe_user_id) if response.status == "OK": if response.was_already_a_primary_user: # The input user was already a primary user and accounts can be linked to it. pass else: # User is now primary and accounts can be linked to it. pass modified_user = response.user print(modified_user.is_primary_user) # will print True elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # in at least one of the tenants that this user belongs to. pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR": # This happens if this user is already linked to another primary user. pass ``` ```python from supertokens_python.recipe.accountlinking.syncio import create_primary_user from supertokens_python.types import RecipeUserId def make_user_primary(recipe_user_id: RecipeUserId): response = create_primary_user(recipe_user_id) if response.status == "OK": if response.was_already_a_primary_user: # The input user was already a primary user and accounts can be linked to it. pass else: # User is now primary and accounts can be linked to it. pass modified_user = response.user print(modified_user.is_primary_user) # will print True elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # in at least one of the tenants that this user belongs to. pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR": # This happens if this user is already linked to another primary user. pass ``` ### 3. Link accounts Once a user has become a primary user, you can link other accounts to this user: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import AccountLinking from "supertokens-node/recipe/accountlinking"; import { RecipeUserId } from "supertokens-node"; // we are linking the input recipeUserId to the primaryUserId async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) { let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); if (response.status === "OK") { if (response.accountsAlreadyLinked) { // The input users were already linked } else { // The two users are now linked } let modifiedUser = response.user; console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // This happens if there already exists another primary user with the same email or phone number // as the recipeUserId's account. } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { // This happens if the input primaryUserId is not actually a primary user ID. // You can call createPrimaryUserId and call linkAccountsAgain } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // This happens if the input recipe user ID is already linked to another primary user. // You can call unlink accounts on the recipe user ID and then try linking again. } } ``` ```python from supertokens_python.recipe.accountlinking.asyncio import link_accounts from supertokens_python.types import RecipeUserId async def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId): response = await link_accounts(recipe_user_id, primary_user_id) if response.status == "OK": if response.accounts_already_linked: # The input users were already linked pass else: # The two users are now linked pass modified_user = response.user print(modified_user.login_methods) # this will now contain the login method of the recipeUserId as well. elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # as the recipeUserId's account. pass elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER": # This happens if the input primaryUserId is not actually a primary user ID. # You can call create_primary_user and call link_accounts again pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if the input recipe user ID is already linked to another primary user. # You can call unlink_accounts on the recipe user ID and then try linking again. pass ``` ```python from supertokens_python.recipe.accountlinking.syncio import link_accounts from supertokens_python.types import RecipeUserId def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId): response = link_accounts(recipe_user_id, primary_user_id) if response.status == "OK": if response.accounts_already_linked: # The input users were already linked pass else: # The two users are now linked pass modified_user = response.user print(modified_user.login_methods) # this will now contain the login method of the recipeUserId as well. elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number # as the recipeUserId's account. pass elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER": # This happens if the input primaryUserId is not actually a primary user ID. # You can call create_primary_user and call link_accounts again pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if the input recipe user ID is already linked to another primary user. # You can call unlink_accounts on the recipe user ID and then try linking again. pass ``` ### 4. Unlink accounts If you want to unlink an account from its primary user ID, you can use the following function: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import AccountLinking from "supertokens-node/recipe/accountlinking"; import { RecipeUserId } from "supertokens-node"; async function unlinkAccount(recipeUserId: RecipeUserId) { let response = await AccountLinking.unlinkAccount(recipeUserId); if (response.status === "OK") { if (response.wasLinked) { // This means that we unlinked the account from its primary user ID } else { // This means that the user was never linked in the first place } if (response.wasRecipeUserDeleted) { // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens // won't know which user info to return - the primary user, or the recipe user. // Note that even though the recipe user is deleted, the session, metadata, roles etc for this // primary user is still intact, and calling getUserById(primaryUserId) will still return // the user object with the other login methods. } else { // There not exists a user account which is not a primary user, with the recipeUserId = to the // input recipeUserId. } } } ``` ```python from supertokens_python.recipe.accountlinking.asyncio import unlink_account from supertokens_python.types import RecipeUserId async def unlink_account_helper(recipe_user_id: RecipeUserId): response = await unlink_account(recipe_user_id) if response.was_linked: # This means that we unlinked the account from its primary user ID pass else: # This means that the user was never linked in the first place pass if response.was_recipe_user_deleted: # This is true if we call unlink_account on the recipe user ID of the primary user ID user. # We delete this user because if we don't and we call get_user_by_id() on this user's ID, SuperTokens # won't know which user info to return - the primary user, or the recipe user. # Note that even though the recipe user is deleted, the session, metadata, roles etc for this # primary user is still intact, and calling get_user_by_id(primary_user_id) will still return # the user object with the other login methods. pass else: # There now exists a user account which is not a primary user, with the recipe_user_id equal to the # input recipe_user_id. pass ``` ```python from supertokens_python.recipe.accountlinking.syncio import unlink_account from supertokens_python.types import RecipeUserId def unlink_account_helper(recipe_user_id: RecipeUserId): response = unlink_account(recipe_user_id) if response.was_linked: # This means that we unlinked the account from its primary user ID pass else: # This means that the user was never linked in the first place pass if response.was_recipe_user_deleted: # This is true if we call unlink_account on the recipe user ID of the primary user ID user. # We delete this user because if we don't and we call get_user_by_id() on this user's ID, SuperTokens # won't know which user info to return - the primary user, or the recipe user. # Note that even though the recipe user is deleted, the session, metadata, roles etc for this # primary user is still intact, and calling get_user_by_id(primary_user_id) will still return # the user object with the other login methods. pass else: # There now exists a user account which is not a primary user, with the recipe_user_id equal to the # input recipe_user_id. pass ``` ### 5. Convert a `userId` into a `recipeUserId` If you notice, the input to a lot of the functions above is of type `RecipeUserId`. You can convert a string userID into a `RecipeUserId` in the following way: :::note[At the moment this feature is not supported through the Go SDK.] ::: ```tsx import SuperTokens from "supertokens-node"; async function getAsRecipeUserIdType(userId: string) { return SuperTokens.convertToRecipeUserId(userId); } ``` ```python from supertokens_python.types import RecipeUserId user_id = "some_user_id"; recipe_user_id = RecipeUserId(user_id) ``` The reason for this type is that it prevents bugs wherein a function expects a recipe user ID (like `createNewSession`, or `updateEmailOrPassword` from email password recipe). However, you might pass in the primary user ID instead. ### 6. Other helper functions Our SDK also exposes other helper functions: - `AccountLinking.createPrimaryUserIdOrLinkAccounts`: Given a recipe user ID, this function attempts linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function makes the input user account a primary one. - `AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId`: Given a recipe user ID, this function returns a primary user ID which this user can link to, based on matching emails / phone numbers. If no such primary user exists, this function returns `undefined`. - `AccountLinking.canCreatePrimaryUser`: Given a recipe user ID, this function returns a status `OK` if the user can become a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can become a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to. - `AccountLinking.canLinkAccounts`: Given a `recipeUserId` and a primary user ID, this function returns a status `OK` if the accounts can link, and if not, it returns a different status (indicating why the accounts can't link). Accounts can link if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all the tenants that it belongs to. - `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#error-status-codes) to see why this might return `false`. - `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#error-status-codes) to see why this might return `false`. - `AccountLinking.isEmailChangeAllowed`: Given the recipe user ID and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` returns: - If the input recipe user is a primary user, then ensure that the new email doesn't belong to any other primary user. If it does, the change is not allowed since multiple primary users can't have the same email. - If the recipe user is not a primary user, and if the new email is not verified, then check if there exists a primary user with the same email. If it exists, do not allow the email change. The disallowance occurs because if this email changes, and the system sends an email verification email, then the primary user may end up clicking on the link by mistake, causing account linking to happen which can result in account takeover if this recipe user is malicious. --- ## See also --- # Initial setup Source: https://supertokens.com/docs/post-authentication/dashboard/initial-setup ## Overview The following page shows you how to set up the dashboard recipe and access the web interface. You can check the next diagram to understand how the dashboard integrates with your application. **Managed service** Flowchart of architecture when using SuperTokens managed service **Self-hosted** Flowchart of architecture when self-hosting SuperTokens ## Steps ### 1. Initialize the `Dashboard` recipe To get started, initialize the Dashboard recipe in the `recipeList`. ```tsx import SuperTokens from "supertokens-node"; import Dashboard from "supertokens-node/recipe/dashboard"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // TODO: Initialise other recipes Dashboard.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/dashboard" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ // TODO: Initialise other recipes dashboard.Init(nil), }, }); } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import dashboard init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ # TODO: Initialise other recipes dashboard.init(), ] ) ``` #### Update your content security policy (optional) If your backend returns a `Content-Security-Policy` header, you encounter the following UI displaying the Content Security Policy violation details. Follow the instructions provided in this UI to make necessary adjustments to your backend Content Security Policy configuration. ![Content Security Policy error handled UI](/docs-assets/img/dashboard/csp-error.png) For example, to address the error message displayed in the above screenshot, you need to modify your `original policy`. In the given example, it appears as follows: If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. ```text script-src: 'self' 'unsafe-inline' https://google.com img-src: https://google.com ``` ```text script-src: 'self' 'unsafe-inline' https://cdn.jsdelivr.net/gh/supertokens/ img-src: https://cdn.jsdelivr.net/gh/supertokens/ https://purecatamphetamine.github.io/ ``` ```text script-src: 'self' 'unsafe-inline' https://cdn.jsdelivr.net/gh/supertokens/ img-src: https://cdn.jsdelivr.net/gh/supertokens/ https://purecatamphetamine.github.io/ ``` To resolve this issue, make the following adjustments: ```text script-src: 'self' 'unsafe-inline' https://google.com img-src: https://google.com https://cdn.jsdelivr.net/gh/supertokens/ ``` Essentially, you need to include the domain listed as the `Blocked URI` in your violated directive block within your original policy. ### 2. Access the dashboard :::note[The backend SDK serves the user management dashboard, and you have to use your API domain when trying to visit the dashboard.] ::: Navigate to `/auth/dashboard` to view the dashboard. :::note[If you are using Next.js, upon integrating the backend SDK into your Next.js API folder, the dashboard becomes accessible by default at `/api/auth/dashboard`. For frameworks other than Next.js, access it at `/auth/dashboard`. Should you have customized the `apiBasePath` configuration property, navigate to `/auth/dashboard` to access the dashboard.] ::: Dashboard login screen UI ### 3. Create dashboard credentials :::info[Paid Feature] You can create 3 dashboard users* for free. If you need to create additional users: - For self hosted users, please [sign up](https://supertokens.com/auth) to generate a license key and follow the instructions sent to you by email. - For managed service users, open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and enable **Additional Dashboard Users** from **Features**. *: A dashboard user is a user that can log into and view the user management dashboard. These users are independent to the users of your application ::: When you first set up SuperTokens, there are no credentials created for the dashboard. If you click the "Add a new user" button in the dashboard login screen you can see the command you need to execute to create credentials. Dashboard sign up screen UI To create credentials you need to make a request to SuperTokens core. - The example above uses the demo core `https://try.supertokens.com`, replace this with the connection URI you pass to the backend SDK when initialising SuperTokens. - Replace `` with your API key. If you are using a self hosted SuperTokens core there is no API key by default. In that case you can either skip or ignore the `api-key` header. - Replace `` and `` with the appropriate values. :::warning[If using self-hosted SuperTokens core, you need to make sure that you add an API key to the core in case it's exposed to the internet. Otherwise, anyone can create or modify dashboard users.] You can add an API key to the core by following the instructions "Auth flow customizations" > "SuperTokens core settings" > "Adding API keys" page. ::: ### 4. Update dashboard credentials You can update the email or password of existing credentials by using the "Forgot Password" button on the dashboard login page. Reset your password screen UI To update credentials you need to make a request to SuperTokens core. - The example above uses the demo core `https://try.supertokens.com`, replace this with the connection URI you pass to the backend SDK when initialising SuperTokens. - Replace `` with your API key. If you are using a self hosted SuperTokens core there is no API key by default. In that case you can either skip or ignore the `api-key` header. - Replace `` and `` with the appropriate values. You can use `newEmail` instead of `newPassword` if you want to update the email ### 5. Restrict access to dashboard users When using the dashboard recipe, you can restrict access to certain features by providing a list of emails considered as "admins." If a dashboard user logs in with an email not present in this list, they can only perform read operations. All write operations result in the backend SDKs failing the request. You can provide an array of emails to the backend SDK when initialising the dashboard recipe: :::note[- Not providing an admins array results in all dashboard users having both read and write operations.] - Providing an empty array as admins results in all dashboard users having only read access. ::: ```tsx import SuperTokens from "supertokens-node"; import Dashboard from "supertokens-node/recipe/dashboard"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // TODO: Initialise other recipes Dashboard.init({ admins: ["johndoe@gmail.com"], }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/dashboard" "github.com/supertokens/supertokens-golang/supertokens" "github.com/supertokens/supertokens-golang/recipe/dashboard/dashboardmodels" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ // TODO: Initialise other recipes dashboard.Init(&dashboardmodels.TypeInput{ Admins: &[]string{ "johndoe@gmail.com", }, }), }, }); } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import dashboard init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ # TODO: Initialise other recipes dashboard.init( admins=[ "johndoe@gmail.com", ], ), ] ) ``` --- # Introduction Source: https://supertokens.com/docs/post-authentication/dashboard/introduction ## Overview With the user management dashboard, you can perform actions through a user interface. This allows you to view and modify tenants and users through different web actions. ## Getting started To get a quick understanding of how the dashboard looks and what it can do you can visit the [live demo](https://dashboard.demo.supertokens.com/api/auth/dashboard). The credentials for logging in are: `email: demo@supertokens.com` and `password: abcd1234`. Alternatively, you can follow the [quickstart guide](/post-authentication/dashboard/initial-setup) to configure the dashboard in your **SuperTokens** integration. See how you can initialize the dashboard in your application. Work with users through the dashboard. Work with tenants through the dashboard. --- # Tenant Management Source: https://supertokens.com/docs/post-authentication/dashboard/tenant-management ## Overview This page shows you what actions you can perform on tenants through the dashboard. :::info[Caution] This is only available with Node and Python SDKs. ::: Tenant Management Landing --- ## Create a new tenant Clicking on `Add Tenant` prompts you to enter the tenant id. Once you enter the tenant id, click on `Create Now` to create the tenant. You then proceed to the Tenant Details page where you can further manage the newly created tenant. Create Tenant ## View tenant details Upon selection or creation of a tenant, the Tenant Details page appears. The sections appear below. Tenant details ### Tenant ID and users The first section shows the tenant ID and the number of users in that tenant. Clicking on `See Users` takes you to the [user management page](/post-authentication/dashboard/user-management) where you can view and manage the users for the selected tenant. Tenant users ### Enabled login methods This section displays the login methods available for the tenant. By enabling these toggles, you can make the corresponding login methods accessible to the users within the tenant. Appropriate recipes must be active to turn on the login methods. For example, - to turn on `emailpassword`, initialize the EmailPassword recipe in the backend. - to turn on `OTP Phone`, initialize the Passwordless recipe with `flowType` `USER_INPUT_CODE` and contactMethod `PHONE` :::info If you are using the Auth React SDK, make sure to enable `usesDynamicLoginMethods` in your tenant configuration to ensure the frontend automatically shows the login methods based on the selection here. See [tenant configuration](/authentication/enterprise/manage-tenants) for details. ::: Login Methods ### Secondary factors This section displays the secondary factors available for the tenant. By enabling these toggles, the corresponding factor becomes active for all users of the tenant. Refer to [MultiFactor Authentication docs](/additional-verification/mfa/introduction) for more information. [MultiFactorAuth](/additional-verification/mfa/initial-setup) recipe must initialize to enable Secondary Factors. Also, initialize appropriate recipes in the backend SDK to use a secondary factor. For example, - to turn on TOTP, initialize the TOTP recipe in the backend. - to turn on `OTP Phone`, initialize the Passwordless recipe with `flowType` `USER_INPUT_CODE` and contactMethod `PHONE` Secondary Factors ### Core configuration Core Configuration This section shows the current configuration values in core for the tenant. You can edit some of these settings by clicking the `pencil` icon next to the property. Edit Core Configuration :::warning[Some configuration values may not be editable since they inherit from the App. If using SuperTokens managed hosting, you can modify deployment-level values on the **Configuration** page in the SaaS Dashboard. Else, if you are self-hosting the SuperTokens core, edit them via Docker environment variables or the `configuration.yaml` file.] ::: --- ## Manage `ThirdParty` providers The Social/Enterprise providers section becomes available once `Third Party` login method is active for the tenant. Initially, configure a new provider. Add provider prompt Later on, you can configure new or existing third-party providers from the **Social/Enterprise providers** section. Social/Enterprise providers ### Configure a new provider When adding a new third-party provider, you receive a list of available options, including built-in enterprise and social providers, custom, and SAML. New Provider Upon selection of the desired provider, provide further details such as `Client ID`, `Client Secret`, etc. New Provider Details #### Enterprise providers For the Enterprise providers, provide certain extra information before proceeding to the Provider details. For example, Active Directory provider requires a `Directory ID` before editing further details. Additional configuration for Active Directory #### Custom providers If a Social/Enterprise provider is not available in the list of built-in providers, you can still use them by selecting the `Add Custom Provider` option. Start off by providing `ThirdParty ID`, `Name` and Client details such as `Client ID`, `Secret`, `Scope`, etc. Custom Provider basic details If using an OpenID compliant provider, you could add the `OIDC Discovery Endpoint`. Otherwise, configure the provider by manually providing `Authorization Endpoint`, `Token Endpoint`, `User Info Endpoint`, etc. OpenID configuration Finally, clicking on `Save` adds the Social/enterprise provider for the tenant. #### SAML providers To add a SAML provider, use the `Add SAML Provider` option. For more information on what is SAML and how it works with SuperTokens, refer [SAML docs](/authentication/enterprise/saml). Upon selection, provide the `Boxy URL` and the `Boxy API Key`. :::note[To use SAML providers, an additional Boxy HQ service is necessary. You can either self-host yourself or email for a managed instance. Details for them are also available on this page.] ::: Boxy SAML Prompt On continuing, you are further asked for the SAML configuration. You have an option to either provide SAML XML directly or via the Metadata URL from the Provider. Also, fill in other details such as `Suffix`, `Name`, `Redirect URLs` and click on `Save` to add the SAML provider. :::warning[Adding ThirdParty suffix is not compulsory, however if you wish to add multiple SAML providers for a tenant, you need to add unique suffixes for each of them.] ::: --- # User management Source: https://supertokens.com/docs/post-authentication/dashboard/user-management ## Overview With the user management dashboard you can view the list of users and their details. You can also perform different operations on these users as mentioned below. --- ## Create users If you have created your app, you may not have any users to show on the dashboard. Empty dashboard screen UI ## List users Navigate to your frontend app and create a user (via the sign-up flow). On creation, if you head back to the dashboard and refresh the page, you see that user: One user in dashboard screen UI --- ## View user details When you select a user you can view detailed information about the user such as email, phone number, user metadata, etc. User details page screen UI part one User details page screen UI part two --- ## Edit user details You can edit user information and perform actions such as resetting a user's password or revoking sessions for a user. Change password modal UI :::info[Note] Enable some features such as user metadata and email verification in your backend before you can use them in the user management dashboard. ::: --- ## Create user roles and permissions :::warning This feature is only available through the Node.js SDK. ::: When you first use the `UserRoles` recipe, the list of roles is empty. To create roles, click on the "Add Role" button. No roles created This action opens a modal, enabling you to create a role along with its associated permissions. Permissions are essentially a list of strings assigned to a specific role. --- ## List user roles :::warning This feature is only available through the Node.js SDK. ::: Create role After creating a role, the UI should display a list of all roles in your app. Roles list You can preview the role you created by clicking on the role row. The modal provides options to edit or delete the role. Preview role --- ## Assign user roles :::warning This feature is only available through the Node.js SDK. ::: To assign a specific role to a user, start by finding the user in the dashboard. Upon clicking the user, navigate to the user details page where you find a section for user roles. If the selected user has associations with multiple tenants, you can choose a `tenantId` from the dropdown menu to specify the tenant for which you'd like to assign roles. Select tenant Click the edit button to start assigning roles. Then, select the "Assign Role" button, and a modal appears with a list of available roles for assignment to this user. Assign role --- ## Remove user roles :::warning This feature is only available through the Node.js SDK. ::: To remove a role assigned to a user, click on the "X" icon next to that specific role. View assigned role --- # Post Login Redirect Source: https://supertokens.com/docs/post-authentication/post-login-redirect ## Change redirection path post login By default, the user is redirected to the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: By default, the user is redirected the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { if (context.redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return context.redirectToPath; } if (context.createdNewUser) { // user signed up } else { // user signed in } return "/dashboard"; } return undefined; }, recipeList: [ /* Recipe list */ ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { if (context.redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return context.redirectToPath; } if (context.createdNewUser) { // user signed up } else { // user signed in } return "/dashboard"; } return undefined; }, recipeList: [ /* Recipe list */ ], }); ``` The user will be redirected to the provided URL on: - Successful sign up. - Successful sign in. - Successful email verification post sign up. - If the user is already logged in. If you want to redirect the user to a different domain, then you can first redirect them to a specific path using the function above, which further redirects them to the final domain. :::info Please refer to [this page](/references/frontend-sdks/hooks#redirection-callback-hook) to learn more about the `getRedirectionURL` hook. ::: ## Redirect user to the login page Use the `redirectToAuth({show?: "signin" | "signup", redirectBack?: boolean}?)` function to redirect the user to the login screen. For example, you may want to call this function when the user clicks on the login button. Redirect the user to the `/auth` (this is the default path for the pre-built UI) Redirect the user to the `/auth` (this is the default path for the pre-built UI) ```tsx import React from "react"; import { redirectToAuth } from "supertokens-auth-react"; function NavBar() { async function onLogin() { redirectToAuth(); } return (
  • Home
  • Login
); } ```
```ts import { Component } from "@angular/core"; @Component({ selector: "nav-bar", template: `
  • Home
  • Login
`, }) export class NavBarComponent { async onLogin() { window.location.href = "/auth?show=signin&redirectToPath=" + encodeURIComponent(window.location.pathname); } } ```
```html ```
- Call `redirectToAuth({show: "signin"})` to take them to the sign in screen - Call `redirectToAuth({show: "signup"})` to take them to the sign up screen - If you do not want the user to be redirected to the current page post sign in, use `redirectToAuth({redirectBack: false})` - Set `show=signin` to take them to the sign in screen - Set `show=signup` to take them to the sign up screen - Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one). - Set `show=signin` to take them to the sign in screen - Set `show=signup` to take them to the sign up screen - Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one). ## Showing sign up by default The login screen shows the sign in UI by default, to change that, you can set the following config: ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, defaultToSignUp: true, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, defaultToSignUp: true, recipeList: [ /* ... */ ], }); ``` --- ## See also --- # Access Session Data Source: https://supertokens.com/docs/post-authentication/session-management/access-session-data ## Overview The session data is accessible, both in the backend and on the frontend, after a user has successfully logged in. This guide shows you how to access different session properties. ## Before you start :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: --- ## Access the JWT Token ### On the backend ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; let app = express(); app.get("/getJWT", verifySession(), async (req, res) => { let session = req.session; let jwt = session.getAccessToken(); res.json({ token: jwt }); }); ``` ```tsx 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: "/getJWT", method: "get", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { let session = req.session; let jwt = session!.getAccessToken(); return res.response({ token: jwt }).code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; let fastify = Fastify(); fastify.get( "/getJWT", { preHandler: verifySession(), }, (req, res) => { let session = req.session; let jwt = session.getAccessToken(); res.send({ token: jwt }); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function getJWT(awsEvent: SessionEvent) { let session = awsEvent.session; let jwt = session!.getAccessToken(); return { body: JSON.stringify({ token: jwt }), statusCode: 200, }; } exports.handler = verifySession(getJWT); ``` ```tsx 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.get("/getJWT", verifySession(), (ctx: SessionContext, next) => { let session = ctx.session; let jwt = session!.getAccessToken(); ctx.body = { token: jwt }; }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, get, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import { SessionContext } from "supertokens-node/framework/loopback"; class GetJWT { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @get("/getJWT") @intercept(verifySession()) @response(200) handler() { let session = this.ctx.session; let jwt = session!.getAccessToken(); return { token: jwt }; } } ``` ```tsx 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 getJWT(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); let session = req.session; let jwt = session!.getAccessToken(); res.json({ token: jwt }); } ``` ```tsx check=false reason="Requires surrounding framework application context" import { Controller, Get, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { @Get("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ token: any }> { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. const jwt = session.getAccessToken(); return { token: jwt }; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) // We assume that you have wrapped this handler with session.VerifySession func getJWT(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) jwt := sessionContainer.GetAccessToken() fmt.Println(jwt) } ``` ```python check=false reason="Requires surrounding framework application context" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.get('/getJWT') async def get_jwt(session: SessionContainer = Depends(verify_session())): current_jwt = session.get_access_token() print(current_jwt) # TODO... ``` ```python check=false reason="Requires surrounding framework application context" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/getJWT', methods=['GET']) @verify_session() def get_jwt(): session: SessionContainer = g.supertokens current_jwt = session.get_access_token() print(current_jwt) # TODO... ``` ```python check=false reason="Requires surrounding application context" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def get_jwt(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) current_jwt = session.get_access_token() print(current_jwt) # TODO... ``` ```tsx check=false reason="Requires surrounding framework application context" 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 }); } let jwt = session!.getAccessToken(); return NextResponse.json({ token: jwt }); }); } ``` ### On the frontend #### 1. Enable `exposeAccessTokenToFrontendInCookieBasedAuth` When using cookie based auth, by default, the access token is not readable by the SDK on the frontend (since it's stored as `httpOnly` cookie). To enable this, you need to set the `exposeAccessTokenToFrontendInCookieBasedAuth` parameter to `true`. :::note[If you are only using header-based sessions, you can skip this step] ::: ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ exposeAccessTokenToFrontendInCookieBasedAuth: true, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "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{ ExposeAccessTokenToFrontendInCookieBasedAuth: true, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import session init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( expose_access_token_to_frontend_in_cookie_based_auth=True ) ] ) ``` #### 2. Read the access token ```tsx import Session from "supertokens-auth-react/recipe/session"; async function getJWT() { if (await Session.doesSessionExist()) { let userId = await Session.getUserId(); let jwt = await Session.getAccessToken(); } } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; async function getJWT() { if (await Session.doesSessionExist()) { let userId = await Session.getUserId(); let jwt = await Session.getAccessToken(); } } ``` ```tsx import Session from "supertokens-web-js/recipe/session"; async function getJWT() { if (await Session.doesSessionExist()) { let userId = await Session.getUserId(); let jwt = await Session.getAccessToken(); } } ``` ```tsx check=false reason="Requires SDK globals from surrounding application" async function getJWT() { if (await supertokensSession.doesSessionExist()) { let userId = await supertokensSession.getUserId(); let jwt = await supertokensSession.getAccessToken(); } } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function getJWT() { if (await SuperTokens.doesSessionExist()) { let userId = await SuperTokens.getUserId(); let jwt = await SuperTokens.getAccessToken(); } } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import org.json.JSONObject class MainApplication: Application() { fun getJWT() { val jwt: String? = SuperTokens.getAccessToken(this); } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func getJWT() { let jwt: String? = SuperTokens.getAccessToken() // Use `jwt` however you like } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future getJWT() async { var jwt = await SuperTokens.getAccessToken(); if (jwt != null) { // Use `jwt` however you like } } ``` --- ## Access the Tenant ID :::info[Multi Tenancy] This feature is only relevant if you are using the multi tenancy feature. ::: The session's access token payload contains the tenant ID in the `tId` claim. You can access it in the following way: ### On the backend ```tsx 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(), (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); //.... }); ``` ```tsx 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(), }, ], }, handler: async (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); //... }, }); ``` ```tsx 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(), }, (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); //.... }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { let tenantId = awsEvent.session!.getTenantId(); //.... } exports.handler = verifySession(likeComment); ``` ```tsx 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(), (ctx: SessionContext, next) => { let tenantId = ctx.session!.getTenantId(); //.... }); ``` ```tsx 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()) @response(200) handler() { let tenantId = (this.ctx as SessionContext).session!.getTenantId(); //.... } } ``` ```tsx 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()(req, res, next); }, req, res, ); let tenantId = req.session!.getTenantId(); //.... } ``` ```tsx check=false reason="Requires surrounding framework application context" 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") @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { let tenantId = session.getTenantId(); //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) }) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) tenantID := sessionContainer.GetTenantId() fmt.Println(tenantID) } ``` ```go import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(nil), likeCommentAPI) } // This is a function that wraps the supertokens verification function // to work the 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 likeCommentAPI(c *gin.Context) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) tenantID := sessionContainer.GetTenantId() fmt.Println(tenantID) } ``` ```go import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) tenantID := sessionContainer.GetTenantId() fmt.Println(tenantID) } ``` ```go import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) tenantID := sessionContainer.GetTenantId() fmt.Println(tenantID) } ``` ```python check=false reason="Requires surrounding framework application context" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.get('/getTenantId') async def get_tenant_id(session: SessionContainer = Depends(verify_session())): tenant_id = session.get_tenant_id() print(tenant_id) ``` ```python check=false reason="Requires surrounding framework application context" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/getTenantId', methods=['GET']) @verify_session() def get_tenant_id(): session: SessionContainer = g.supertokens tenant_id = session.get_tenant_id() print(tenant_id) ``` ```python check=false reason="Requires surrounding application context" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def get_tenant_id(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) tenant_id = session.get_tenant_id() print(tenant_id) ``` ```tsx check=false reason="Requires surrounding framework application context" 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 }); } let tenantId = session!.getTenantId(); //.... return NextResponse.json({}); }); } ``` :::note[If you are not using the backend SDK and are doing JWT verification yourself, you can fetch the tenant ID from the JWT by reading the `tId` claim.] ::: ### On the frontend You can read the tenant ID on the frontend by adding the `tId` claim from the [access token payload](/additional-verification/session-verification/claim-validation#using-the-access-token-payload). --- ## Fetch all user sessions Given a user ID, you can fetch all sessions that are active for that user in the following way: ```tsx import Session from "supertokens-node/recipe/session"; async function getSessions() { let userId = "someUserId"; // fetch somehow // sessionHandles is string[] let sessionHandles = await Session.getAllSessionHandlesForUser(userId); sessionHandles.forEach((handle) => { /* we can do the following with the handle: * - revoke this session * - change access token payload or session data * - fetch access token payload or session data */ }); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { // sessionHandles is string[] tenantId := "public" sessionHandles, err := session.GetAllSessionHandlesForUser("someUserId", &tenantId) if err != nil { // TODO: handle error return } for _, currSessionHandle := range sessionHandles { /* we can do the following with the currSessionHandle: * - revoke this session * - change access token payload or session data * - fetch access token payload or session data */ fmt.Println(currSessionHandle) } } ``` ```python from supertokens_python.recipe.session.asyncio import get_all_session_handles_for_user async def some_func(): # session_handles is List[string] session_handles = await get_all_session_handles_for_user("someUserId") for _ in session_handles: pass # TODO # # we can do the following with the session_handle: # - revoke this session # - change JWT payload or session data # - fetch JWT payload or session data # ``` ```python from supertokens_python.recipe.session.syncio import get_all_session_handles_for_user # session_handles is List[string] session_handles = get_all_session_handles_for_user("someUserId") for session_handle in session_handles: pass # TODO # # we can do the following with the session_handle: # - revoke this session # - change JWT payload or session data # - fetch JWT payload or session data # ``` :::info[Multi Tenancy] By default, the method returns all the `session handles` for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. ::: --- ## See also --- # Blacklist access tokens Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting ## Overview By default, session verification is stateless. This means that SuperTokens does not check that the session actually exists in the database, and only verifies the session by checking its signature. Although this makes session verification fast, it also means that if you revoke a session, the user can still use its access token until the token expires. If verification must fail immediately after session revocation, force an authoritative database check. You can enable this per API. Choose endpoints based on the sensitivity of the data or action and your threat model, not the HTTP method. A confidential `GET` endpoint can require immediate revocation just as much as a state-changing endpoint. ## Before you start :::warning Database-backed verification adds a Core request to each protected API call. Managed service users should check the [current rate limit policy](/deployment/rate-limits) for their plan before enabling it broadly. Apply `checkDatabase` to every endpoint that requires immediate revocation, including sensitive read endpoints, and capacity-plan for that traffic. ::: --- ## Using `Verify Session` ```tsx 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({ checkDatabase: true }), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }); ``` ```tsx 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({ checkDatabase: true }), }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //... }, }); ``` ```tsx 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({ checkDatabase: true }), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { let userId = awsEvent.session!.getUserId(); //.... } exports.handler = verifySession(likeComment, { checkDatabase: true }); ``` ```tsx 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({ checkDatabase: true }), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //.... }); ``` ```tsx 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({ checkDatabase: true })) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //.... } } ``` ```tsx 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({ checkDatabase: true })(req, res, next); }, req, res, ); let userId = req.session!.getUserId(); //.... } ``` ```tsx check=false reason="Requires surrounding framework application context" 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") @UseGuards(new AuthGuard({ checkDatabase: true })) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { let userId = session.getUserId(); //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession checkDb := true session.VerifySession(&sessmodels.VerifySessionOptions{ CheckDatabase: &checkDb, }, likeCommentAPI).ServeHTTP(rw, r) }) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession checkDb := true router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ CheckDatabase: &checkDb, }), likeCommentAPI) } // This is a function that wraps the supertokens verification function // to work the 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 likeCommentAPI(c *gin.Context) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession checkDb := true r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ CheckDatabase: &checkDb, }, likeCommentAPI)) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession checkDb := true router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ CheckDatabase: &checkDb, }, likeCommentAPI)).Methods(http.MethodPost) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```python check=false reason="Requires surrounding framework application context" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends(verify_session(check_database=True))): user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="Requires surrounding framework application context" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/update-jwt', methods=['POST']) @verify_session(check_database=True) def like_comment(): session: SessionContainer = g.supertokens user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="Requires surrounding application context" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session(check_database=True) async def like_comment(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() print(user_id) ``` ```tsx check=false reason="Requires surrounding framework application context" 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 }); } let userId = session!.getUserId(); //.... return NextResponse.json({}); }, { checkDatabase: true }, ); } ``` --- ## Using `Get Session` ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; let app = express(); app.post("/like-comment", async (req, res, next) => { try { let session = await Session.getSession(req, res, { checkDatabase: true }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } catch (err) { next(err); } }); ``` ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/like-comment", method: "post", handler: async (req, res) => { let session = await Session.getSession(req, res, { checkDatabase: true }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //... }, }); ``` ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { let session = await Session.getSession(req, res, { checkDatabase: true }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { let session = await Session.getSession(awsEvent, awsEvent, { checkDatabase: true }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } exports.handler = middleware(likeComment); ``` ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { let session = await Session.getSession(ctx, ctx, { checkDatabase: true }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... }); ``` ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; class LikeComment { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/like-comment") @response(200) async handler() { let session = await Session.getSession(this.ctx, this.ctx, { checkDatabase: true }); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; export default async function likeComment(req: SessionRequest, res: any) { let session = await superTokensNextWrapper( async (next) => { return await Session.getSession(req, res, { checkDatabase: true }); }, req, res, ); if (session !== undefined) { let userId = session.getUserId(); } else { // user is not logged in... } //.... } ``` ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import Session from "supertokens-node/recipe/session"; @Controller() export class ExampleController { @Post("example") async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { checkDatabase: true }); if (session !== undefined) { const userId = session.getUserId(); } else { // user is not logged in... } //.... return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func likeCommentAPI(w http.ResponseWriter, r *http.Request) { checkDb := true sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{ CheckDatabase: &checkDb, }) if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: send 500 to client } return } if sessionContainer != nil { // session exists userID := sessionContainer.GetUserID() fmt.Println(userID) } else { // user is not logged in } } ``` ```python check=false reason="Requires surrounding framework application context" from fastapi import Request from supertokens_python.recipe.session.asyncio import get_session @app.post('/like-comment') async def like_comment(request: Request): session = await get_session(request, check_database=True) if session is not None: user_id = session.get_user_id() print(user_id) # TODO: else: pass # user is not logged in ``` ```python check=false reason="Requires surrounding framework application context" from flask.wrappers import Request from supertokens_python.recipe.session.syncio import get_session @app.route('/like-comment', methods=['POST']) def like_comment(request: Request): session = get_session(request, check_database=True) if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```python from django.http import HttpRequest from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): session = await get_session(request, check_database=True) if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in ``` ```tsx check=false reason="Requires surrounding framework application context" 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 }); } let userId = session!.getUserId(); //.... return NextResponse.json({}); }, { checkDatabase: true }, ); } ``` --- # Implement anonymous sessions Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/anonymous-session ## Overview With anonymous sessions, you can keep track of user's action / data before they login, and then transfer that data to their post login session. Anonymous sessions have different properties than regular, logged in sessions: - The `userID` of anonymous sessions doesn't matter - The security constraints on anonymous sessions are lesser than regular sessions, as you would want users to log in before doing anything sensitive anyway. - Each visitor that visits your app / website gets an anonymous session if they don't have one previously. This does not require them to log in. - Anonymous sessions are not stored in the database to avoid the risk of flooding the database with sessions that are not useful to the app. Given the different characteristics of anonymous sessions, using a simple, long lived JWT is a perfect use case. They can store any information about the user's activity, and they don't occupy any database space either. ## Steps ### 1. Create the JWT Start by creating a JWT like in the next example: ```tsx import Session from "supertokens-node/recipe/session"; async function createAnonymousJWT(payload: any) { let jwtResponse = await Session.createJWT( { key: "value", // more payload... }, 315360000, ); // 10 years lifetime if (jwtResponse.status === "OK") { // Send JWT as Authorization header to M2 return jwtResponse.jwt; } throw new Error("Unable to create JWT. Should never come here."); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { validitySessions := uint64(315360000) jwtResponse, err := session.CreateJWT(map[string]interface{}{ "key": "value", // ...additional payload }, &validitySessions, nil) // 10 years lifetime if err != nil { // handle error } jwtString := jwtResponse.OK.Jwt fmt.Println(jwtString) // Send JWT as Authorization header to M2 } ``` ```python from supertokens_python.recipe.session import asyncio from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult async def create_jwt(): jwtResponse = await asyncio.create_jwt({ "key": "value", # ... extra payload }, 315360000) # 10 years lifetime if isinstance(jwtResponse, CreateJwtOkResult): _ = jwtResponse.jwt # Send JWT as Authorization header to M2 else: raise Exception("Unable to create JWT. Should never come here.") ``` ```python from supertokens_python.recipe.session.syncio import create_jwt from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult jwtResponse = create_jwt({ "source": "microservice", # ... extra payload }) if isinstance(jwtResponse, CreateJwtOkResult): jwtStr = jwtResponse.jwt # Send JWT as Authorization header to M2 else: raise Exception("Unable to create JWT. Should never come here.") ``` - As shown in the code above, you can add any payload you like to the JWT. You can even add a `sub` (`userId`) payload with a random `UUID` if you like, or some user ID with a prefix like `"G-.."` which indicates this is a guest user ID. - You could create your own application middleware which inspects the request and auto adds a JWT to it in the response cookies. This way, whenever a user visits your website and makes an API call, they get a JWT in their cookies, and you can use that JWT to track their activity. ### 2. Send the JWT to the client After creating the JWT, you can send it to a user as a cookie. This way you can use it to track the activity during a session. #### Verify the JWT To check if an anonymous session is valid read through the [manual JWT verification section](/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library). On thing to note here is that in the section about verification using the public key string, you do not need to set `useDynamicAccessTokenSigningKey` to `true`. The token creation process in this scenario uses the static signing key (`kid` starting the `s-..`) by default. :::warning[You **cannot** use the `verifySession` or `getSession` functions to verify JWTs from anonymous sessions.] The `verifySession` and `getSession` check for the presence of certain claims in the JWT (`sessionHandle`, `refreshTokenHash` etc...) that the Session recipe adds for authenticated users. ::: ### 3. Transfer the data to a logged in session The idea here is to override the `Session` recipe on the `backend`. Whenever the user logs in or signs up, the data from the anonymous session transfers to the logged-in session. The override attempts to read the request header cookie to get the JWT. It assumes that you have added it to the cookies, verifies it, and then adds the payload to the logged-in session. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // ... Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, createNewSession: async function (input) { let userId = input.userId; const request = SuperTokens.getRequestFromUserContext(input.userContext); let jwt: string | undefined; let jwtPayload = {}; if (request !== undefined) { jwt = request.getCookieValue("jwt"); } else { /** * This is possible if the function is triggered from the user management dashboard * * In this case because we cannot read the JWT, we create a session without the custom * payload properties */ } if (jwt !== undefined) { // verify JWT using a JWT verification library.. jwtPayload = { /* ... get from decoded jwt ... */ }; } // This goes in the access token, and is available to read on the frontend. input.accessTokenPayload = { ...input.accessTokenPayload, ...jwtPayload, }; return originalImplementation.createNewSession(input); }, }; }, }, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "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 { // First we copy the original implementation func originalCreateNewSession := *originalImplementation.CreateNewSession // Now we override the CreateNewSession function (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { request := supertokens.GetRequestFromUserContext(userContext) if (request != nil) { jwt, err := request.Cookie("jwt") if err != nil { return nil, err } if jwt != nil { // verify JWT using a jwt verification library.. decodedJWT := map[string]interface{}{ // from JWT verification lib } // This goes in the access token, and is available to read on the frontend. if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } accessTokenPayload["someKey"] = decodedJWT["someKey"] } } else { /** * This is possible if the function is triggered from the user management dashboard * * In this case because we cannot read the JWT, we create a session without the custom * payload properties */ } return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo, get_request_from_user_context from supertokens_python.recipe import session from supertokens_python.recipe.session.interfaces import RecipeInterface from typing import Any, Dict, Optional from supertokens_python.types import RecipeUserId def override_functions(original_implementation: RecipeInterface): original_implementation_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], ): request = get_request_from_user_context(user_context) jwt: Optional[str] = None if request is not None: jwt = request.get_cookie("jwt") else: # # This is possible if the function is triggered from the user management dashboard # # In this case because we cannot read the JWT, we create a session without the custom # payload properties # jwt = None if jwt is not None: # verify JWT using a JWT verification library.. jwt_payload = { # from JWT verification lib } # This goes in the access token, and is available to read on the frontend. if access_token_payload is None: access_token_payload = {} access_token_payload["someKey"] = jwt_payload["someKey"] return await original_implementation_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( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", recipe_list=[ session.init( override=session.InputOverrideConfig(functions=override_functions) ) ], ) ``` - In the above code snippet, the system reads the JWT from the request header cookie. If it exists, it verifies it and then adds the payload to the logged-in session. - If the JWT doesn't exist, or if the system cannot verify it, it would be safe to ignore it and create the logged-in session anyway. - We assume that the you have saved the JWT in the cookies in with the key of `jwt`. But if not, you can remove the JWT from the request based on how you have saved it. You can even read the headers from the request. --- # Customize error handling Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/customize-error-handling ## Overview The following page shows the errors that the SuperTokens Session recipe throws and how you can customize them. --- ## Unauthorised error The system generates the error when someone accesses a protected backend API without a session. The default behavior is to clear session tokens (if any) and send a 401 to the frontend. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ errorHandlers: { onUnauthorised: async (message, request, response, userContext) => { // TODO: Write your own logic and then send a 401 response to the frontend }, }, }), ], }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "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{ ErrorHandlers: &sessmodels.ErrorHandlers{ OnUnauthorised: func(message string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 401 response to the frontend return nil }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse async def unauthorised_callback(req: BaseRequest, err: str, response: BaseResponse): # TODO: Write your own logic and then send a 401 response to the frontend return response init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( error_handlers=session.InputErrorHandlers( on_unauthorised=unauthorised_callback ) ) ] ) ``` --- ## Invalid claim error The system generates the error when someone accesses a protected backend API with a session that doesn't pass the claim validators. The default behavior is to send a 403 to the frontend with the errors included in the body. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ errorHandlers: { onInvalidClaim: async (validatorErrors, request, response, userContext) => { // TODO: Write your own logic and then send a 403 response to the frontend }, }, }), ], }); ``` ```go 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() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ ErrorHandlers: &sessmodels.ErrorHandlers{ OnInvalidClaim: func(validationErrors []claims.ClaimValidationError, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 403 response to the frontend return nil }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.recipe.session.exceptions import ClaimValidationError from supertokens_python.framework import BaseRequest, BaseResponse from typing import List async def invalid_claim_callback(req: BaseRequest, invalid_claims: List[ClaimValidationError], response: BaseResponse): # TODO: Write your own logic and then send a 403 response to the frontend return response init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( error_handlers=session.InputErrorHandlers( on_invalid_claim=invalid_claim_callback ) ) ] ) ``` --- ## Token theft detected error The system generates the error when the system detects a [session hijacking](https://en.wikipedia.org/wiki/Session_hijacking) attempt. This happens through the use of [rotating refresh tokens](https://supertokens.com/blog/the-best-way-to-securely-manage-user-sessions). The default behavior is to revoke the session and send a `401` to the frontend. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ errorHandlers: { onTokenTheftDetected: async (sessionHandle, userId, req, res, userContext) => { // TODO: Write your own logic and then send a 401 response to the frontend }, }, }), ], }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "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{ ErrorHandlers: &sessmodels.ErrorHandlers{ OnTokenTheftDetected: func(sessionHandle, userID string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 401 response to the frontend return nil }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse from supertokens_python.types import RecipeUserId async def token_theft_detected_callback(req: BaseRequest, session_handle: str, user_id: str, recipe_user_id: RecipeUserId, response: BaseResponse): # TODO: Write your own logic and then send a 401 response to the frontend return response init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( error_handlers=session.InputErrorHandlers( on_token_theft_detected=token_theft_detected_callback ) ) ] ) ``` --- ## Try refresh token error Thrown when the access token expires or is invalid. The session refresh endpoint can also throw this if multiple access tokens are present in the request cookies. The default behavior is to send a `401` to the frontend. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ errorHandlers: { onTryRefreshToken: async (message, request, response, userContext) => { // TODO: Write your own logic and then send a 401 response to the frontend }, }, }), ], }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "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{ ErrorHandlers: &sessmodels.ErrorHandlers{ OnTryRefreshToken: func(message string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 401 response to the frontend return nil }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse async def try_refresh_callback(req: BaseRequest, err: str, response: BaseResponse): # TODO: Write your own logic and then send a 401 response to the frontend return response init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( error_handlers=session.InputErrorHandlers( on_try_refresh_token=try_refresh_callback ) ) ] ) ``` --- ## Clear duplicate session cookies error Thrown when the refresh session API clears session cookies from the `olderCookieDomain` because it found multiple access tokens in the request cookies. See [this issue](https://github.com/supertokens/supertokens-node/issues/826) for more information. The default behavior is to send a 200 to the frontend. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ errorHandlers: { onClearDuplicateSessionCookies: async (message, request, response, userContext) => { // TODO: Write your own logic and then send a 200 response to the frontend }, }, }), ], }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "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{ ErrorHandlers: &sessmodels.ErrorHandlers{ OnClearDuplicateSessionCookies: func(message string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 200 response to the frontend return nil }, }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse async def on_clear_duplication_session_cookies_callback(req: BaseRequest, err: str, response: BaseResponse): # TODO: Write your own logic and then send a 200 response to the frontend return response init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( error_handlers=session.InputErrorHandlers( on_clear_duplicate_session_cookies=on_clear_duplication_session_cookies_callback ) ) ] ) ``` --- # Disable frontend network interceptors Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/disable-frontend-interceptors ## Overview SuperTokens frontend SDKs add interceptors to networking libraries to: - Enable auto refreshing of session tokens. - Auto adding of the right request headers (Authorization header in case of header based auth, or the anti-csrf headers in case of cookie based auth). - Setting `credentials: true` for cookie based auth to ensure the browser adds session cookies. Whilst this helps for greenfield projects, for existing projects, you may want to disable this interception for your API calls. Take control of how you want to attach session tokens to the request yourself. You can do this as follows: ## Steps ### 1. Update the frontend configuration You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import Session from "supertokens-auth-react/recipe/session"; Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUISession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` You can use the `doesSessionExist` function to check if a session exists. :::note[At the moment this feature is not supported through the Android SDK.] ::: :::note[At the moment this feature is not supported through the iOS SDK.] ::: :::note[At the moment this feature is not supported through the Flutter SDK.] ::: ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokensSession.init({ override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "...", override: { functions: (oI) => { return { ...oI, shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { try { let urlObj = new URL(url); if (!urlObj.pathname.startsWith("/auth")) { return false; } } catch (ignored) {} return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); }, }; }, }, }); ``` In the code above, the `shouldDoInterceptionBasedOnUrl` function is overridden to only allow interception for all API calls that start with `/auth` in their path. This ensures that API calls made from frontend SDKs (like sign out) continue to use the session tokens as expected by backend APIs. It also allows you to take control of how you want to attach session tokens to your own API calls (ones that have a path that don't start with `/auth`). If you want to also change how session tokens attach to API calls (like sign out), you can return `false` from the function override. Then, attach custom session headers using the [pre-API hook function](/references/frontend-sdks/hooks#pre-api-hook) on the frontend. --- # Usage inside an iframe Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/in-iframe ## Overview If your website can embed in an iframe that other websites consume, update your configuration based on this guide. ## Before you start If the sites where your iframe can embed share the same top-level domain as the iframe domain, then you can ignore this section. ## Steps ### 1. Update the frontend configuration - Set `isInIframe` to `true` during `Session.init` on the frontend. - You need to use `https` during testing / `dev` for this to work. You can use tools like [ngrok](https://ngrok.com/) to create a `dev` `env` with `https` on your website / API domain. - Switch to using header based auth - Provide a custom `windowHandler` and a custom `cookieHandler` to ensure that the app works on safari and chrome incognito. These handlers switch from using `document.cookies` to `localstorage` to store tokens on the frontend (since safari doesn't allow access to `document.cookies` in iframes), and use in-memory storage for chrome incognito (since chrome incognito doesn't even allow access to `localstorage`). You can find implementations of these handlers [here (`windowHandler`)](https://github.com/SuperTokens/supertokens-auth-react/blob/master/examples/with-next-iframe/config/windowHandler.js) and [here (`cookieHandler`)](https://github.com/SuperTokens/supertokens-auth-react/blob/master/examples/with-next-iframe/config/cookieHandler.js). You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx check=false reason="Requires SDK globals from surrounding application" import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ tokenTransferMethod: "header", isInIframe: true, }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokensUIInit({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUISession.init({ tokenTransferMethod: "header", isInIframe: true, }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx check=false reason="Requires SDK globals from surrounding application" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ tokenTransferMethod: "header", isInIframe: true, }), ], }); ``` :::warning[Not applicable to mobile apps] ::: ```tsx check=false reason="Requires SDK globals from surrounding application" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ tokenTransferMethod: "header", isInIframe: true, }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ cookieHandler, windowHandler, appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ tokenTransferMethod: "header", isInIframe: true, }), ], }); ``` :::warning[Because of the restrictions on access to storage on Chrome incognito, you must use in-memory storage to store the tokens on the frontend. This in turn implies that if the user refreshes the page, or if your app does a full page navigation, the user logs out.] ::: --- # Work with multiple API endpoints Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints ## Overview To enable use of sessions for multiple API endpoints, you need to update the configuration on both the frontend and backend. ## Before you start - All your API endpoints must have the same top level domain. For example, they can be `{"api.example.com", "api2.example.com"}`, but they cannot be `{"api.example.com", "api.otherdomain.com"}`. - Perform the backend configuration steps only if you are using cookie-based authentication. If using header based auth, please skip to step 3. ## Steps ### 1. Set the cookie domain in the backend configuration :::warning This step is only applicable for cookie based authentication. ::: Set the `cookieDomain` value to be the common top level domain. For example, if your API endpoints are `{"api.example.com", "api2.example.com", "api3.example.com"}`, the common portion of these endpoints is `".example.com"` (The dot is important). You would need to set the following: ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ cookieDomain: ".example.com", }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { cookieDomain := ".example.com" supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ CookieDomain: &cookieDomain, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( cookie_domain='.example.com' ) ] ) ``` The above sets the session cookies' domain to `example.com`, allowing them to send to `*.example.com`. :::note[Whilst the `cookieDomain` can start with a leading `.`, the value of the `apiDomain` in `appInfo` must point to an exact API domain only. This should be the API in which you want to expose all the auth related endpoints (for example `/auth/signin`).] For local development, you should not set the `cookieDomain` to an IP address-based domain, or `.localhost` - browsers reject these cookies. Instead, you should [alias `localhost` to a named domain and use that](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: ### 2. Set the older cookie domain in the backend configuration :::warning This step is only applicable for cookie based authentication. ::: To avoid locking out users with existing sessions (they get a 500 error when trying to refresh their session), set `olderCookieDomain` to match your previous `cookieDomain`. If your `cookieDomain` was not set, you can use an empty string. However, if you don't have any existing sessions, you can skip this step entirely. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ cookieDomain: ".example.com", olderCookieDomain: "", // Set to an empty string if your previous cookieDomain was unset. Otherwise, use your old cookieDomain value. }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { cookieDomain := ".example.com" olderCookieDomain := "" // Set to an empty string if your previous cookieDomain was unset. Otherwise, use your old cookieDomain value. supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ CookieDomain: &cookieDomain, OlderCookieDomain: &olderCookieDomain, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( cookie_domain='.example.com', older_cookie_domain='' # Set to an empty string if your previous cookie_domain was unset. Otherwise, use your old cookie_domain value. ) ] ) ``` :::warning[- If `olderCookieDomain` isn't set, users with older sessions get a 500 error from the session refresh endpoint, locking them out. This continues until you set `olderCookieDomain` correctly or they clear their cookies.] - Keep the value set for `olderCookieDomain` for 1 year because the cookie lifetime of the access token on the frontend is 1 year (even though the JWT expiry is a few hours). - If you have changed the `cookieDomain` more than once within one year, to prevent a stuck state, switch to [header-based auth](https://supertokens.com/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication) for all your clients. The important thing here is that you have to set the backend configuration to header even though that doc says it's optional. This ensures that all clients use header-based auth. - Changing the `cookieDomain` can cause a temporary spike in requests, even if you set the `olderCookieDomain` correctly. This happens because older sessions, with older cookie domain, require additional refresh calls to clear their old cookies and set new ones. This spike is a one-time event and should not recur after the update. ::: :::info[Set the `olderCookieDomain` value to prevent clients from having multiple session cookies from different domains. This can happen when cookies from a previous domain are still valid and sent with requests. For instance, if your previous `cookieDomain` was `api.example.com` and the new one is `.example.com`, both sets of cookies would send to the `apiDomain` `api.example.com`, leading to an inconsistent state. This can cause issues until you clear the older cookies. Setting `olderCookieDomain` in the configuration ensures that the SuperTokens SDK can automatically remove these older cookies.] ::: ### 3. Update the frontend configuration Set the same value for `sessionTokenBackendDomain` on the frontend. This allows the frontend SDK to apply interception and automatic refreshing across all your API calls: You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ sessionTokenBackendDomain: ".example.com", }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUISession.init({ sessionTokenBackendDomain: ".example.com", }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ sessionTokenBackendDomain: ".example.com", }), ], }); ``` ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ sessionTokenBackendDomain: ".example.com", }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ sessionTokenBackendDomain: ".example.com", }), ], }); ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "...", sessionTokenBackendDomain: ".example.com", }); ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "...") .sessionTokenBackendDomain(".example.com") .build() } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "...", sessionTokenBackendDomain: ".example.com" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; void initialiseSuperTokens() { SuperTokens.init( apiDomain: "...", sessionTokenBackendDomain: ".example.com", ); } ``` --- # Implement user impersonation Source: https://supertokens.com/docs/post-authentication/session-management/advanced-workflows/user-impersonation ## Overview Impersonating a user allows you to login as them without using their credentials. This is useful for testing purposes, or for customer support. This guide shows you how to achieve this by only allowing a certain type of users, `admins`, to perform the impersonation. ## Before you start :::danger[Impersonation is a privileged operation] Require a valid staff session and enforce an explicit backend role or permission check before reading the target identifier, looking up the target, or creating a session. Hiding the UI, checking a role in the browser, restricting access to approved IP addresses, or knowing the endpoint URL is not authorization. ::: For production use, apply these additional controls: - Require a recent step-up authentication before each impersonation starts. For example, require the staff member to complete MFA again. - Require a reason and write append-only audit events for every attempt and outcome. Include the actor's user ID, target user ID, reason, timestamp, outcome, and impersonation session handle. Do not rely only on a custom access token claim as the audit record. Fail closed if the start event cannot be recorded. - Set a short maximum duration. Enforce the deadline on the backend, revoke the impersonation session when it expires, and provide an explicit way to terminate it early. Monitor and alert on unusual impersonation activity. - Decide which targets and actions staff may access while impersonating. For example, prevent impersonation of other administrators and block credential, MFA, payment, and destructive account changes unless your policy explicitly allows them. ## Steps ### 1. Create the impersonation endpoint Create a new API endpoint that accepts a stable user ID and creates a new impersonation session for that user. If you instead use an email address, phone number, or other account information, require the lookup to return exactly one user; never select the first of multiple matches. In order for this to work, admins need to first log in to the application as themselves. Once they create their session (like any regular user's session), they can call the API via a frontend UI that's only shown to them. You can detect the admin role on the frontend by seeing [this guide](/additional-verification/user-roles/protecting-routes#protect-frontend-routes). ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import Session from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post( "/impersonate", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }), async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); res.json({ message: "Impersonation successful!" }); }, ); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Session from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/impersonate", method: "post", options: { pre: [ { method: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }), }, ], }, handler: async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return res.response({ message: "Impersonation successful!" }).code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import Session from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); fastify.post( "/impersonate", { preHandler: verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }), }, async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); res.send({ message: "Impersonation successful!" }); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { middleware } from "supertokens-node/framework/awsLambda"; import Session from "supertokens-node/recipe/session"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; async function impersonate(awsEvent: SessionEvent) { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(awsEvent, awsEvent, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return { body: JSON.stringify({ message: "Impersonation successful!" }), statusCode: 200, }; } exports.handler = verifySession(impersonate, { overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import Session from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); router.post( "/impersonate", verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }), async (ctx, next) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(ctx, ctx, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); ctx.body = { message: "Impersonation successful!" }; }, ); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; class Login { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @post("/impersonate") @intercept( verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }), ) @response(200) async handler() { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(this.ctx, this.ctx, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return { message: "Impersonation successful!" }; } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { createNewSession } from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; export default async function impersonate(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], })(req, res, next); }, req, res, ); let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await superTokensNextWrapper( async (next) => { await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); }, req, res, ); res.json({ message: "Impersonation successful!", }); } ``` ```ts check=false reason="Requires surrounding framework application context" import { Controller, Post, Res, Req, UseGuards } from "@nestjs/common"; import type { Response, Request } from "express"; import { AuthGuard } from "./auth/auth.guard"; import { createNewSession, SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("impersonate") @UseGuards( new AuthGuard({ overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }), ) async postLogin(@Req() req: Request, @Res() res: Response): Promise<{ message: string }> { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { email, }); if (user.length !== 1) { throw new Error("Identifier does not uniquely identify a user"); } await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { isImpersonation: true, }); return { message: "Impersonation successful!" }; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "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/recipe/userroles/userrolesclaims" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, impersonate).ServeHTTP(rw, r) }) } func impersonate(w http.ResponseWriter, r *http.Request) { email := "..." // read from request body // we are using emailpassword recipe here, but you can use the recipe you use // as well.. user, err := emailpassword.GetUserByEmail("public", email) if err != nil { // Send 500 to client return } if user == nil { // Send 400 to client cause user does not exist return } _, err = session.CreateNewSession(r, w, "public", user.ID, map[string]interface{}{ "isImpersonation": true, }, nil) if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // Send 500 to client } return } // Send 200 success to client } ``` ```python check=false reason="Requires surrounding framework application context" from fastapi import Depends, Request from fastapi.responses import JSONResponse from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.asyncio import create_new_session from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.types.base import AccountInfoInput @app.post("/impersonate") async def impersonate( request: Request, session: SessionContainer = Depends( verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) ), ): email = "..." # get from request body # we use the email password recipe here, but you can use the recipe you use user = await list_users_by_account_info("public", AccountInfoInput(email=email)) if len(user) != 1: # return a 400 error because the identifier is missing or ambiguous return await create_new_session( request, "public", user[0].login_methods[0].recipe_user_id, {"isImpersonation": True}, ) return JSONResponse({"message": "Impersonation complete!"}) ``` ```python check=false reason="Requires surrounding framework application context" from flask import jsonify from flask.wrappers import Request from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session.syncio import create_new_session from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput @app.route("/impersonate", methods=["POST"]) @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) def login(request: Request): email = "..." # get from request body # we use the email password recipe here, but you can use the recipe you use user = list_users_by_account_info("public", AccountInfoInput(email=email)) if len(user) != 1: # return a 400 error because the identifier is missing or ambiguous return create_new_session( request, "public", user[0].login_methods[0].recipe_user_id, {"isImpersonation": True}, ) return jsonify({"message": "Impersonation complete!"}) ``` ```python from django.http import HttpRequest, JsonResponse from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe.session.asyncio import create_new_session from supertokens_python.recipe.session.framework.django.asyncio import verify_session from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.types.base import AccountInfoInput @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] ) async def impersonate(request: HttpRequest): email = "..." # get from request body # we use the email password recipe here, but you can use the recipe you use user = await list_users_by_account_info("public", AccountInfoInput(email=email)) if len(user) != 1: # return a 400 error because the identifier is missing or ambiguous return await create_new_session( request, "public", user[0].login_methods[0].recipe_user_id, {"isImpersonation": True}, ) return JsonResponse({"message": "User logged in!"}) ``` ```tsx check=false reason="Requires surrounding framework application context and application audit logging" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; import Session, { createNewSession } from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; import { backendConfig } from "@/app/config/backend"; import { assertRecentImpersonationStepUp, createImpersonationAttemptId, getAuthorizedImpersonationTenant, getImpersonationExpiry, recordImpersonationAuditEvent, registerImpersonationExpiry, } from "@/app/auth/impersonation-security"; SuperTokens.init(backendConfig()); function copyCollectedCredentials(source: CollectingResponse, destination: CollectingResponse) { source.headers.forEach((value, key) => destination.setHeader(key, value, false)); for (const cookie of source.cookies) { destination.setCookie( cookie.key, cookie.value, cookie.domain, cookie.secure, cookie.httpOnly, cookie.expires, cookie.path, cookie.sameSite, ); } } export function POST(request: NextRequest) { return withPreParsedRequestResponse( request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { const actorSession = await Session.getSession(baseRequest, baseResponse, { sessionRequired: true, overrideGlobalClaimValidators: async (globalValidators) => [ ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), ], }); const actorUserId = actorSession.getUserId(); const attemptId = createImpersonationAttemptId(); await recordImpersonationAuditEvent({ attemptId, actorUserId, outcome: "ATTEMPT_STARTED", }); try { await assertRecentImpersonationStepUp(actorSession); } catch { await recordImpersonationAuditEvent({ attemptId, actorUserId, outcome: "STEP_UP_FAILED", }); return NextResponse.json({ message: "Impersonation request denied" }, { status: 403 }); } // This application-owned helper derives allowed tenants from trusted // server-side staff entitlements. It must not trust a request-body tenant ID. let tenantId: string; try { tenantId = await getAuthorizedImpersonationTenant({ actorUserId, actorSessionTenantId: actorSession.getTenantId(), }); } catch { await recordImpersonationAuditEvent({ attemptId, actorUserId, outcome: "TENANT_AUTHORIZATION_FAILED", }); return NextResponse.json({ message: "Impersonation request denied" }, { status: 403 }); } // Authorization and step-up have succeeded. Only now read the target and reason. let targetRecipeUserId = "..."; // validate and read a stable recipe user ID from the request body let reason = "..."; // require a non-empty support or incident reason let targetUser: Awaited>; try { targetUser = await SuperTokens.getUser(targetRecipeUserId); } catch { await recordImpersonationAuditEvent({ attemptId, actorUserId, tenantId, reason, outcome: "TARGET_LOOKUP_FAILED", }); return NextResponse.json({ message: "Impersonation request denied" }, { status: 400 }); } const targetLoginMethod = targetUser?.loginMethods.find( (loginMethod) => loginMethod.recipeUserId.getAsString() === targetRecipeUserId, ); if (!targetUser || !targetLoginMethod?.tenantIds.includes(tenantId)) { await recordImpersonationAuditEvent({ attemptId, actorUserId, targetRecipeUserId, tenantId, reason, outcome: "TARGET_DENIED", }); return NextResponse.json({ message: "Impersonation request denied" }, { status: 400 }); } let expiresAt: number; try { expiresAt = await getImpersonationExpiry({ actorUserId, tenantId }); } catch { await recordImpersonationAuditEvent({ attemptId, actorUserId, targetUserId: targetUser.id, targetRecipeUserId, tenantId, reason, outcome: "EXPIRY_DERIVATION_FAILED", }); return NextResponse.json({ message: "Impersonation request denied" }, { status: 500 }); } await recordImpersonationAuditEvent({ attemptId, actorUserId, targetUserId: targetUser.id, targetRecipeUserId, tenantId, reason, outcome: "ATTEMPT_APPROVED", expiresAt, }); const stagedResponse = new CollectingResponse(); let impersonationSession: Awaited> | undefined; let stage: "SESSION_CREATION" | "EXPIRY_REGISTRATION" | "SUCCESS_AUDIT" = "SESSION_CREATION"; try { impersonationSession = await createNewSession(baseRequest, stagedResponse, tenantId, targetLoginMethod.recipeUserId, { isImpersonation: true, impersonatedBy: actorUserId, impersonationExpiresAt: expiresAt, }); stage = "EXPIRY_REGISTRATION"; await registerImpersonationExpiry({ sessionHandle: impersonationSession.getHandle(), expiresAt, }); stage = "SUCCESS_AUDIT"; await recordImpersonationAuditEvent({ attemptId, actorUserId, targetUserId: targetUser.id, targetRecipeUserId, tenantId, reason, outcome: "SESSION_CREATED", impersonationSessionHandle: impersonationSession.getHandle(), expiresAt, }); } catch { let revoked: boolean | undefined; if (impersonationSession) { try { revoked = await Session.revokeSession(impersonationSession.getHandle()); } catch { revoked = false; } } let failureOutcome = "SESSION_CREATION_FAILED"; if (stage === "EXPIRY_REGISTRATION") { failureOutcome = "EXPIRY_REGISTRATION_FAILED"; } else if (stage === "SUCCESS_AUDIT") { failureOutcome = "SUCCESS_AUDIT_FAILED"; } await recordImpersonationAuditEvent({ attemptId, actorUserId, targetUserId: targetUser.id, targetRecipeUserId, tenantId, reason, outcome: failureOutcome, impersonationSessionHandle: impersonationSession?.getHandle(), revoked, revocationFailed: revoked === false, expiresAt, }); return NextResponse.json({ message: "Impersonation request failed" }, { status: 500 }); } // Staged credentials remain unreachable until every post-creation control succeeds. copyCollectedCredentials(stagedResponse, baseResponse); return NextResponse.json({ message: "Impersonation successful" }); }, ); } ``` :::info[Multi Tenancy] Most examples use the default `"public"` tenant. In a multi-tenant application, derive the target tenant from trusted server-side data and verify that the actor may impersonate users in that tenant. Never authorize a tenant only because its ID was supplied in the request body. If the client selects a tenant, treat that value as client-controlled input and check it against the actor's server-side tenant assignments before target lookup or session creation. ::: - The API should be called from your frontend application so that the frontend SDK's network interceptors run. - In the APIs above, required-session verification and backend admin-role validation run before the request reads or looks up the target. A missing or invalid session is rejected, and a session without the required role is rejected with `403`. - Prefer a stable target user or recipe-user ID. If you look up by account information instead, reject zero or multiple matches rather than selecting the first result. - A new session is then created using the target user's user ID. The `isImpersonation` flag is added to the access token payload so that the frontend can show that the staff member is impersonating a user. Backend APIs can also use this claim to restrict actions while impersonating. Treat claims such as `isImpersonation` and `impersonatedBy` as enforcement context, not as a durable audit log. - The new session tokens attach to the response and overwrite the active admin credentials in that browser. This does not revoke the original admin session in SuperTokens. Cookies apply if the request contains the `st-auth-mode: "cookie"` header; otherwise, the mode is header-based authentication. The frontend interceptors set this header automatically. - Signing out revokes the current impersonation session. Also provide explicit early termination and enforce your maximum duration on the backend; do not depend on the user remembering to sign out. --- # Session Management Source: https://supertokens.com/docs/post-authentication/session-management/introduction ## Session lifecycle summary - Signing in creates a session and issues access and refresh tokens to the frontend. - Protected API calls verify the access token and its expiry. - When an access token expires, the frontend uses the refresh token to obtain new tokens and retries the original request. - Revoking a session removes its refresh token and session information, so refresh fails and the user must log in again. ## Overview SuperTokens provides session management out of the box. Sessions get created when a user signs in and maintained throughout the authentication lifecycle. In the next graphic you can see a high level overview of how the session flow works. Flowcharts showing an overview of session flow - After sign in, the system creates a new session by issuing a refresh and access token to the frontend. - The frontend sends the access token for each API call that requires session authentication. - These API calls verify the access token and its expiry. If verification fails, the API throws a session expired error, else, execution continues. - If an API throws session expired error, the frontend uses its refresh token to get a new refresh and a new access token. The frontend performs this action via a special API on your backend. If you revoke a session, this API also throws session expired after which the user has to login again. - After obtaining a new set of tokens, the frontend retries the original API call, yielding the desired result. - To revoke a session, the backend removes the refresh token and its session information from its database. ## Information about session cookies | Cookie Name | Description | |-------------|-------------| | `sAccessToken` | This is the session's access token which each API call uses to verify that the user authenticated and to get their user ID (when using cookie based authentication). | | `sRefreshToken` | This is the session's refresh token which retrieves a new access (and refresh token) when the existing access token expires (when using cookie based authentication). | | `sFrontToken` | Used to access a session's access token payload and user ID on the frontend without exposing the `sAccessToken`. | | `sAntiCsrf` | Used to prevent CSRF attacks. | | `st-last-access-token-update` | Used by the frontend to know if a session exists, and when the access token has changed. | | `st-access-token` | Used by the frontend to store the access token for header based authentication. | | `st-refresh-token` | Used by the frontend to store the refresh token for header based authentication. | ## Getting started Including the `Session` recipe in the initial configuration enables sessions. This step is outlined in all the guides that show you how to integrate different authentication methods: [Email Password](/authentication/email-password/introduction), [Passwordless](/authentication/passwordless/introduction) or [Social Login](/authentication/social/introduction). Additionally, this section includes information on how to work with sessions after a user has signed in. See how you can read the properties of an active session. Learn how to revoke a session either through a user action or programmatically. ## Customization Update the configuration to share sessions across different subdomains. Choose how tokens are sent and stored during the authentication lifecycle. Learn how to track session data event if a user has not logged in. See how to act as a different user during the authentication flow. Change the default error handling behaviour. Use the same session management logic for multiple API endpoints. Block access tokens from being used. --- # Security Source: https://supertokens.com/docs/post-authentication/session-management/security ## Overview The following page takes you through some common security considerations that the **SuperTokens** `Session` recipe handles. --- ## Anti-csrf CSRF attacks can happen if a logged in user visits a malicious website which makes an API call to your website's API to maliciously change that user's data. To protect against this attack, the cookie `sameSite` attribute works along with some anti-csrf measures. This attribute declares if your cookies should restrict to a first-party or same-site context. Configuring `sameSite` can prevent CSRF attacks. For example, if `sameSite` is `lax`, the browser only sends cookies for requests that originate from the same top level domain as the API's domain. If a user visits a malicious site, requests from those sites do not have the session cookies. ### Configure anti-csrf :::warning[- SuperTokens automatically defends against CSRF attacks.] - Please only change this setting if you know what you are doing. If you are unsure, please feel free to [ask questions](https://supertokens.com/discord). - This setting does not apply while using header-based authentication, since they get the same protection as `antiCsrf` set to `VIA_CUSTOM_HEADER`. ::: You can change the `antiCsrf` configuration option to take control of the kind of protection you get. You can use on of the following values: - `"NONE"` would disable any anti-csrf protection from our end. You can use this if you have an implementation of CSRF protection. - `"VIA_CUSTOM_HEADER"` uses [this method](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers) to prevent CSRF protection. This sets automatically if `sameSite` is `none` or if your `apiDomain` and `websiteDomain` do not share the same top level domain name. - `"VIA_TOKEN"` uses an explicit anti-csrf token. Use this method if you want to allow any origin to query your APIs. This method may cause issues in browsers like Safari, especially if your site embeds as an `iframe`. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ antiCsrf: "VIA_CUSTOM_HEADER", // Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" antiCsrf := "VIA_CUSTOM_HEADER" supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ AntiCsrf: &antiCsrf, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( # Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" anti_csrf='VIA_CUSTOM_HEADER' ) ] ) ``` --- ## Cookie consent [Per GDPR](https://gdpr.eu/cookies/), users do not need to give consent for your application to use session cookies. This is because they fall under essential cookies and not tracking cookies: :::info[Important] "While it is not required to obtain consent for these cookies, explain to the user what they do and why they are necessary." ::: --- ## Same site cookies To ensure session cookies have protection from CSRF attacks, the ``sameSite`` cookie attribute ensures this protection. The ``sameSite`` cookie attribute declares if your cookies should restrict to a first-party or same-site context. The ``sameSite`` attribute can have three possible values: - ``none`` - Cookies attach in all contexts, that is, cookies attach to both first-party and cross-origin requests. - On Safari however, if third-party cookies do not work (which is the default behavior), and the website and `API` domains do not share the same top-level domain, then cookies do not go. Please check [the session management page](/post-authentication/session-management/switch-between-cookies-and-header-authentication) to see how you can switch to using headers. - ``lax`` - Cookies are only sent in a first-party context and along with `GET` requests initiated by third party websites (that result in browser navigation - user clicking on a link). - ``strict`` - Cookies are only sent in a first-party context and not sent along with requests initiated by third party websites. ### Configuration :::warning[- SuperTokens automatically sets the value of the ``sameSite`` cookie attribute based on your website and `API` domain configuration.] - Please only change this setting if you are a web security expert. If you are unsure, please feel free to [ask questions](https://supertokens.com/discord). ::: ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ cookieSameSite: "strict", // Should be one of "strict" or "lax" or "none" }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // Should be one of "strict" or "lax" or "none" cookieSameSite := "lax" supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ CookieSameSite: &cookieSameSite, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( cookie_same_site='lax' # Should be one of 'strict' or 'lax' or 'none' ) ] ) ``` --- ## Cookies and HTTPS SuperTokens ensures that cookies have security by enabling the ``secure`` flag when generating session cookies. When set, the ``secure`` attribute limits the scope of the cookie to attach only to secure domains. This results in the cookie only attaching to requests transmitted over `https`. This, in turn, prevents cookie theft via man in the middle attacks. You can explicitly set the security level of cookies using the next snippet: :::note[If not explicitly set, SuperTokens automatically determines the value for the `secure` attribute based on your API domain having `http` or `https`.] ::: ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ cookieSecure: true, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { cookieSecure := true supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ CookieSecure: &cookieSecure, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( cookie_secure=True ) ] ) ``` --- ## Access Token signing key rotation Access Token signing key rotation implies that the secret key for signing the access tokens changes at a fixed time interval. This reduces the risk of key theft. :::info[- Existing logged in users are not logged out on key change.] - This feature enables by default. ::: ### Change the key rotation interval ```bash docker run \ -p 3567:3567 \ -e ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL=168 \ -d supertokens/supertokens- ``` ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command access_token_dynamic_signing_key_update_interval: 168 ``` - ``access_token_dynamic_signing_key_update_interval`` - Time in hours for how frequently the signing key changes. - It must have a ``number`` value with, the default value set to ``168`` :::info[For managed service, update this value in the **Session Management** configuration card in the relevant deployment's **Configuration** page.] ::: ### Use static keys If you do not want to use dynamic keys for session creation, then you can tell SuperTokens to use the static key instead. This is useful in cases where you want to [hard-code the public key for JWT verification in some process](/additional-verification/session-verification/protect-api-routes#with-the-public-key-string). ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ useDynamicAccessTokenSigningKey: false, }), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { useDynamicAccessTokenSigningKey := false supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ UseDynamicAccessTokenSigningKey: &useDynamicAccessTokenSigningKey, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( use_dynamic_access_token_signing_key=False ) ] ) ``` :::warning[Updating this value causes a spike in the session refresh API, as and when users visit your application.] ::: --- ## See also --- # Session Invalidation Source: https://supertokens.com/docs/post-authentication/session-management/session-invalidation ## Overview You can invalidate a session in **SuperTokens** in different ways. The main recommendation is to use the `signOut` function from the frontend SDK. Besides that you can also revoke sessions manually, through the backend SDKs. This guide shows you how to implement each of these. ## Before you start :::info[Access token guidance] This guide applies to scenarios involving **SuperTokens Session Access Tokens**. ::: --- ## User sign out The frontend SDK exposes a `signOut` function that revokes the session for the user. You need to add your own UI element for this since the library does not expose any components. The `signOut` function calls the sign out API exposed by the session recipe on the backend and revokes the current session. It does not revoke the user's other sessions. Use the explicit all-session API shown below when that is the intended behavior. If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, the SDKs automatically perform a session refresh before revoking the session. :::note[You have to add your own redirection logic after the sign out call completes.] ::: ```tsx import React from "react"; import { signOut } from "supertokens-auth-react/recipe/session"; function NavBar() { async function onLogout() { await signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } return (
  • Home
  • Logout
); } ```
```tsx import Session from "supertokens-web-js/recipe/session"; async function logout() { await Session.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ```
```tsx import Session from "supertokens-web-js/recipe/session"; async function logout() { await Session.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` ```tsx check=false reason="Requires SDK globals from surrounding application" async function logout() { await supertokensSession.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` ```tsx import SuperTokens from "supertokens-react-native"; async function logout() { await SuperTokens.signOut(); // navigate to the login screen.. } ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun logout() { SuperTokens.signOut(this); // navigate to the login screen.. } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func signOut() { SuperTokens.signOut(completionHandler: { error in if error != nil { // handle error } else { // Signed out successfully } }) } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; Future signOut() async { await SuperTokens.signOut( completionHandler: (error) => { // Handle error if any } ); } ``` ### Expose a backend sign out method If you do not want to use the frontend function you can expose a backend sign out method. ```tsx 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("/someapi", verifySession(), async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); res.send("Success! User session revoked"); }); ``` ```tsx 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: "/someapi", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); return res.response("Success! User session revoked").code(200); }, }); ``` ```tsx 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( "/someapi", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); res.send("Success! User session revoked"); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function someapi(awsEvent: SessionEvent) { // This will delete the session from the db and from the frontend (cookies) await awsEvent.session!.revokeSession(); return { body: JSON.stringify({ message: "Success! User session revoked" }), statusCode: 200, }; } exports.handler = verifySession(someapi); ``` ```tsx 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("/someapi", verifySession(), async (ctx: SessionContext, next) => { // This will delete the session from the db and from the frontend (cookies) await ctx.session!.revokeSession(); ctx.body = "Success! User session revoked"; }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import { SessionContext } from "supertokens-node/framework/loopback"; class Logout { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/someapi") @intercept(verifySession()) @response(200) async handler() { // This will delete the session from the db and from the frontend (cookies) await this.ctx.session!.revokeSession(); return "Success! User session revoked"; } } ``` ```tsx 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 someapi(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); res.send("Success! User session revoked"); } ``` ```ts check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("someapi") @UseGuards(new AuthGuard()) async postSomeAPI(@Session() session: SessionContainer): Promise { await session.revokeSession(); return "Success! User session revoked"; } } ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func someAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) // This will delete the session from the db and from the frontend (cookies) err := sessionContainer.RevokeSession() if err != nil { err = supertokens.ErrorHandler(err, r, w) if err != nil { // TODO: Send 500 status code to client } return } // TODO: Send 200 response to client } ``` ```python from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.session import SessionContainer from fastapi import Depends from fastapi.responses import PlainTextResponse async def some_api(session: SessionContainer = Depends(verify_session())): await session.revoke_session() # This will delete the session from the db and from the frontend (cookies) return PlainTextResponse(content='success') ``` ```python check=false reason="Requires surrounding framework application context" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session import SessionContainer from flask import g @app.route('/some_api', methods=['POST']) @verify_session() def some_api(): session: SessionContainer = g.supertokens session.sync_revoke_session() # This will delete the session from the db and from the frontend (cookies) return 'success' ``` ```python check=false reason="Requires surrounding async application context" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def some_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) # Set by the session middleware. await session.revoke_session() ``` ```tsx check=false reason="Requires surrounding framework application context" 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 }); } // This will delete the session from the db and from the frontend (cookies) await session!.revokeSession(); return NextResponse.json({ message: "Success! User session revoked" }); }); } ``` :::info[Tip] If you are using the pre-built UI and the `` component, you can set custom post-logout logic with the `onSessionExpired` prop. The handler gets called if: - The backend has revoked the session, but not the frontend. - The user has been inactive for too long and their refresh token has expired. ```tsx check=false reason="Requires surrounding framework application context" import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import MyComponent from "./myComponent"; const App = () => { return ( { /* ... */ }} > ); }; ``` ::: --- ## Direct session invalidation To invalidate a session without relying on the intervention of a user you can create your own custom methods using the backend SDKs. :::warning[This method of revoking a session only deletes the session from the database and not from the frontend.] This implies that the user can still access protected endpoints while their access token is alive. If you want to instantly logout the user in this mode, you should [enable access token blacklisting](/post-authentication/session-management/advanced-workflows/access-token-blacklisting). ::: ### Revoke a specific session ```tsx import Session from "supertokens-node/recipe/session"; async function revokeSession(sessionHandle: string) { let revoked = await Session.revokeSession(sessionHandle); } ``` ```go import "github.com/supertokens/supertokens-golang/recipe/session" func main() { sessionHandle := "someSessionHandle" revoked, err := session.RevokeSession(sessionHandle) if err != nil { // TODO: Handle error return } if revoked { // session was revoked } else { // session was not found } } ``` ```python from supertokens_python.recipe.session.asyncio import revoke_session async def some_func(): session_handle = "someSessionHandle" _ = await revoke_session(session_handle) ``` ```python from supertokens_python.recipe.session.syncio import revoke_session session_handle = "someSessionHandle" revoked = revoke_session(session_handle) ``` You can fetch all the `sessionHandle`s for a user using the [`getAllSessionHandlesForUser` function](/post-authentication/session-management/access-session-data#fetch-all-user-sessions) ### Revoke all sessions for a user ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; let app = express(); app.use("/revoke-all-user-sessions", async (req, res) => { let userId = req.body.userId; await Session.revokeAllSessionsForUser(userId); res.send("Success! All user sessions have been revoked"); }); ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { tenantId := "public" revokedSessionHandles, err := session.RevokeAllSessionsForUser("userId", &tenantId) if err != nil { // TODO: Handle error return } // revokedSessionHandles is an array of revoked session handles. fmt.Println(revokedSessionHandles) } ``` ```python from supertokens_python.recipe.session.asyncio import revoke_all_sessions_for_user async def some_func(): user_id = "someUserId" revoked_session_handles = await revoke_all_sessions_for_user(user_id) print(revoked_session_handles) # revoked_session_handles is an array of revoked session handles. ``` ```python from supertokens_python.recipe.session.syncio import revoke_all_sessions_for_user user_id = "someUserId" revoked_session_handles = revoke_all_sessions_for_user(user_id) # revoked_session_handles is an array of revoked session handles. ``` :::info[Multi Tenancy] By default, revokeAllSessionsForUser deletes all the sessions for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. ::: --- ## See also --- # Share sessions across sub domains Source: https://supertokens.com/docs/post-authentication/session-management/share-session-across-sub-domains ## Overview Configure sharing sessions across multiple subdomains in SuperTokens by setting the `sessionTokenFrontendDomain` attribute of the Session recipe in your frontend code. :::info[Example] - Your app has two subdomains `abc.example.com` and `xyz.example.com`. Assume that the user logs in via `example.com` - To enable sharing sessions across `example.com`, `abc.example.com` and `xyz.example.com`, set the `sessionTokenFrontendDomain` attribute to `.example.com`. ::: ## Steps ### 1. Update the frontend configuration You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { // ... // this should be equal to the domain where the user will see the login UI apiDomain: "...", appName: "...", websiteDomain: "https://example.com", }, recipeList: [ Session.init({ sessionTokenFrontendDomain: ".example.com", }), ], }); ``` ```tsx check=false reason="Partial configuration example" supertokensUIInit({ appInfo: { // ... // this should be equal to the domain where the user will see the login UI apiDomain: "...", appName: "...", websiteDomain: "https://example.com", }, recipeList: [ supertokensUISession.init({ sessionTokenFrontendDomain: ".example.com", }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ sessionTokenFrontendDomain: ".example.com", }), ], }); ``` :::warning - Do not set `sessionTokenFrontendDomain` to a value that's in the [public suffix list](https://publicsuffix.org/list/public_suffix_list.dat) (Search for your value without the leading dot). Otherwise, session management does not work. - Do not set `sessionTokenFrontendDomain` to `.localhost` or an IP address based domain with a leading `.` since browsers reject these cookies. For local development, you should configure [your machine to use alias for `localhost`](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: :::info[Multi Tenancy] If each tenant belongs to one subdomain, and a user has access to more than one tenant, the tenant ID in the session is always the one from which they logged in. For example, if a user has access to tenant `t1.example.com` and `t2.example.com`, and they logged in via `t1.example.com`, then the tenant ID in the session is always `t1`. This remains true even if they navigate to `t2.example.com` or make an API request from `t2.example.com`. To solve this, add extra information about access token payload containing a list of all the tenants that the user has access to. Then read from that list instead of the `tId` claim. ::: :::warning[Not applicable] ::: ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ sessionTokenFrontendDomain: ".example.com", }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ // ... sessionTokenFrontendDomain: ".example.com", }), ], }); ``` :::warning - Do not set `sessionTokenFrontendDomain` to a value that's in the [public suffix list](https://publicsuffix.org/list/public_suffix_list.dat) (Search for your value without the leading dot). Otherwise, session management does not work. - Do not set `sessionTokenFrontendDomain` to `.localhost` or an IP address based domain with a leading `.` since browsers reject these cookies. For local development, you should configure [your machine to use alias for `localhost`](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: :::info[Multi Tenancy] If each tenant belongs to one subdomain, and a user has access to more than one tenant, the tenant ID in the session is always the one from which they logged in. For example, if a user has access to tenant `t1.example.com` and `t2.example.com`, and they logged in via `t1.example.com`, then the tenant ID in the session is always `t1`. This remains true even if they navigate to `t2.example.com` or make an API request from `t2.example.com`. To solve this, add extra information about access token payload containing a list of all the tenants that the user has access to. Then read from that list instead of the `tId` claim. ::: --- ## See also --- # Switch between cookie and header-based sessions Source: https://supertokens.com/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication ## Overview SuperTokens supports 2 methods of authorizing requests. The following guide shows you how to switch between them. ### Cookie based - The default in the web SDKs - Uses [`HttpOnly` cookies](https://owasp.org/www-community/HttpOnly) by default to prevent token theft via XSS ### Header based - The default in the mobile SDKs - Uses the [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) with a [`Bearer` auth-scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes) - This can make it easier to work with API gateways and third-party services - Preferable in mobile environments, since they can have buggy and/or unreliable cookie implementations When creating or authorising sessions, the SDK has to choose to send the tokens to the frontend by cookies or custom headers. The backend controls this choice, but it follows a preference set in the frontend configuration. ## Before you start :::warning[We recommend cookie-based sessions in browsers because header-based sessions require saving the access and refresh tokens in storage vulnerable to XSS attacks.] ::: ## Steps ### 1. Update the frontend configuration You can provide a `tokenTransferMethod` property in the configuration of the Session recipe to set the preferred token transfer method. The backend receives this method with every request in the `st-auth-mode` header. By default, the backend follows this preference. You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: This change is in your auth route configuration. ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ tokenTransferMethod: "header", // or "cookie" }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUISession.init({ tokenTransferMethod: "header", // or "cookie" }), ], }); ``` This change goes in the `supertokens-web-js` SDK configuration at the root of your application: ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ tokenTransferMethod: "header", // or "cookie" }), ], }); ``` You can use the `tokenTransferMethod` builder method to set what mode the SDK should use for sessions. ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ Session.init({ tokenTransferMethod: "header", // or "cookie" }), ], }); ``` ```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ appInfo: { apiDomain: "...", appName: "...", }, recipeList: [ supertokensSession.init({ tokenTransferMethod: "header", // or "cookie", }), ], }); ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "...", tokenTransferMethod: "header", // or "cookie". "header" by default }); ``` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "...") .tokenTransferMethod("header") // or "cookie". "header" by default .build() } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "...", tokenTransferMethod: .header // or .cookie . header by default ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "...", tokenTransferMethod: SuperTokensTokenTransferMethod.COOKIE, ); } ``` ### Using cookies When using cookies for session management you need to enable cookies before making requests. #### With `HttpURLConnection` ```kotlin import android.app.Application import com.supertokens.session.SuperTokens import com.supertokens.session.SuperTokensHttpURLConnection import com.supertokens.session.SuperTokensPersistentCookieStore import java.net.CookieManager class MainApplication: Application() { override fun onCreate() { super.onCreate() CookieManager.setDefault(CookieManager(SuperTokensPersistentCookieStore(this), null)) // TODO: Make sure to call SuperTokens.init } } ``` `SuperTokensPersistentCookieStore` is a cookie store that SuperTokens provides which uses SharedPreferences to persist sessions across app launches #### With `OkHttp` / `Retrofit` ```kotlin import android.content.Context import com.franmontiel.persistentcookiejar.PersistentCookieJar import com.franmontiel.persistentcookiejar.cache.SetCookieCache import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor import com.supertokens.session.SuperTokens import com.supertokens.session.SuperTokensInterceptor import okhttp3.OkHttpClient import retrofit2.Retrofit class NetworkManager { fun getClient(context: Context): OkHttpClient { val clientBuilder = OkHttpClient.Builder() clientBuilder.addInterceptor(SuperTokensInterceptor()) // TODO: Make sure to call SuperTokens.init // Sets persistent cookies clientBuilder.cookieJar(PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context))) val client = clientBuilder.build() // REQUIRED FOR RETROFIT ONLY val instance = Retrofit.Builder() .baseUrl("") .client(client) .build() return client } } ``` In the above example, `PersistentCookieJar` from `'com.github.franmontiel:PersistentCookieJar:v1.0.1'` enables persistently storing cookies using SharedPreferences to maintain sessions across app launches. ### 2. Update the backend configuration (optional) This step is optional. You can force the backend to use a specific token transfer method regardless of the frontend configuration. :::warning[**You should not set this on the backend if you have more than one client using different modes** (for example if you have a website that uses cookie based, and a mobile app that uses header based sessions).] ::: ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ getTokenTransferMethod: () => "header", }), ], }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "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{ GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { return sessmodels.HeaderTransferMethod }, }), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest from typing import Dict, Any def get_token_transfer_method(req: BaseRequest, for_create_new_session: bool, user_context: Dict[str, Any]): # OR use session.init(get_token_transfer_method=lambda *_: "header") return "header" init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework='...', recipe_list=[ session.init( get_token_transfer_method=get_token_transfer_method ) ] ) ``` :::note[By default, session verification allows both cookie and authorization bearer tokens. When creating a new session, it follows the preference of the frontend indicated by the `st-auth-mode` request header (set by the frontend SDK).] ::: --- # Account deduplication Source: https://supertokens.com/docs/post-authentication/user-management/account-deduplication ## Overview Users may forget the initial method they used to sign up and may create multiple accounts with the same email ID - leading to a poor user experience. Preventing this from happening refers to account deduplication. As an example, assume that your app has Google and GitHub login. There exists a user who had signed up with Google using their email ID - `user@gmail.com`. If this user then tries to sign up with GitHub, which has this same email (`user@gmail.com`), your app disallows this. It shows them an appropriate message like "Your account already exists via Google sign in. Please use that instead." ### Comparison to account linking Related to this problem is also the concept of account linking. The difference is that whilst deduplication prevents duplicate sign ups, account linking allows duplicate sign ups, but implicitly merges the duplicate accounts into one. ## Steps ### 1. Override the authentication recipes The approach to implementing account deduplication is to override the backend functions / APIs. This way, you can check if a user already exists and return an error to the frontend if the condition is true. ```tsx check=false reason="Requires surrounding application context" import ThirdParty from "supertokens-node/recipe/thirdparty"; import Passwordless from "supertokens-node/recipe/passwordless"; import supertokens from "supertokens-node"; let recipeList = [ Passwordless.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, createCodePOST: async function (input) { if ("email" in input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email, }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.createCodePOST!(input); } if ( existingUsers.find( (u) => u.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined, ) ) { // this means that the existing user is a passwordless login user. So we allow it return originalImplementation.createCodePOST!(input); } return { status: "GENERAL_ERROR", message: "Seems like you already have an account with another method. Please use that instead.", }; } // phone number based login, so we allow it. return originalImplementation.createCodePOST!(input); }, }; }, }, }), ThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, signInUp: async function (input) { let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { email: input.email, }); if (existingUsers.length === 0) { // this means this email is new so we allow sign up return originalImplementation.signInUp(input); } if ( existingUsers.find( (u) => u.loginMethods.find( (lM) => lM.hasSameThirdPartyInfoAs({ id: input.thirdPartyId, userId: input.thirdPartyUserId, }) && lM.recipeId === "thirdparty", ) !== undefined, ) ) { // this means we are trying to sign in with the same social login. So we allow it return originalImplementation.signInUp(input); } // this means that the email already exists with another social or passwordless login method, so we throw an error. throw new Error("Cannot sign up as email already exists"); }, }; }, apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: async function (input) { try { return await originalImplementation.signInUpPOST!(input); } catch (err: any) { if (err.message === "Cannot sign up as email already exists") { // this error was thrown from our function override above. // so we send a useful message to the user return { status: "GENERAL_ERROR", message: "Seems like you already have an account with another method. Please use that instead.", }; } throw err; } }, }; }, }, }), ]; ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/recipe/thirdparty" "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { _ = []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ Override: &plessmodels.OverrideStruct{ APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { originalCreateCodePOST := *originalImplementation.CreateCodePOST (*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { if email != nil { thirdPartyExistingUsers, err := thirdparty.GetUsersByEmail(tenantId, *email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } if len(thirdPartyExistingUsers) == 0 { // this means this email is either a new user or an existing passwordless user, so we allow return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } return plessmodels.CreateCodePOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Seems like you already have an account with another method. Please use that instead.", }, }, nil } // phone number based login, so we allow it. return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } return originalImplementation }, }, }), 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) { existingUsers, err := thirdparty.GetUsersByEmail(tenantId, email) if err != nil { return tpmodels.SignInUpResponse{}, err } emailPasswordExistingUser, err := passwordless.GetUserByEmail(tenantId, email) if err != nil { return tpmodels.SignInUpResponse{}, err } if emailPasswordExistingUser != nil { return tpmodels.SignInUpResponse{}, errors.New("Cannot sign up as email already exists") } if len(existingUsers) == 0 { // this means this email is new so we allow sign up return ogSignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext) } isSignIn := false for _, user := range existingUsers { if user.ThirdParty.ID == thirdPartyID && user.ThirdParty.UserID == thirdPartyUserID { // this means we are trying to sign in with the same social login. So we allow it isSignIn = true } } if isSignIn { return ogSignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext) } return tpmodels.SignInUpResponse{}, errors.New("Cannot sign up as email already exists") } return originalImplementation }, APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { originalSignInUpPOST := *originalImplementation.SignInUpPOST (*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext *map[string]interface{}) (tpmodels.SignInUpPOSTResponse, error) { resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext) if err != nil && err.Error() == "Cannot sign up as email already exists" { // this error was thrown from our function override above. // so we send a useful message to the user return tpmodels.SignInUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "Seems like you already have an account with another method. Please use that instead.", }, }, nil } return resp, err } return originalImplementation }, }, }), } } ``` ```python check=false reason="Partial configuration example" from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.recipe import passwordless, thirdparty from supertokens_python.recipe.passwordless.interfaces import ( APIInterface as PasswordlessAPIInterface, ) from supertokens_python.recipe.passwordless.interfaces import ( APIOptions as PasswordlessAPIOptions, ) from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface as ThirdPartyAPIInterface, ) from supertokens_python.recipe.thirdparty.interfaces import ( APIOptions as ThirdPartyAPIOptions, ) from supertokens_python.recipe.thirdparty.interfaces import ( RecipeInterface, ) from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo from supertokens_python.recipe.thirdparty.types import ( RawUserInfoFromProvider, ThirdPartyInfo, ) from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput def override_thirdparty_functions(original_implementation: RecipeInterface): 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], ): existing_users = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) if len(existing_users) == 0: # this means this email is new so we allow sign up 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, ) if any( any( lm.recipe_id == "thirdparty" and lm.has_same_third_party_info_as( ThirdPartyInfo(third_party_user_id, third_party_id) ) for lm in user.login_methods ) for user in existing_users ): # this means we are trying to sign in with the same social login. So we allow it 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, ) # this means that the email already exists with another social login method. # so we throw an error. raise Exception("Cannot sign up as email already exists") original_implementation.sign_in_up = sign_in_up return original_implementation def override_thirdparty_apis(original_implementation: ThirdPartyAPIInterface): original_sign_in_up_post = original_implementation.sign_in_up_post async def sign_in_up_post( provider: Provider, redirect_uri_info: Optional[RedirectUriInfo], oauth_tokens: Optional[Dict[str, Any]], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: ThirdPartyAPIOptions, user_context: Dict[str, Any], ): try: return await original_sign_in_up_post( provider, redirect_uri_info, oauth_tokens, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) except Exception as e: if str(e) == "Cannot sign up as email already exists": return GeneralErrorResponse( "Seems like you already have an account with another social login provider. Please use that instead." ) raise e original_implementation.sign_in_up_post = sign_in_up_post return original_implementation def override_passwordless_apis(original_implementation: PasswordlessAPIInterface): original_create_code_post = original_implementation.create_code_post async def create_code_post( email: Union[str, None], phone_number: Union[str, None], session: Optional[SessionContainer], should_try_linking_with_session_user: Union[bool, None], tenant_id: str, api_options: PasswordlessAPIOptions, user_context: Dict[str, Any], ): if email is not None: existing_users = await list_users_by_account_info( tenant_id, AccountInfoInput(email=email) ) if len(existing_users) == 0: # this means this email is new so we allow sign up return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) if any( user.login_methods and any( lm.recipe_id == "passwordless" and lm.has_same_email_as(email) for lm in user.login_methods ) for user in existing_users ): # this means that the existing user is a passwordless login user. So we allow it return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) return GeneralErrorResponse( "Seems like you already have an account with another method. Please use that instead." ) # phone number based login, so we allow it. return await original_create_code_post( email, phone_number, session, should_try_linking_with_session_user, tenant_id, api_options, user_context, ) original_implementation.create_code_post = create_code_post 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( apis=override_passwordless_apis, ), ), thirdparty.init( override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis, functions=override_thirdparty_functions ) ), ], ) ``` In the above code snippet, override the `signInUpPOST` (third party recipe) and the `createCodePOST` (passwordless recipe) API as well as the `signInUp` recipe function. The frontend calls the `signInUpPOST` API after the user returns to the app from the third-party provider's login page. The API then exchanges the auth code with the provider and calls the `signInUp` function with the user's email and third-party info. The system calls the `createCodePOST` API when the user enters their email, or phone number during passwordless login. This API generates the passwordless OTP / link and sends it to the user's email / phone. The `signInUp` recipe function is overridden to: - Get all ThirdParty or Passwordless users that have the same input email. - If no users exist with that email, it means that this is a new email and the system calls the `originalImplementation` function to create a new user. - If instead, a user exists, but has the same `thirdPartyId` and `thirdPartyUserId`, implying that this is a sign in (for example a user who had signed up with Google is signing in with Google), the operation proceeds by calling the `originalImplementation` function. - If neither of the conditions above match, it means that the user is trying to sign up with a third party provider whilst they already have an account with another provider or via passwordless login. Here, the system throws an error with some custom message. Finally, the `signInUpPOST` API is overridden to catch that custom error and return a [general error status](/references/backend-sdks/api-overrides#error-management) to the frontend with a message displayed to the user in the sign in form. The `createCodePOST` API is also overridden to perform similar checks: - If the input is phone number based, then the system calls the `originalImplementation` function allowing sign up or sign in. This is OK since social login is always email based, there is no scope of duplication. - Otherwise, get all ThirdParty or Passwordless users that have the same input email. - If no users exist with that email, it means that this is a new email and the system calls the `originalImplementation` function to create a new user. - Else, check if the existing user is not a Third Party login user, implying that it's a Passwordless login user. Here, the `originalImplementation` function is also called to allow the user to sign in. - If neither of the conditions above match, it means that the user is trying to sign up with passwordless login whilst they already have an account with a third party provider. Here, the system returns an appropriate message to display on the frontend. :::info[Multi Tenancy] For a multi tenant setup, the customisations above ensure that multiple accounts with the same email don't exist within a single tenant. To ensure no duplication across all tenants, when fetching the list of existing users, loop through all tenants in the app. You can fetch them by using the `listAllTenants` function of the multi tenancy recipe. ::: --- # Allow users to update their data Source: https://supertokens.com/docs/post-authentication/user-management/allow-users-to-update-their-data ## Overview This guide shows you how to implement a feature that allows users to update their email or password. ## Before you start :::warning[SuperTokens does not provide the UI for this type of use case.] You need to create the UI and set up a route on your backend to have this functionality. ::: --- ## Email update This section has instructions on how to create a route, on your backend, to update a user's email. Calling this route checks if the new email is valid and not already in use and proceeds to update the user's account with the new email. ### Without email verification In this flow, a user can update their account's email without verifying the new email ID. #### 1. Create the email update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. - To learn more about how to use the session verification middleware for other frameworks, click [this link](/additional-verification/session-verification/protect-api-routes) ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { // TODO: see next steps }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r) }) } func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // TODO: see next steps } ``` ```python check=false reason="Requires surrounding framework application context" # the following example uses flask from flask import Flask from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route('/change-email', methods=['POST']) @verify_session() def change_email(): pass # TODO: see next steps ``` #### 2. Update the account - Validate the input email. - Update the account with the input email. ```tsx // the following example uses express import Passwordless from "supertokens-node/recipe/passwordless"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { let session = req.session!; let email = req.body.email; // Validate the input email if (!isValidEmail(email)) { // TODO: handle invalid email error return; } // Update the email let resp = await Passwordless.updateUser({ recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO: send successfully updated email response return; } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO: handle error that email exists with another account. return; } if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { // This is possible if you have enabled account linking. // See our docs for account linking to know more about this. // TODO: tell the user to contact support. } throw new Error("Should never come here"); }); function isValidEmail(email: string) { let regexp = new RegExp( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ); return regexp.test(email); } ``` ```go import ( "encoding/json" "log" "net/http" "regexp" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/session" ) type RequestBody struct { Email string } // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r) }) } func changeEmailAPI(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) var requestBody RequestBody err := json.NewDecoder(r.Body).Decode(&requestBody) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } // validate the input email if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } // update the email userId := sessionContainer.GetUserID() updateResponse, err := passwordless.UpdateUser(userId, &requestBody.Email, nil) if err != nil { // TODO: handle error } if updateResponse.OK != nil { // TODO: send successfully updated email response return } if updateResponse.EmailAlreadyExistsError != nil { // TODO: handle error that email exists with another account return } log.Fatal("should not reach here") } func isValidEmail(email string) bool { emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(email)) if err != nil { return false } return emailCheck } ``` ```python check=false reason="Requires surrounding framework application context" from re import fullmatch from flask import Flask, g, request from supertokens_python.recipe.passwordless.interfaces import ( EmailChangeNotAllowedError, UpdateUserEmailAlreadyExistsError, UpdateUserOkResult, ) from supertokens_python.recipe.passwordless.syncio import update_user from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route("/change-email", methods=["POST"]) @verify_session() def change_email(): session: SessionContainer = g.supertokens request_body = request.get_json() email = str(request_body["email"]) if request_body is None: # TODO: handle invalid body error return # validate the input email if not is_valid_email(email): # TODO: handle invalid email error return # update the users email update_response = update_user(session.get_recipe_user_id(), email=email) if isinstance(update_response, UpdateUserOkResult): # TODO send successful email update response return if isinstance(update_response, UpdateUserEmailAlreadyExistsError): # TODO handle error, email already exists return if isinstance(update_response, EmailChangeNotAllowedError): # This is possible if you have enabled account linking. # See our docs for account linking to know more about this. # TODO: tell the user to contact support. return raise Exception("Should never reach here") def is_valid_email(value: str) -> bool: return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value, ) is not None ) ``` ### With email verification In this flow, the user's account updates once they have verified the new email. #### 1. Create the email update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. - To learn more about how to use the session verification middleware for other frameworks, click [this link](/additional-verification/session-verification/protect-api-routes) ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { // TODO: see next steps }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r) }) } func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // TODO: see next steps } ``` ```python check=false reason="Requires surrounding framework application context" # the following example uses flask from flask import Flask from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route('/change-email', methods=['POST']) @verify_session() def change_password(): pass # TODO: see next steps ``` #### 2. Initiate the email verification flow - Validate the input email - Check if the input email associates with an account. - Check if the input email is already verified. - If the email is **NOT** verified, create and send the verification email. - If the email has been verified, update the account with the new email. ```tsx import Passwordless from "supertokens-node/recipe/passwordless"; import EmailVerification from "supertokens-node/recipe/emailverification"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; import supertokens from "supertokens-node"; import { isEmailChangeAllowed } from "supertokens-node/recipe/accountlinking"; let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { let session = req.session!; let email = req.body.email; // validate the input email if (!isValidEmail(email)) { return res.status(400).send("Email is invalid"); } // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); if (!isVerified) { if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, false))) { // this can come here if you have enabled the account linking feature, and // if there is a security risk in changing this user's email. return res.status(400).send("Email change not allowed. Please contact support"); } // Before sending a verification email, we check if the email is already // being used by another user. If it is, we throw an error. let user = (await supertokens.getUser(session.getUserId()))!; for (let i = 0; i < user?.tenantIds.length; i++) { // Since once user can be shared across many tenants, we need to check if // the email already exists in any of the tenants. let usersWithEmail = await supertokens.listUsersByAccountInfo(user?.tenantIds[i], { email, }); for (let y = 0; y < usersWithEmail.length; y++) { if (usersWithEmail[y].id !== session.getUserId()) { // TODO handle error, email already exists with another user. return; } } } // Now we create and send the email verification link to the user for the new email. await EmailVerification.sendEmailVerificationEmail( session.getTenantId(), session.getUserId(), session.getRecipeUserId(), email, ); // TODO send successful response that email verification email sent. return; } // Since the email is verified, we try and do an update let resp = await Passwordless.updateUser({ recipeUserId: session.getRecipeUserId(), email: email, }); if (resp.status === "OK") { // TODO send successful response that email updated. return; } if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { // TODO handle error, email already exists with another user. return; } throw new Error("Should never come here"); }); function isValidEmail(email: string) { let regexp = new RegExp( /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, ); return regexp.test(email); } ``` ```go import ( "encoding/json" "log" "net/http" "regexp" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/passwordless" "github.com/supertokens/supertokens-golang/recipe/session" ) type RequestBody struct { Email string } // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r) }) } func changeEmailAPI(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) var requestBody RequestBody err := json.NewDecoder(r.Body).Decode(&requestBody) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } // validate the input email if !isValidEmail(requestBody.Email) { // TODO: handle invalid email error return } // Check if the new email is already associated with another email-password user. // If it is, then we throw an error. If it's already associated with this user, // then we return a success response with an appropriate message. userId := sessionContainer.GetUserID() // Then, we check if the email is verified for this user ID or not. // It is important to understand that SuperTokens stores email verification // status based on the user ID AND the email, and not just the email. isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.Email) if err != nil { // TODO: handle error } if !isVerified { // Now we create and send the email verification link to the user for the new email. _, err := emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.Email) if err != nil { // TODO: handle error } return } // Since the email is verified, we try and do an update updateResponse, err := passwordless.UpdateUser(userId, &requestBody.Email, nil) if err != nil { // TODO: handle error } if updateResponse.OK != nil { // TODO: send successfully updated email response return } if updateResponse.EmailAlreadyExistsError != nil { // TODO: handle error, email already exists for another account return } log.Fatal("should not reach here") } func isValidEmail(email string) bool { emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(email)) if err != nil { return false } return emailCheck } ``` ```python check=false reason="Requires surrounding framework application context" from re import fullmatch from flask import Flask, g, request from supertokens_python.recipe.accountlinking.syncio import is_email_change_allowed from supertokens_python.recipe.emailverification.syncio import ( is_email_verified, send_email_verification_email, ) from supertokens_python.recipe.passwordless.interfaces import ( UpdateUserEmailAlreadyExistsError, UpdateUserOkResult, ) from supertokens_python.recipe.passwordless.syncio import update_user from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.syncio import get_user, list_users_by_account_info from supertokens_python.types.base import AccountInfoInput app = Flask(__name__) @app.route("/change-email", methods=["POST"]) @verify_session() def change_email(): session: SessionContainer = g.supertokens request_body = request.get_json() if request_body is None: # TODO: handle invalid body error return # validate the input email if not is_valid_email(request_body["email"]): # TODO: handle invalid email error return user_id = session.get_user_id() # Then, we check if the email is verified for this user ID or not. # It is important to understand that SuperTokens stores email verification # status based on the user ID AND the email, and not just the email. is_verified = is_email_verified(session.get_recipe_user_id(), request_body["email"]) if not is_verified: if not is_email_change_allowed( session.get_recipe_user_id(), request_body["email"], False ): # Email change is not allowed, send a 400 error return {"error": "Email change not allowed"}, 400 # Before sending a verification email, we check if the email is already # being used by another user. If it is, we throw an error. user = get_user(user_id) if user is not None: for tenant_id in user.tenant_ids: users_with_same_email = list_users_by_account_info( tenant_id, AccountInfoInput(email=request_body["email"]) ) for curr_user in users_with_same_email: # Since one user can be shared across many tenants, we need to check if # the email already exists in any of the tenants that belongs to this user. if curr_user.id != user_id: # TODO handle error, email already exists with another user. return # Create and send the email verification link to the user for the new email. send_email_verification_email( session.get_tenant_id(), user_id, session.get_recipe_user_id(), request_body["email"], ) # TODO send successful email verification response return # update the users email update_response = update_user( session.get_recipe_user_id(), email=request_body["email"] ) if isinstance(update_response, UpdateUserOkResult): # TODO send successful email update response return if isinstance(update_response, UpdateUserEmailAlreadyExistsError): # TODO handle error, email already exists return raise Exception("Should never reach here") def is_valid_email(value: str) -> bool: return ( fullmatch( r'^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$', value, ) is not None ) ``` :::info[Multi Tenancy] - Notice that the process loops through all the tenants that this user belongs to check that for each of the tenants, there is no other user with the new email. If this step is not done, then calling `updateEmailOrPassword` would fail because the email is already used by another user in one of the tenants that this user belongs to. In that case, the verification process should not proceed either. - The `tenantId` of the current session is also passed when calling the `sendEmailVerificationEmail` function, ensuring that the link generated opens the tenant's UI that the user interacts with. - When calling `updateEmailOrPassword`, it returns `EMAIL_ALREADY_EXISTS_ERROR` if the new email exists in any of the tenants that the user ID is a part of. ::: #### 3. Update the account on successful email verification - Update the accounts email on successful email verification. ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Passwordless.init({ flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", contactMethod: "EMAIL_OR_PHONE", }), EmailVerification.init({ mode: "REQUIRED", override: { apis: (oI) => { return { ...oI, verifyEmailPOST: async function (input) { let response = await oI.verifyEmailPOST!(input); if (response.status === "OK") { // This will update the email of the user to the one // that was just marked as verified by the token. await Passwordless.updateUser({ recipeUserId: response.user.recipeUserId, email: response.user.email, }); } return response; }, }; }, }, }), Session.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "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/sessmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { err := supertokens.Init(supertokens.TypeInput{ AppInfo: supertokens.AppInfo{ AppName: "...", APIDomain: "...", WebsiteDomain: "...", }, RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE_AND_MAGIC_LINK", ContactMethodEmailOrPhone: plessmodels.ContactMethodEmailOrPhoneConfig{ Enabled: true, }, }), emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, Override: &evmodels.OverrideStruct{ APIs: func(originalImplementation evmodels.APIInterface) evmodels.APIInterface { originalVerifyEmailPOST := *originalImplementation.VerifyEmailPOST (*originalImplementation.VerifyEmailPOST) = func(token string, sessionContainer sessmodels.SessionContainer, tenantId string, options evmodels.APIOptions, userContext supertokens.UserContext) (evmodels.VerifyEmailPOSTResponse, error) { response, err := originalVerifyEmailPOST(token, sessionContainer, tenantId, options, userContext) if response.OK != nil { // This will update the email of the user to the one // that was just marked as verified by the token. _, err := passwordless.UpdateUser(response.OK.User.ID, &response.OK.User.Email, nil) if err != nil { // TODO: Handle error } } return response, err } return originalImplementation }, }, }), session.Init(nil), }, }) if err != nil { panic(err.Error()) } } ``` ```python from typing import Any, Dict, Optional from supertokens_python import ( InputAppInfo, SupertokensConfig, init, ) from supertokens_python.recipe import emailverification, passwordless from supertokens_python.recipe.emailverification.interfaces import ( APIInterface, APIOptions, EmailVerifyPostOkResult, ) from supertokens_python.recipe.passwordless import ContactEmailOrPhoneConfig from supertokens_python.recipe.passwordless.asyncio import update_user from supertokens_python.recipe.session.interfaces import SessionContainer def override_email_verification_apis(original_implementation: APIInterface): original_email_verification_verify_email_post = ( original_implementation.email_verify_post ) async def email_verify_post( token: str, session: Optional[SessionContainer], tenant_id: str, api_options: APIOptions, user_context: Dict[str, Any], ): verification_response = await original_email_verification_verify_email_post( token, session, tenant_id, api_options, user_context ) if isinstance(verification_response, EmailVerifyPostOkResult): await update_user( verification_response.user.recipe_user_id, verification_response.user.email, ) return verification_response original_implementation.email_verify_post = email_verify_post return original_implementation init( supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="flask", recipe_list=[ passwordless.init( flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", contact_config=ContactEmailOrPhoneConfig(), ), emailverification.init( "REQUIRED", override=emailverification.InputOverrideConfig( apis=override_email_verification_apis ), ), ], ) ``` --- ## Password update This section has instructions on how to create a route, on your backend, that can update a user's password. Calling this route checks if the old password is valid and updates the user's profile with the new password. ### 1. Create the password update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. - To learn more about how to use the session verification middleware for other frameworks, click [this link](/additional-verification/session-verification/protect-api-routes#using-verify-session) ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { // TODO: see next steps }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changePasswordAPI).ServeHTTP(rw, r) }) } func changePasswordAPI(w http.ResponseWriter, r *http.Request) { // TODO: see next steps } ``` ```python check=false reason="Requires surrounding framework application context" # the following example uses flask from flask import Flask from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) @app.route('/change-password', methods=['POST']) @verify_session() def change_password(): pass # TODO: see next steps ``` ### 2. Update the user password - The `session` object can be used to retrieve the logged-in user's `userId`. - Use the recipe's sign in function and check if the old password is valid - Update the user's password. ```tsx // the following example uses express import EmailPassword from "supertokens-node/recipe/emailpassword"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; import supertokens from "supertokens-node"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { // get the supertokens session object from the req let session = req.session; // retrieve the old password from the request body let oldPassword = req.body.oldPassword; // retrieve the new password from the request body let updatedPassword = req.body.newPassword; // get the signed in user's email from the getUserById function let userInfo = await supertokens.getUser(session!.getUserId()); if (userInfo === undefined) { throw new Error("Should never come here"); } let loginMethod = userInfo.loginMethods.find( (lM) => lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword", ); if (loginMethod === undefined) { throw new Error("Should never come here"); } const email = loginMethod.email!; // call signin to check that input password is correct let isPasswordValid = await EmailPassword.verifyCredentials(session!.getTenantId(), email, oldPassword); if (isPasswordValid.status !== "OK") { // TODO: handle incorrect password error return; } // update the user's password using updateEmailOrPassword let response = await EmailPassword.updateEmailOrPassword({ recipeUserId: session!.getRecipeUserId(), password: updatedPassword, tenantIdForPasswordPolicy: session!.getTenantId(), }); if (response.status === "PASSWORD_POLICY_VIOLATED_ERROR") { // TODO: handle incorrect password error return; } // TODO: send successful password update response }); ``` ```go import ( "encoding/json" "net/http" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changePasswordAPI).ServeHTTP(rw, r) }) } type RequestBody struct { OldPassword string NewPassword string } func changePasswordAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) // retrieve the old password from the request body var requestBody RequestBody err := json.NewDecoder(r.Body).Decode(&requestBody) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // get the userId from the session userID := sessionContainer.GetUserID() // get the signed in user's email from the getUserById function userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error return } // call signin to check that the input is correct isPasswordValid, err := emailpassword.SignIn(sessionContainer.GetTenantId(), userInfo.Email, requestBody.OldPassword) if err != nil { // TODO: Handle error return } if isPasswordValid.WrongCredentialsError != nil { // TODO: Handle error return } tenantId := sessionContainer.GetTenantId() updateResponse, err := emailpassword.UpdateEmailOrPassword(userID, &userInfo.Email, &requestBody.NewPassword, nil, &tenantId, nil) if err != nil { // TODO: Handle error return } if updateResponse.PasswordPolicyViolatedError != nil { // This error is returned if the new password doesn't match the defined password policy // TODO: Handle error return } // TODO: send successful password update response } ``` ```python check=false reason="Requires surrounding framework application context" from flask import g, request from supertokens_python.recipe.emailpassword.interfaces import ( PasswordPolicyViolationError, WrongCredentialsError, ) from supertokens_python.recipe.emailpassword.syncio import ( update_email_or_password, verify_credentials, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.syncio import get_user @app.route("/change-password", methods=["POST"]) @verify_session() def change_password(): session: SessionContainer = g.supertokens # get the signed in user's email from the getUserById function users_info = get_user(session.get_user_id()) if users_info is None: raise Exception("TODO: Handle error. User not found.") # Find the login method for the current user login_method = next( ( lm for lm in users_info.login_methods if lm.recipe_user_id.get_as_string() == session.get_recipe_user_id().get_as_string() and lm.recipe_id == "emailpassword" ), None, ) if login_method is None: raise Exception("Should never come here") email = login_method.email if email is None: raise Exception("Email not found for the user") request_body = request.get_json() if request_body is None: # TODO: handle invalid body error return # call signin to check that the input password is correct isPasswordValid = verify_credentials( "public", email, password=request_body["oldPassword"] ) if isinstance(isPasswordValid, WrongCredentialsError): # TODO: handle incorrect password error return # update the users password update_response = update_email_or_password( session.get_recipe_user_id(), password=request_body["newPassword"], tenant_id_for_password_policy=session.get_tenant_id(), ) if isinstance(update_response, PasswordPolicyViolationError): # TODO: handle password policy violation error return # TODO: send successful password update response ``` :::info[Multi Tenancy] Notice that the `tenantId` passes as an argument to the `signIn` and the `updateEmailOrPassword` functions. This ensures that the current tenant has email password enabled, and ensures that the user's new password matches the password policy defined for their tenant (if different password policies exist for different tenants). If this user shares access across multiple tenants, their password changes for all tenants. ::: ### 3. Revoke all sessions associated with the user (optional) - Revoking all sessions associated with the user forces them to re-authenticate with their new password. ```tsx // the following example uses express import Session from "supertokens-node/recipe/session"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { let userId = req.session!.getUserId(); /** * * ... * see previous step * ... * * */ // revoke all sessions for the user await Session.revokeAllSessionsForUser(userId); // revoke the current user's session, this removes the auth cookies, logging out the user on the frontend. await req.session!.revokeSession(); // TODO: send successful password update response }); ``` ```go import ( "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, changePasswordAPI).ServeHTTP(rw, r) }) } type ResponseBody struct { OldPassword string NewPassword string } func changePasswordAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() /** * * ... * see previous step * ... * * */ // revoke all sessions for the user _, err := session.RevokeAllSessionsForUser(userID, nil) if err != nil { // TODO: Handle error } // revoke the user's current session, this removes the auth cookies, logging out the user on the frontend err = sessionContainer.RevokeSession() if err != nil { // TODO: Handle error } // TODO: send successful password update response } ``` ```python check=false reason="Requires surrounding framework application context" from typing import cast from flask import Flask from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session.syncio import revoke_all_sessions_for_user app = Flask(__name__) @app.route('/change-password', methods=['POST']) @verify_session() def change_password(): session: SessionContainer = cast(SessionContainer, g.supertokens) # get the userId from the session object user_id = session.get_user_id() # TODO: see previous step... # revoke all sessions for the user revoke_all_sessions_for_user(user_id) # revoke the user's current session, this removes the auth cookies, logging out the user on the frontend session.sync_revoke_session() # TODO: send successful password update response ``` --- # Common actions Source: https://supertokens.com/docs/post-authentication/user-management/common-actions ## Overview **SuperTokens** exposes a set of functions and APIs that you can use to have manual control over your users. Actions like fetching users or deleting them are available through different SDK calls. --- ## Get user ### By email ```tsx import supertokens from "supertokens-node"; async function getUserInfo() { let usersInfo = await supertokens.listUsersByAccountInfo("public", { email: "test@example.com", }); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/emailpassword" ) func main() { // Note that usersInfo has type User[] // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki userInfo, err := emailpassword.GetUserByEmail("public", "test@example.com") if err != nil { // TODO: Handle error return } fmt.Println(userInfo) //... } ``` ```python from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput async def some_func(): # Note that users_info has type List[User] user_info = await list_users_by_account_info("public", AccountInfoInput(email="test@example.com")) print(user_info) # # user_info contains the following info: # - emails # - id # - timeJoined # - tenantIds # - phone numbers # - third party login info # - all the login methods associated with this user. # - information about if the user's email is verified or not. # ``` ```python from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput def some_func(): # Note that users_info has type List[User] user_info = list_users_by_account_info("public", AccountInfoInput(email="test@example.com")) print(user_info) # # user_info contains the following info: # - emails # - id # - timeJoined # - tenantIds # - phone numbers # - third party login info # - all the login methods associated with this user. # - information about if the user's email is verified or not. # ``` :::info[Multi Tenancy] Notice that the first argument of the above function is `"public"`. This is the default `tenantId`, which means that SuperTokens returns information about the user whose email is `"test@example.com"` in the `"public"` tenant. If you are using the multi-tenancy feature, you can pass in a different `tenantId` to get information about a user in a different tenant. ::: ### By phone number ```tsx import supertokens from "supertokens-node"; async function handler() { let usersInfo = await supertokens.listUsersByAccountInfo("public", { phoneNumber: "+1234567890", }); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/passwordless" ) func main() { tenantId := "public" userInfo, err := passwordless.GetUserByPhoneNumber(tenantId, "+1234567890") if err != nil { // TODO: Handle error return } fmt.Println(userInfo) //... } ``` ```python from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput async def some_func(): _ = await list_users_by_account_info( "public", AccountInfoInput(phone_number="+1234567890") ) ``` ```python from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput def some_func(): _ = list_users_by_account_info( "public", AccountInfoInput(phone_number="+1234567890") ) ``` :::info[Multi Tenancy] Notice that the `"public"` `tenantId` appears in the function call above. This is the default `tenantId` and returns the user with the given phone number that belongs to the `public` tenant. You can provide a different `tenantId` if required. ::: ### By User ID ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import Hapi from "@hapi/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import supertokens from "supertokens-node"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/get-user-info", method: "get", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import supertokens from "supertokens-node"; const fastify = Fastify(); fastify.post( "/like-comment", { preHandler: verifySession(), }, async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import supertokens from "supertokens-node"; async function getUserInfo(awsEvent: SessionEvent) { let userId = awsEvent.session!.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } exports.handler = verifySession(getUserInfo); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import supertokens from "supertokens-node"; let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; import supertokens from "supertokens-node"; class GetUserInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} @get("/get-user-info") @intercept(verifySession()) @response(200) async handler() { let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; export default async function likeComment(req: SessionRequest, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); let userId = req.session!.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ } ``` ```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; import { AuthGuard } from "./auth/auth.guard"; import { Session } from "./auth/session.decorator"; import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; @Controller() export class ExampleController { @Post("example") @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample( @Request() req: SessionRequest, @Session() session: Session, @Response({ passthrough: true }) res: Response, ): Promise { let userId = session.getUserId(); let userInfo = await supertokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ return true; } } ``` ```go import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/emailpassword" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(nil, getUserInfoAPI).ServeHTTP(rw, r) }) } func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error return } fmt.Println(userInfo) } ``` ```go import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/emailpassword" ) func main() { router := gin.New() router.GET("/getuserinfo", verifySession(nil), getUserInfoAPI) } 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 getUserInfoAPI(c *gin.Context) { sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error return } fmt.Println(userInfo) //... } ``` ```go import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/emailpassword" ) func main() { r := chi.NewRouter() r.Get("/getuserinfo", session.VerifySession(nil, getUserInfoAPI)) } func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error return } fmt.Println(userInfo) } ``` ```go import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/emailpassword" ) func main() { router := mux.NewRouter() router.HandleFunc("/getuserinfo", session.VerifySession(nil, getUserInfoAPI)).Methods(http.MethodGet) } func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error return } fmt.Println(userInfo) } ``` ```python from fastapi import Depends, FastAPI from supertokens_python.asyncio import get_user from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session app = FastAPI() @app.post('/get_user_info_api') async def get_user_info_api(session: SessionContainer = Depends(verify_session())): user_id = session.get_user_id() # You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki _ = await get_user(user_id) ``` ```python check=false reason="Requires surrounding framework application context" from flask import Flask, g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.syncio import get_user app = Flask(__name__) @app.route('/get_user_info', methods=['GET']) @verify_session() def get_user_info_api(): session: SessionContainer = g.supertokens user_id = session.get_user_id() # You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki _ = get_user(user_id) ``` ```python check=false reason="Requires surrounding async application context" from typing import cast from django.http import HttpRequest from supertokens_python.asyncio import get_user from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def get_user_info_api(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() # You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki _ = await get_user(user_id) ``` ```tsx check=false reason="Requires surrounding framework application context" 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 }); } const userId = session!.getUserId(); let userInfo = await SuperTokens.getUser(userId); /** * * userInfo contains the following info: * - emails * - id * - timeJoined * - tenantIds * - phone numbers * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. * */ return NextResponse.json({}); }); } ``` :::info[Tip] The authentication session also contains the user ID and the session payload. You can access it on [both the backend and the frontend](/additional-verification/session-verification/claim-validation#using-the-access-token-payload). ::: #### Using the user metadata recipe Checkout the [user metadata recipe docs](/post-authentication/user-management/user-metadata) which shows you how to save and fetch any JSON object against the user's ID. You can use this to save information like the user's name (`first_name` and `last_name`) or any other field associated with the user. --- ## Delete user ```tsx import { deleteUser } from "supertokens-node"; async function deleteUserForId() { let userId = "..."; // get the user ID await deleteUser(userId); // this will succeed even if the userId didn't exist. } ``` ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { userId := "..." // get the user ID somehow... supertokens.DeleteUser(userId) // this will succeed even if the userId didn't exist. } ``` ```python from supertokens_python.asyncio import delete_user async def do_delete(): user_id = "..." # get the user ID somehow... await delete_user(user_id) # this will succeed even if the userId didn't exist. ``` ```python from supertokens_python.syncio import delete_user user_id = "..." # get the user ID somehow... delete_user(user_id) # this will succeed even if the userId didn't exist. ``` :::warning[- Calling this function permanently removes all information associated with this user, including their sessions.] - Deletion removes the user's database sessions, but it does not immediately invalidate an already-issued stateless access token. Without `checkDatabase: true`, that token can continue to pass session verification until it expires. Enable an authoritative database check on every endpoint that must reject the deleted user immediately. After the access token expires, refresh fails because the database session no longer exists. ::: --- ## List users ### Newest first ```tsx import { getUsersNewestFirst } from "supertokens-node"; async function getUsers() { // get the latest 100 users let usersResponse = await getUsersNewestFirst({ tenantId: "public", }); let users = usersResponse.users; let nextPaginationToken = usersResponse.nextPaginationToken; // get the next 200 users usersResponse = await getUsersNewestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, }); users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; // get for specific recipes usersResponse = await getUsersNewestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, // only get for those users who signed up with includeRecipeIds: [""], }); users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; } ``` ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { // get the latest 100 users result, err := supertokens.GetUsersNewestFirst("", nil, nil, nil, nil) if err != nil { // TODO: Handle error return } // get the next 200 users limit := 200 result, err = supertokens.GetUsersNewestFirst("", result.NextPaginationToken, &limit, nil, nil) if err != nil { // TODO: Handle error return } // get for specific recipes includeRecipeIds := []string{""} result, err = supertokens.GetUsersNewestFirst("", result.NextPaginationToken, &limit, &includeRecipeIds, nil) if err != nil { // TODO: Handle error return } } ``` ```python from supertokens_python.asyncio import get_users_newest_first async def some_func(): # get the latest 100 users users_response = await get_users_newest_first("public") # get the next 200 users users_response = await get_users_newest_first("public", 200, users_response.next_pagination_token) # get for specific recipes users_response = await get_users_newest_first( "public", 200, users_response.next_pagination_token, # only get for those users who signed up with [""] ) ``` ```python from supertokens_python.syncio import get_users_newest_first # get the latest 100 users users_response = get_users_newest_first("public") # get the next 200 users users_response = get_users_newest_first("public", 200, users_response.next_pagination_token) # get for specific recipes users_response = get_users_newest_first( "public", 200, users_response.next_pagination_token, # only get for those users who signed up with [""] ) ``` ### Oldest first ```ts import { getUsersOldestFirst } from "supertokens-node"; async function getUsers() { // get the latest 100 users let usersResponse = await getUsersOldestFirst({ tenantId: "public", }); let users = usersResponse.users; let nextPaginationToken = usersResponse.nextPaginationToken; // get the next oldest 200 users usersResponse = await getUsersOldestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, }); users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; // get for specific recipes usersResponse = await getUsersOldestFirst({ tenantId: "public", limit: 200, paginationToken: nextPaginationToken, // only get for those users who signed up with includeRecipeIds: [""], }); users = usersResponse.users; nextPaginationToken = usersResponse.nextPaginationToken; } ``` ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { // get the oldest 100 users result, err := supertokens.GetUsersOldestFirst("", nil, nil, nil, nil) if err != nil { // TODO: Handle error return } // get the next oldest 200 users limit := 200 result, err = supertokens.GetUsersOldestFirst("", result.NextPaginationToken, &limit, nil, nil) if err != nil { // TODO: Handle error return } // get for specific recipes includeRecipeIds := []string{""} result, err = supertokens.GetUsersOldestFirst("", result.NextPaginationToken, &limit, &includeRecipeIds, nil) if err != nil { // TODO: Handle error return } } ``` ```python from supertokens_python.asyncio import get_users_oldest_first async def some_func(): # get the latest 100 users users_response = await get_users_oldest_first("public") # get the next 200 users users_response = await get_users_oldest_first("public", 200, users_response.next_pagination_token) # get for specific recipes users_response = await get_users_oldest_first( "public", 200, users_response.next_pagination_token, # only get for those users who signed up with [""] ) ``` ```python from supertokens_python.syncio import get_users_oldest_first # get the latest 100 users users_response = get_users_oldest_first("public") # get the next 200 users users_response = get_users_oldest_first("public", 200, users_response.next_pagination_token) # get for specific recipes users_response = get_users_oldest_first( "public", 200, users_response.next_pagination_token, # only get for those users who signed up with [""] ) ``` - If the `nextPaginationToken` is `undefined`, then there are no more users to loop through. - If there are no users in your app, then `nextPaginationToken` is `undefined` and `users` is an empty array - Each element in the `users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). - If the `result.NextPaginationToken` is `nil`, then there are no more users to loop through. - If there are no users in your app, then `result.NextPaginationToken` is `nil` and `result.Users` is an empty array - Each element in the `result.Users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). :::info[Multi Tenancy] Notice that the `tenantId` appears as `"public"`. This means that the functions above loop through the users of the `public` `tenantId`. If you want to loop through other tenant IDs, you can pass in the tenant ID string to the function call. This also implies that there is no way to loop through all users across all tenants in one go. If you want to do this, you must loop through each tenant one by one. ::: --- ## Count users ```ts import { getUserCount } from "supertokens-node"; async function getCount() { let count = await getUserCount(); } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { tenantId := "" count, err := supertokens.GetUserCount(nil, &tenantId) if err != nil { // TODO: Handle error return } fmt.Println(count) } ``` ```python from supertokens_python.asyncio import get_user_count async def some_func(): user_count = await get_user_count() print(user_count) # TODO.. ``` ```python from supertokens_python.syncio import get_user_count user_count = get_user_count() ``` :::info[Multi Tenancy] By default, the getUserCount function returns the number of users across all tenants. If you want to get the number of users for a specific tenant, you can pass in the tenant ID string to the function call. ::: --- # Introduction Source: https://supertokens.com/docs/post-authentication/user-management/introduction ## Overview Users are at the core of every authentication flow. Each **SuperTokens** recipe interacts with users in a way or another. They get created either from the recipe sign up flow, through the migration API or because of direct SDK calls. ## Prerequisites This section assumes that you are familiar with basic **SuperTokens** concepts like **recipes**, **sessions** and authentication methods. If not, please refer to the [quickstart guide](/quickstart) first. ## Customization Besides the common actions which you can use to manage users, you can also integrate different features which allow you extend the user functionality. See how to create, update and delete users. Learn how to include additional data to each user. Implement a series of steps that enable users to change their credentials. Prevent the creation of multiple accounts with the same email ID. --- # Progressive profiling Source: https://supertokens.com/docs/post-authentication/user-management/progressive-profiling ## Overview This tutorial shows you how to add progressive profiling functionality to your **SuperTokens** authentication flows. The guide makes use of the `plugins` functionality which provides a step-by-step user profile collection system with customizable forms and field types. This way you can gather user information gradually, during the sign up process. ## How it works The plugin breaks profile collection into manageable sections. It automatically enforces profile completion through session claims, redirecting users to the setup page until their profile is complete. The system provides dynamic validation, progress tracking, and flexible storage options for collected data. ## Before you start The progressive profiling plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. The implementation is in early stages and APIs might change. For more information on how plugins work refer to the [references page](/references/plugins/introduction). You need to start from a working **SuperTokens** setup. If you haven't done that already, please refer to the [Quickstart Guides](/quickstart). ## Steps ### 1. Initialize the backend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/progressive-profiling-nodejs ``` #### 1.2 Update your backend SDK configuration ```typescript import SuperTokens from "supertokens-node"; import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profiling-nodejs"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes (Session recipe is required) ], experimental: { plugins: [ ProgressiveProfilingPlugin.init({ sections: [ { id: "basic-info", label: "Basic Information", description: "Tell us about yourself", fields: [ { id: "firstName", label: "First Name", type: "string", required: true, placeholder: "Enter your first name", }, { id: "lastName", label: "Last Name", type: "string", required: true, placeholder: "Enter your last name", }, { id: "company", label: "Company", type: "string", required: false, placeholder: "Enter your company name", }, ], }, { id: "preferences", label: "Preferences", description: "Customize your experience", fields: [ { id: "notifications", label: "Email Notifications", type: "boolean", required: false, defaultValue: true, }, { id: "theme", label: "Preferred Theme", type: "select", required: false, options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, { value: "auto", label: "Auto" }, ], defaultValue: "auto", }, ], }, ], }), ], }, }); ``` ##### Supported field types The plugin supports the following field types: | Field Type | Description | Example Use Case | |------------|-------------|------------------| | `string` | Single-line text input | Name, title, company | | `text` | Multi-line text area | Bio, description, comments | | `number` | Numeric input | Age, salary, years of experience | | `boolean` | Checkbox input | Newsletter subscription, terms acceptance | | `toggle` | Toggle switch | Feature preferences, notifications | | `email` | Email input with validation | Contact email, secondary email | | `phone` | Phone number input | Contact number, emergency contact | | `date` | Date picker | Birth date, start date | | `select` | Dropdown selection | Country, department, role | | `multiselect` | Multiple selection dropdown | Skills, interests, languages | | `password` | Password input | API keys, secure tokens | | `url` | URL input with validation | Website, social profiles | | `image-url` | Image URL input with preview | Profile picture, logo | :::info The plugin provides built-in validation for form fields: - **Required Fields**: Automatically validates that required fields are not empty - **Field Type Validation**: Ensures values match the expected field type ::: ### 2. Initialize the frontend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/progressive-profiling-react ``` #### 2.2 Update your frontend SDK configuration ```typescript import SuperTokens from "supertokens-auth-react"; import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profiling-react"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ ProgressiveProfilingPlugin.init({ setupPagePath: "/user/setup", // Optional: defaults to "/user/setup" requireSetup: true, // Optional: defaults to true showStartSection: true, // Optional: defaults to true showEndSection: true, // Optional: defaults to true onSuccess: async (data) => { // Optional: callback after successful profile completion console.log("Profile completed:", data); }, }), ], }, }); ``` ### 3. Test the implementation With this configuration, users automatically get redirected to the profile setup page after authentication. Try to authenticate and check if you get sent to the new form. Progressive profiling setup interface ## Customization ### Storage handlers By default, the plugin stores profile data using the `User Metadata` recipe. You can also implement your custom storage solution by overriding the `defaultStorageHandlerSetFields` and `defaultStorageHandlerGetFields` functions. ```typescript check=false reason="Partial configuration example" import SuperTokens from "supertokens-node"; import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profiling-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { // your app info }, recipeList: [ // your recipes ], experimental: { plugins: [ ProgressiveProfilingPlugin.init({ sections: [ { id: "basic-info", label: "Basic Information", description: "Tell us about yourself", fields: [ { id: "firstName", label: "First Name", type: "string", required: true, placeholder: "Enter your first name", }, { id: "lastName", label: "Last Name", type: "string", required: true, placeholder: "Enter your last name", }, { id: "company", label: "Company", type: "string", required: false, placeholder: "Enter your company name", }, ], }, ], override: (oI) => ({ ...oI, defaultStorageHandlerSetFields: async ({ pluginFormFields, data, session, userContext }) => { const userId = session.getUserId(userContext); const existingProfile = await customGetProfileData(userId); const profile = pluginFormFields.reduce( (acc, field) => { const newValue = data.find((d) => d.fieldId === field.id)?.value; const existingValue = existingProfile?.[field.id]; return { ...acc, [field.id]: newValue ?? existingValue ?? field.defaultValue, }; }, { ...existingProfile }, ); // Implement your own logic for storing profile data await customSetProfileData(userId, profile); }, defaultStorageHandlerGetFields: ({ pluginFormFields, session, userContext }) => { const userId = session.getUserId(userContext); // Implement your own logic for fetching profile data const existingProfile = await customGetProfileData(userId); return pluginFormFields.map((field) => ({ sectionId: field.sectionId, fieldId: field.id, value: existingProfile[field.id] ?? field.defaultValue, })); }, }), }), ], }, }); ``` ### User interface To create your own UI you can use the `usePluginContext` hook. It exposes an interface which you can use to interface with the endpoints exposed by the backend plugin. ```tsx check=false reason="Requires surrounding application context" import { usePluginContext } from "@supertokens-plugins/progressive-profiling-react"; function CustomProfileComponent() { const { api, t } = usePluginContext(); const [profile, setProfile] = useState([]); const handleLoadProfile = async () => { const result = await api.getProfile(); if (result.status === "OK") { setProfile(result.data); } }; const handleUpdateProfile = async (data) => { const result = await api.updateProfile({ data }); if (result.status === "OK") { console.log("Profile updated successfully"); } else if (result.status === "INVALID_FIELDS") { console.error("Validation errors:", result.errors); } }; return (

{t("PL_PP_SECTION_PROFILE_START_LABEL")}

{/* Your custom form components */}
); } ``` :::info[pre-built UI] You can integrate the pre-built UI into other pages/components by importing the `UserProfileWrapper` component: ```tsx import { UserProfileWrapper } from "@supertokens-plugins/progressive-profiling-react"; function MyApp() { return (
); } ``` ::: ### Form fields validation To change the default validation behavior you can override the `validateField` function: ```ts check=false reason="Partial configuration example" import SuperTokens from "supertokens-node"; import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profiling-nodejs"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { // your app info }, recipeList: [ // your recipes ], experimental: { plugins: [ ProgressiveProfilingPlugin.init({ sections: [ { id: "basic-info", label: "Basic Information", description: "Tell us about yourself", fields: [ { id: "firstName", label: "First Name", type: "string", required: true, placeholder: "Enter your first name", }, { id: "lastName", label: "Last Name", type: "string", required: true, placeholder: "Enter your last name", }, { id: "company", label: "Company", type: "string", required: false, placeholder: "Enter your company name", }, ], }, ], override: (oI) => ({ ...oI, validateField: ({ field, value }) => { // Custom logic to validate field values if (field.required && value === "") { return "Field is required"; } // Return undefined if validation passes return undefined; }, }), }), ], }, }); ``` ## Next steps Besides progressive profiling, you can also explore other user management features: Implement user banning functionality to restrict access. Implement role-based access control for your users. General information on how plugins work. --- # User banning Source: https://supertokens.com/docs/post-authentication/user-management/user-banning ## Overview This tutorial shows you how to add a user banning feature to your SuperTokens authentication flows. The guide makes use of the plugins functionality which provides the ability to ban/unban users. ## How it works The plugin makes use of the `UserRoles` feature to keep track of banned users. When you ban someone, they get assigned a new role, `banned`, and their session gets revoked immediately. Additionally, the default session validation logic gets overridden to prevent users with the banned role from accessing the application. ### Caching To avoid extra network calls during session verification, the plugin uses an in-memory cache to keep track of the ban status. The cache gets reloaded during the first session verification, after a server start, causing a slight increase in latency. If you are working with a serverless environment or with distributed applications, you can implement your own caching strategy through overrides. ## Before you start The user banning plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. Besides initializing the plugin, you also have to include the `UserRoles` recipe in your SuperTokens configuration. ## Steps ### 1. Initialize the backend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/user-banning-nodejs ``` #### 1.2 Update your backend SDK configuration ```typescript import SuperTokens from "supertokens-node"; import UserBanningPlugin from "@supertokens-plugins/user-banning-nodejs"; import UserRoles from "supertokens-node/recipe/userroles"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ UserRoles.init(), // Required: UserRoles recipe must be initialized // your other recipes ], experimental: { plugins: [ UserBanningPlugin.init({ userBanningPermission: "ban-user", // Optional: defaults to "ban-user" bannedUserRole: "banned", // Optional: defaults to "banned" }), ], }, }); ``` :::warning Make sure to also initialize the `UserRoles` recipe if you haven't already. ::: ### 2. Initialize the frontend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/user-banning-react ``` #### 2.2 Update your frontend SDK configuration ```typescript import SuperTokens from "supertokens-auth-react"; import UserBanningPlugin from "@supertokens-plugins/user-banning-react"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ UserBanningPlugin.init({ userBanningPermission: "ban-user", // Should match backend config bannedUserRole: "banned", // Should match backend config onPermissionFailureRedirectPath: "/", // Optional: defaults to "/" }), ], }, }); ``` ### 3. Ban users #### 3.1 Using the user banning interface The plugin provides a complete administrative interface accessible at `/admin/ban-user`. Before you access the interface, make sure that your user has the required permission, `ban-user` by default. Read the [role management actions page](/additional-verification/user-roles/role-management-actions#add-permissions) for instructions on how to add permissions to your users. User banning UI From the interface you can check the banning status of a user. Based on that status, you can either ban or remove the ban for that account. #### 3.2 Using direct API calls You can also manage user bans programmatically using the exposed API endpoints. ##### Ban/unban user ```javascript check=false reason="Requires surrounding async application context" // Ban a user const banResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?tenantId=public", { method: "POST", credentials: "include", // Include session cookies headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email: "user@example.com", // You can also pass the userId instead of the email // userId: "user123", isBanned: true, // true to ban, false to remove ban }), }); const banResult = await banResponse.json(); if (banResult.status === "OK") { console.log("User banned successfully"); } else { console.error("Failed to ban user:", banResult.message); } // Remove ban from a user const unbanResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?tenantId=public", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email: "user@example.com", isBanned: false, }), }); ``` ##### Check ban status ```javascript check=false reason="Requires surrounding async application context" // Check if a user is banned const statusResponse = await fetch( "/plugin/supertokens-plugin-user-banning/ban?tenantId=public&email=user@example.com", { method: "GET", credentials: "include", }, ); const status = await statusResponse.json(); if (status.status === "OK") { console.log("User is banned:", status.banned); } else { console.error("Error checking ban status:", status.message); } ``` ## Customization ### Implement a custom user interface To create a custom user interface you can use the `usePluginContext` hook. It allows you to access the plugin's API methods and configuration in custom React components: ```tsx import { usePluginContext } from "@supertokens-plugins/user-banning-react"; function MyCustomAdminComponent() { const { api, pluginConfig, t } = usePluginContext(); const handleBanUser = async (email: string) => { try { const result = await api.updateBanStatus("public", email, true); if (result.status === "OK") { console.log("User banned successfully"); } else { console.error("Failed to ban user:", result.message); } } catch (error) { console.error("Error:", error); } }; const handleCheckBanStatus = async (email: string) => { try { const result = await api.getBanStatus("public", email); if (result.status === "OK") { console.log("User has ban:", result.banned); } else { console.error("Error:", result.message); } } catch (error) { console.error("Error:", error); } }; return (

{t("PL_UB_BAN_PAGE_TITLE")}

); } ``` ## Next steps Besides user banning you can also look into other user management features and security measures: Prevent suspicious authentication attempts. Implement role-based access control for your users. General information on how plugins work. --- # User metadata Source: https://supertokens.com/docs/post-authentication/user-management/user-metadata ## Overview You can use the `UserMetadata` recipe to store your custom data about each user. This can be any arbitrary values that are JSON serializable. The following page shows you how to enable and use the feature. --- ## Enable the `UserMetadata` recipe ```tsx import SuperTokens from "supertokens-node"; import UserMetadata from "supertokens-node/recipe/usermetadata"; SuperTokens.init({ supertokens: { connectionURI: "...", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // Initialize other recipes as seen in the quick setup guide UserMetadata.init(), ], }); ``` ```go import ( "github.com/supertokens/supertokens-golang/recipe/usermetadata" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ // Initialize other recipes as seen in the quick setup guide usermetadata.Init(nil), }, }) } ``` ```python check=false reason="Partial configuration example" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import usermetadata init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..." ), framework='...', recipe_list=[ # Initialize other recipes as seen in the quick setup guide usermetadata.init() ] ) ``` --- ## Store data :::note[Only root-level properties merge into the stored object. Nested objects and all lower-level properties replace the existing ones.] ::: ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); res.json({ message: "successfully updated user metadata" }); }); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); return res.response({ message: "successfully updated user metadata" }).code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); res.send({ message: "successfully updated user metadata" }); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserMetadata from "supertokens-node/recipe/usermetadata"; async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); return { body: JSON.stringify({ message: "successfully updated user metadata" }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); ctx.body = { message: "successfully updated user metadata" }; }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import { SessionContext } from "supertokens-node/framework/loopback"; import UserMetadata from "supertokens-node/recipe/usermetadata"; class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); return { message: "successfully updated user metadata" }; } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); res.json({ message: "successfully updated user metadata" }); } ``` ```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ message: string }> { const userId = session.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); return { message: "successfully updated user metadata" }; } } ``` ```go import "github.com/supertokens/supertokens-golang/recipe/usermetadata" func main() { userId := "..." usermetadata.UpdateUserMetadata(userId, map[string]interface{}{ "newKey": "data", }) } ``` ```python from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata async def some_func(): user_id = "..." await update_user_metadata(user_id, { "newKey": "data" }) ``` ```python from supertokens_python.recipe.usermetadata.syncio import update_user_metadata user_id = "..." update_user_metadata(user_id, { "newKey": "data" }) ``` ```tsx check=false reason="Requires surrounding framework application context" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserMetadata from "supertokens-node/recipe/usermetadata"; 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 }); } const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); return NextResponse.json({ success: "successfully updated user metadata" }); }); } ``` :::info[Multi Tenancy] User metadata that associates with a user shares across all tenants that that user is a part of. If instead, you want to store user metadata on a tenant level, you can add a custom key in the JSON like: ```json { "tenant1": { "someKey": "specific to teannt1" }, "tenant2": { "someKey": "specific to teannt2" }, "someKey": "common for all tenants" } ``` and then read the appropriate key based on the `tenantId`. ::: --- ## Access data ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); res.json({ preferences: metadata.preferences }); }); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); return res.response({ preferences: metadata.preferences }).code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); res.send({ preferences: metadata.preferences }); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); return { body: JSON.stringify({ preferences: metadata.preferences }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); ctx.body = { preferences: metadata.preferences }; }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { SessionContext } from "supertokens-node/framework/loopback"; class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); return { preferences: metadata.preferences }; } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { SessionRequest } from "supertokens-node/framework/express"; export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); res.json({ preferences: metadata.preferences }); } ``` ```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ preferences: any }> { const userId = session.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); return { preferences: metadata.preferences }; } } ``` ```go import ( "fmt" "github.com/supertokens/supertokens-golang/recipe/usermetadata" ) func main() { userId := "..." metadata, err := usermetadata.GetUserMetadata(userId) if err != nil { // TODO: handle error... } exampleValue := metadata["exampleKey"] fmt.Println(exampleValue) } ``` ```python from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata async def some_func(): user_id = "..." metadataResult = await get_user_metadata(user_id) exampleValue = metadataResult.metadata["exampleKey"] print(exampleValue) ``` ```python from supertokens_python.recipe.usermetadata.syncio import get_user_metadata user_id = "..." metadataResult = get_user_metadata(user_id) exampleValue = metadataResult.metadata["exampleKey"] print(exampleValue) ``` ```tsx check=false reason="Requires surrounding framework application context" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserMetadata from "supertokens-node/recipe/usermetadata"; 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 }); } const userId = session!.getUserId(); const { metadata } = await UserMetadata.getUserMetadata(userId); return NextResponse.json({ preferences: metadata.preferences }); }); } ``` :::info[Important] By default, all users have an empty metadata object. ::: ## Delete metadata You can either delete all the user's metadata, or certain fields from them: ### Delete specific fields You can do this by calling the update metadata function and setting the field you want to remove to be `null`. For example, if you have the following metadata object for a user: ```json { "preferences": { "theme": "dark" }, "notifications": { "email": true }, "todos": ["use-text-notifs"] } ``` And you want to remove the `"notifications"` field, you can update the metadata object with the following JSON: ```json { "notifications": null } ``` This would result in the final metadata object: ```json { "preferences": { "theme": "dark" }, "todos": ["use-text-notifs"] } ``` :::info[Important] You can only remove the root level fields in the metadata object in this way. From the above example, if you set `preferences.theme: null`, then it does not remove the `"theme"` field, but instead sets it to a JSON null value. ::: In code, it would look like: ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); res.json({ message: "successfully updated user metadata" }); }); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); return res.response({ message: "successfully updated user metadata" }).code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); res.send({ message: "successfully updated user metadata" }); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserMetadata from "supertokens-node/recipe/usermetadata"; async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); return { body: JSON.stringify({ message: "successfully updated user metadata" }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); ctx.body = { message: "successfully updated user metadata" }; }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import { SessionContext } from "supertokens-node/framework/loopback"; import UserMetadata from "supertokens-node/recipe/usermetadata"; class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); return { message: "successfully updated user metadata" }; } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); res.json({ message: "successfully updated user metadata" }); } ``` ```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ message: string }> { const userId = session.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); return { message: "successfully updated user metadata" }; } } ``` ```go import "github.com/supertokens/supertokens-golang/recipe/usermetadata" func main() { userId := "..." usermetadata.UpdateUserMetadata(userId, map[string]interface{}{ "notifications": nil, }) } ``` ```python from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata async def some_func(): user_id = "..." await update_user_metadata(user_id, { "notifications": None }) ``` ```python from supertokens_python.recipe.usermetadata.syncio import update_user_metadata user_id = "..." update_user_metadata(user_id, { "notifications": None }) ``` ```tsx check=false reason="Requires surrounding framework application context" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserMetadata from "supertokens-node/recipe/usermetadata"; 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 }); } const userId = session!.getUserId(); await UserMetadata.updateUserMetadata(userId, { notifications: null }); return NextResponse.json({ message: "successfully updated user metadata" }); }); } ``` ### Delete the entire metadata object Using this function deletes all the fields in the user metadata object for that user. ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let app = express(); app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); res.json({ success: true }); }); ``` ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; import { SessionRequest } from "supertokens-node/framework/hapi"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let server = Hapi.server({ port: 8000 }); server.route({ path: "/updateinfo", method: "post", options: { pre: [ { method: verifySession(), }, ], }, handler: async (req: SessionRequest, res) => { const session = req.session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); return res.response({ success: true }).code(200); }, }); ``` ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let fastify = Fastify(); fastify.post( "/updateinfo", { preHandler: verifySession(), }, async (req, res) => { const session = req.session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); res.send({ success: true }); }, ); ``` ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserMetadata from "supertokens-node/recipe/usermetadata"; async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); return { body: JSON.stringify({ success: true }), statusCode: 200, }; } exports.handler = verifySession(updateinfo); ``` ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserMetadata from "supertokens-node/recipe/usermetadata"; let router = new KoaRouter(); router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => { const session = ctx.session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); ctx.body = { success: true }; }); ``` ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import { SessionContext } from "supertokens-node/framework/loopback"; import UserMetadata from "supertokens-node/recipe/usermetadata"; class UpdateInfo { constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} @post("/updateinfo") @intercept(verifySession()) @response(200) async handler() { const session = this.ctx.session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); return { success: true }; } } ``` ```tsx import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserMetadata from "supertokens-node/recipe/usermetadata"; export default async function updateInfo(req: any, res: any) { await superTokensNextWrapper( async (next) => { await verifySession()(req, res, next); }, req, res, ); const session = (req as SessionRequest).session; const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); res.json({ success: true }); } ``` ```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ success: boolean }> { const userId = session.getUserId(); // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. await UserMetadata.clearUserMetadata(userId); return { success: true }; } } ``` ```go import "github.com/supertokens/supertokens-golang/recipe/usermetadata" func main() { userId := "..." usermetadata.ClearUserMetadata(userId) } ``` ```python from supertokens_python.recipe.usermetadata.asyncio import clear_user_metadata async def some_func(): user_id = "..." await clear_user_metadata(user_id) ``` ```python from supertokens_python.recipe.usermetadata.syncio import clear_user_metadata user_id = "..." clear_user_metadata(user_id) ``` ```tsx check=false reason="Requires surrounding framework application context" import { NextResponse, NextRequest } from "next/server"; import SuperTokens from "supertokens-node"; import { withSession } from "supertokens-node/nextjs"; import UserMetadata from "supertokens-node/recipe/usermetadata"; 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 }); } const userId = session!.getUserId(); await UserMetadata.clearUserMetadata(userId); return NextResponse.json({ success: true }); }); } ``` --- ## See also --- # Profile Management Source: https://supertokens.com/docs/post-authentication/user-management/user-profile ## Overview This tutorial shows you how to add comprehensive user profile management to your **SuperTokens** authentication flows. The guide makes use of the `plugins` functionality which provides a complete profile management interface with customizable form fields, account information display, and automatic third-party data integration. The functionality integrates with the [progressive profiling plugin](/post-authentication/user-management/progressive-profiling) by default. This allows you to: - Gradually collect user information through the progressive profiling flow - Display collected information in the profile details interface - Keep both systems synchronized automatically ## Before you start The profile details plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. You need to start from a working **SuperTokens** setup. If you haven't done that already, please refer to the [Quickstart Guides](/quickstart). ## Steps ### 1. Initialize the backend plugin #### 1.1 Install the plugin ```bash npm install @supertokens-plugins/profile-details-nodejs ``` #### 1.2 Update your backend SDK configuration The backend plugin exposes new endpoints which, in turn, get used by the frontend implementation. ```typescript import SuperTokens from "supertokens-node"; import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-nodejs"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", }, recipeList: [ // your recipes (Session recipe is required) ], experimental: { plugins: [ ProfileDetailsPlugin.init({ sections: [ { id: "personal-details", label: "Personal Information", description: "Your personal details", fields: [ { id: "firstName", label: "First Name", type: "string", required: true, placeholder: "Enter your first name", }, { id: "lastName", label: "Last Name", type: "string", required: true, placeholder: "Enter your last name", }, { id: "company", label: "Company", type: "string", required: false, placeholder: "Enter your company name", }, ], }, { id: "preferences", label: "Preferences", description: "Customize your experience", fields: [ { id: "avatar", label: "Profile Picture", type: "image-url", required: false, placeholder: "https://example.com/avatar.jpg", }, { id: "theme", label: "Preferred Theme", type: "select", required: false, options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, { value: "auto", label: "Auto" }, ], defaultValue: "auto", }, ], }, ], registerSectionsForProgressiveProfiling: true, // Optional: defaults to true }), ], }, }); ``` ##### Supported field types The plugin supports the following field types: | Field Type | Description | Value Type | Example Use Case | |------------|-------------|------------|------------------| | `string` | Single-line text input | `string` | Name, title, company | | `text` | Multi-line text area | `string` | Bio, description, comments | | `number` | Numeric input | `number` | Age, salary, experience | | `boolean` | Checkbox input | `boolean` | Newsletter subscription | | `toggle` | Toggle switch | `boolean` | Feature preferences | | `email` | Email input with validation | `string` | Contact email | | `phone` | Phone number input | `string` | Contact number | | `date` | Date picker | `string` (ISO 8601 format) | Birth date, start date | | `select` | Dropdown selection | `string` | Country, department, role | | `multiselect` | Multiple selection dropdown | `string[]` | Skills, interests, languages | | `password` | Password input | `string` | API keys, secure tokens | | `url` | URL input with validation | `string` | Website, social profiles | | `image-url` | Image URL input with preview | `string` | Profile picture, logo | ##### Third-party data integration The plugin automatically integrates with third-party authentication providers to populate profile fields. When users sign in using external providers, the plugin maps provider data to profile fields (if the fields are configured): - `firstName`: Maps from `name`, `given_name`, or `first_name` - `lastName`: Maps from `family_name` or `last_name` - `avatar`: Maps from `picture` or `avatar_url` :::info You can customize how third-party data maps to your profile fields by overriding the function `getFieldValueFromThirdPartyUserInfo`. ```typescript check=false reason="Requires surrounding application context" import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-nodejs"; SuperTokens.init({ // ... other config experimental: { plugins: [ ProfileDetailsPlugin.init({ override: (oI) => ({ ...oI, getFieldValueFromThirdPartyUserInfo: (providerId, field, rawUserInfoFromProvider, profile) => { return rawUserInfoFromProvider[field.id]; }, }), }), ], }, }); ``` ::: ### 2. Initialize the frontend plugin #### 2.1 Install the plugin ```bash npm install @supertokens-plugins/profile-details-react ``` #### 2.2 Update your frontend SDK configuration Initialize the frontend plugin in your existing configuration. With the following setup the `/user/profile` path renders the profile details page. ```typescript import SuperTokens from "supertokens-auth-react"; import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-react"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ProfileDetailsPlugin.init()], }, }); ``` :::info[The user profile page gets rendered by default on the `/user/profile` path.] If you want to change the path, you have to initialize the `profile-base-react` plugin with the `profilePagePath` option. ```typescript import SuperTokens from "supertokens-auth-react"; import ProfileBasePlugin from "@supertokens-plugins/profile-base-react"; import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-react"; SuperTokens.init({ appInfo: { appName: "My App", apiDomain: "https://api.example.com", websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { plugins: [ ProfileBasePlugin.init({ profilePagePath: "/user/profile", }), ProfileDetailsPlugin.init(), ], }, }); ``` ::: ### 3. Test the implementation Authenticate and then visit the `/user/profile` path. You should see the new interface that renders the profile data. Progressive profiling setup interface ## Customization ### Custom field components To add custom rendering behavior for fields you have to pass an override during plugin initialization. ```typescript check=false reason="Requires surrounding framework application context" import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-react"; import { CustomStringInput, CustomStringView } from "./your-custom-components"; SuperTokens.init({ // ... other config experimental: { plugins: [ ProfileDetailsPlugin.init({ override: (oI) => ({ ...oI, fieldInputComponentMap: (originalMap) => ({ ...originalMap, string: CustomStringInput, }), fieldViewComponentMap: (originalMap) => ({ ...originalMap, string: CustomStringView, }), }), }), ], }, }); ``` ### Custom user interface To create your own UI you can use the `usePluginContext` hook. It exposes an interface which you can use to call the endpoints exposed by the backend plugin. ```tsx check=false reason="Requires surrounding application context" import { usePluginContext } from "@supertokens-plugins/profile-details-react"; function CustomProfileComponent() { const { api, t, fieldInputComponentMap } = usePluginContext(); const [profile, setProfile] = useState(null); const handleGetDetails = async () => { const result = await api.getDetails(); if (result.status === "OK") { setProfile(result.profile); } }; const handleUpdateProfile = async (data) => { const result = await api.updateProfile({ data }); if (result.status === "OK") { console.log("Profile updated successfully"); } }; return (

{t("PL_CD_SECTION_ACCOUNT_LABEL")}

{/* Your custom form components */}
); } ``` ## Next steps Besides profile details management, you can also explore other user management features: Gradually collect user information through customizable forms. Implement user banning functionality to restrict access. Implement role-based access control for your users. General information on how plugins work. --- # Quickstart Guide Source: https://supertokens.com/docs/quickstart ## Overview Inspect this repository and integrate SuperTokens into the existing application. First discover the frontend stack and backend stack, including languages, frameworks, package managers, routers, SDK versions, existing authentication code, and environment configuration. If the frontend or backend stack cannot be determined reliably, ask the user to provide it before making changes. Also ask which authentication methods and UI approach they need if those choices cannot be inferred. Use the current SuperTokens documentation and SDK APIs, preserve the project's conventions, and do not commit secrets. Configure the frontend, backend, sessions, routes, middleware, cookies, CORS, and environment variables as required. Run the relevant typechecks, tests, and build, then summarize changed files, required environment variables, and validation results. This guide walks through adding Email/Password authentication with either the SuperTokens prebuilt UI or your own custom UI. Configure the frontend first, then connect your backend and prepare the integration for production. ## Steps ### 1. Integrate the frontend SDK #### Frontend integration summary - React uses `supertokens-auth-react`; Angular and Vue use `supertokens-web-js`. - Initialize the authentication and Session recipes. React applications also wrap their component tree with `SuperTokensWrapper`. - Render the prebuilt login UI on `/auth`. - The SDK intercepts `fetch` and XHR requests to manage session tokens automatically. Web sessions use HTTP-only cookies by default, with header-based authentication available as an alternative. Start the setup by configuring your frontend application to use **SuperTokens** for authentication. This guide uses the **SuperTokens pre-built UI** components. If you want to create your own interface please check the **Custom UI** tutorial. 1.1 Install the SDK Run the following command in your terminal to install the package. ```bash title="Reactjs" option="package-managers:npm" npm i -s supertokens-auth-react ``` ```bash title="Reactjs" option="package-managers:yarn" yarn add supertokens-auth-react supertokens-web-js ``` ```bash title="Reactjs" option="package-managers:pnpm" pnpm add supertokens-auth-react supertokens-web-js ``` ```bash title="Reactjs" option="package-managers:bun" bun add supertokens-auth-react supertokens-web-js ``` ```bash title="Angular" option="package-managers:npm" npm i -s supertokens-web-js ``` ```bash title="Angular" option="package-managers:yarn" yarn add supertokens-web-js ``` ```bash title="Angular" option="package-managers:pnpm" pnpm add supertokens-web-js ``` ```bash title="Angular" option="package-managers:bun" bun add supertokens-web-js ``` ```bash title="Vue" option="package-managers:npm" npm i -s supertokens-web-js ``` ```bash title="Vue" option="package-managers:yarn" yarn add supertokens-web-js ``` ```bash title="Vue" option="package-managers:pnpm" pnpm add supertokens-web-js ``` ```bash title="Vue" option="package-managers:bun" bun add supertokens-web-js ``` #### 1.2 Initialize the SDK In your main application file call the `SuperTokens.init` function to initialize the SDK. The `init` call includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. After that you have to wrap the application with the `SuperTokensWrapper` component. This provides authentication context for the rest of the UI tree. Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Angular app. **Architecture** - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Angular app, so that all pages in your app can use it. - You have to create a `/auth*` route in the Angular app which renders our pre-built UI. which also needs to be initialised, but only on that route. Creating the `/auth` route - Use the Angular CLI to generate a new route Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Vue app **Architecture** - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Vue app, so that all pages in your app can use it. - We create a `/auth*` route in the Vue app which renders our pre-built UI which also needs to be initialised, but only on that route. **Creating the `/auth` route** - Create a new file `AuthView.vue`, this Vue component is used to render the auth component: ```tsx title="Reactjs" import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [EmailPassword.init(), Session.init()], }); /* Your App */ class App extends React.Component { render() { return {/*Your app components*/}; } } ``` ```bash title="Angular" ng generate module auth --route auth --module app.module ``` ```tsx check=false reason="This is a Vue single-file component containing both TypeScript and template markup." title="Vue" ``` - Add the following code to your `auth` angular component - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. - Initialize the `supertokens-web-js` SDK in your Vue app's `main.ts` file. This provides session management across your entire application. ```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; import { DOCUMENT } from "@angular/common"; @Component({ selector: "app-auth", template: '
', }) export class AuthComponent implements OnDestroy, AfterViewInit { constructor( private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, ) {} ngAfterViewInit() { this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@v0.48.0/build/static/js/main.81589a39.js"); } ngOnDestroy() { // Remove the script when the component is destroyed const script = this.document.getElementById("supertokens-script"); if (script) { script.remove(); } } private loadScript(src: string) { const script = this.renderer.createElement("script"); script.type = "text/javascript"; script.src = src; script.id = "supertokens-script"; script.onload = () => { supertokensUIInit("supertokensui", { appInfo: { appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [supertokensUIEmailPassword.init(), supertokensUISession.init()], }); }; this.renderer.appendChild(this.document.body, script); } } ``` ```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" import { createApp } from "vue"; import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import App from "./App.vue"; import router from "./router"; SuperTokens.init({ appInfo: { appName: "", apiDomain: "", apiBasePath: "/auth", }, recipeList: [Session.init()], }); const app = createApp(App); app.use(router); app.mount("#app"); ```
- In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. - Initialize the `supertokens-web-js` SDK in your angular app's root component. This provides session management across your entire application. ```tsx title="Angular" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ appInfo: { appName: "", apiDomain: "", apiBasePath: "/auth", }, recipeList: [Session.init()], }); ``` #### 1.3 Configure routing In order for the **pre-built UI** to be rendered inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. Call the `getSuperTokensRoutesForReactRouterDom` method from within any `react-router-dom` `Routes` component. Add the route handling shown below to your root-level `render` function. Update your angular router so that all auth related requests load the `auth` component Update your Vue router so that all auth related requests load the `AuthView` component ```tsx title="Reactjs" option="react-router:yes" import React from "react"; import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { render() { return ( {/*This renders the login UI on the /auth route*/} {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} {/*Your app routes*/} ); } } ``` ```tsx title="Reactjs" option="react-router:no" import React from "react"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { render() { if (canHandleRoute([EmailPasswordPreBuiltUI])) { // This renders the login UI on the /auth route return getRoutingComponent([EmailPasswordPreBuiltUI]); } return {/*Your app*/}; } } ``` ```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ { path: "auth", loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule), }, { path: "**", loadChildren: () => import("./home/home.module").then((m) => m.HomeModule), }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} ``` ```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" import { createRouter, createWebHistory } from "vue-router"; import HomeView from "../views/HomeView.vue"; import AuthView from "../views/AuthView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "home", component: HomeView, }, { path: "/auth/:pathMatch(.*)*", name: "auth", component: AuthView, }, ], }); export default router; ``` :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. ::: ```tsx title="Reactjs" option="react-router:yes" import React from "react"; import { BrowserRouter, useRoutes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; function AppRoutes() { const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ ]); const routes = useRoutes([ ...authRoutes.map((route) => route.props), // Include the rest of your app routes ]); return routes; } function App() { return ( ); } ``` #### 1.4 Handle session tokens This part is handled automatically by the **Frontend SDK**. You don't need to do anything. The step serves more as a way for us to tell you how is this handled under the hood. After you call the `init` function, the **SDK** adds interceptors to both `fetch` and `XHR`, XMLHTTPRequest. The latter is used by the `axios` library. The interceptors save the session tokens that are generated from the authentication flow. Those tokens are then added to requests initialized by your frontend app which target the backend API. By default, the tokens are stored through session cookies but you can also switch to [header based authentication](/post-authentication/session-management/switch-between-cookies-and-header-authentication). #### 1.5 Secure application routes In order to prevent unauthorized access to certain parts of your frontend application you can use our utilities. Follow the code samples below to understand how to do this. You can wrap your components with the `` react component. This ensures that your component renders only if the user is logged in. If they are not logged in, the user is redirected to the login page. You can use the `doesSessionExist` function to check if a session exists in all your routes. You can use the `doesSessionExist` function to check if a session exists in all your routes. ```tsx check=false reason="Requires surrounding quickstart application context" title="Reactjs" import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; import MyDashboardComponent from "./dashboard"; class App extends React.Component { render() { return ( {/*Components that require to be protected by authentication*/}
} /> ); } } ``` ```tsx title="Angular" import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx title="Vue" import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` 1.1 Install the SDK Use the following command to install the required package. :::info If you want to implement a common authentication experience for both web and mobile, please look at our [**Unified Login guide**](/authentication/unified-login/introduction). ::: Add to your `settings.gradle`: Using CocoaPods Add the CocoaPods dependency to your `Podfile` Add the dependency to your pubspec.yaml ```bash title="Web" option="install-method:npm" npm i -s supertokens-web-js ``` ```bash title="Mobile" option="mobile-frameworks:reactnative" npm i -s supertokens-react-native@5.1.5 @react-native-async-storage/async-storage@2.2.0 ``` ```bash title="Mobile" option="mobile-frameworks:android" dependencyResolutionManagement { ... repositories { ... maven { url 'https://jitpack.io' } } } ``` ```bash title="Mobile" option="mobile-frameworks:ios" pod 'SuperTokensIOS', '0.4.2' ``` ```bash title="Mobile" option="mobile-frameworks:flutter" supertokens_flutter: 0.6.5 ``` Add the following to you app level's `build.gradle`: ##### Using Swift Package Manager Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. When adding the dependency, select version `0.4.2` after you enter the SuperTokens iOS repository URL: You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-flutter/releases) (ignore the `v` prefix in the releases). ```bash title="Mobile" option="mobile-frameworks:android" implementation 'com.github.supertokens:supertokens-android:0.5.3' ``` ```bash title="Mobile" option="mobile-frameworks:ios" https://github.com/supertokens/supertokens-ios ``` You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-android/releases) (ignore the `v` prefix in the releases). #### 1.2 Initialize SuperTokens Call the SDK init function at the start of your application. The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. Add the `SuperTokens.init` function call at the start of your application. ```tsx title="Web" option="install-method:npm" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; import EmailPassword from "supertokens-web-js/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "", apiBasePath: "/auth", appName: "...", }, recipeList: [Session.init(), EmailPassword.init()], }); ``` ```tsx title="Mobile" option="mobile-frameworks:reactnative" import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "", apiBasePath: "/auth", }); ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(this, "") .apiBasePath("/auth") .build() } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "", apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "", apiBasePath: "/auth", ); } ``` #### 1.3 Add the login UI The **Email/Password** flow involves two types of user interfaces. One for registering and creating new users, the *Sign Up Form*. And one for the actual authentication attempt, the *Sign In Form*. If you are provisioning users from a different method you can skip over adding the sign up form. ##### 1.3.1 Add the sign-up form For the **Sign Up** flow you have to first add the UI elements which render your form. After that, call the following function when the user submits the form that you have previously created. For the **Sign Up** flow you have to first add the UI elements which render your form. After that, call the following API when the user submits the form that you have previously created. ```tsx title="Web" option="install-method:npm" import { signUp } from "supertokens-web-js/recipe/emailpassword"; async function signUpClicked(email: string, password: string) { try { let response = await signUp({ formFields: [ { id: "email", value: email, }, { id: "password", value: password, }, ], }); if (response.status === "FIELD_ERROR") { // one of the input formFields failed validation response.formFields.forEach((formField) => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax), // or the email is not unique. window.alert(formField.error); } else if (formField.id === "password") { // Password validation failed. // Maybe it didn't match the password strength window.alert(formField.error); } }); } else if (response.status === "SIGN_UP_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign up was not allowed. window.alert(response.reason); } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage"; } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash title="Mobile" curl --location --request POST '/auth/signup' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }] }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User creation was successful. The response also contains more information about the user, for example their user ID. - `status: "FIELD_ERROR"`: One of the form field inputs failed validation. The response body contains information about which form field input based on the `id`: - The email could fail validation if it's syntactically not an email, of it it's not unique. - The password could fail validation if it's not string enough (as defined by the backend password validator). Either way, you want to show the user an error next to the input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. The `formFields` input is a key-value array. You must provide it an `email` and a `password` value at a minimum. If you want to provide additional items, for example the user's name or age, you can append it to the array like so: ```json { "formFields": [ { "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }, { "id": "name", "value": "John Doe" } ] } ``` On the backend, the `formFields` array is available to you for consumption. On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. ###### How to check if an email is unique As a part of the sign up form, you may want to explicitly check that the entered email is unique. Whilst this is already done via the sign up API call, it may be a better UX to warn the user about a non unique email right after they finish typing it. ```tsx title="Web" option="install-method:npm" import { doesEmailExist } from "supertokens-web-js/recipe/emailpassword"; async function checkEmail(email: string) { try { let response = await doesEmailExist({ email, }); if (response.doesExist) { window.alert("Email already exists. Please sign in instead"); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash title="Mobile" curl --location --request GET '/auth/emailpassword/email/exists?email=john@example.com' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: The response also contains a `exists` boolean which is `true` if the input email already belongs to an email password user. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. ##### 1.3.2 Add the sign-in form For the **Sign In** flow you have to first add the UI elements which render your form. After that, call the following function when the user submits the form that you have previously created. For the **Sign In** flow you have to first add the UI elements which render your form. After that, call the following API when the user submits the form that you have previously created. ```tsx title="Web" option="install-method:npm" import { signIn } from "supertokens-web-js/recipe/emailpassword"; async function signInClicked(email: string, password: string) { try { let response = await signIn({ formFields: [ { id: "email", value: email, }, { id: "password", value: password, }, ], }); if (response.status === "FIELD_ERROR") { response.formFields.forEach((formField) => { if (formField.id === "email") { // Email validation failed (for example incorrect email syntax). window.alert(formField.error); } }); } else if (response.status === "WRONG_CREDENTIALS_ERROR") { window.alert("Email password combination is incorrect."); } else if (response.status === "SIGN_IN_NOT_ALLOWED") { // the reason string is a user friendly message // about what went wrong. It can also contain a support code which users // can tell you so you know why their sign in was not allowed. window.alert(response.reason); } else { // sign in successful. The session tokens are automatically handled by // the frontend SDK. window.location.href = "/homepage"; } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { // this may be a custom error message sent from the API by you. window.alert(err.message); } else { window.alert("Oops! Something went wrong."); } } } ``` ```bash title="Mobile" curl --location --request POST '/auth/signin' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ "id": "email", "value": "john@example.com" }, { "id": "password", "value": "somePassword123" }] }' ``` The response body from the API call has a `status` property in it: - `status: "OK"`: User sign in was successful. The response also contains more information about the user, for example their user ID. - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. #### 1.4 Handle session tokens You can use sessions with SuperTokens in two modes: - Using `httpOnly` cookies - Authorization bearer token. Our frontend SDK uses `httpOnly` cookie based session for websites by default as it secures against tokens theft via XSS attacks. For other platforms, like mobile apps, we use a bearer token in the `Authorization` header by default. ##### With the Frontend SDK :::success[No action required.] ::: Our frontend SDK handles everything for you. You only need to make sure that you have called `supertokens.init` before making any network requests. Our SDK adds interceptors to `fetch` and `XHR` (used by `axios`) to save and add session tokens from and to the request. By default, our web SDKs use cookies to provide credentials. Our frontend SDK handles everything for you. You only need to make sure that you have added our network interceptors as shown below :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ###### Axios ###### Using a custom Axios instance ###### HttpURLConnection ###### `URLSession` ###### Using `URLSession.shared` ###### `http` You can make requests as you normally would with `http`, the only difference is that you import the client from the SuperTokens package instead. ```tsx title="Mobile" option="mobile-frameworks:reactnative" import axios from "axios"; import SuperTokens from "supertokens-react-native"; let axiosInstance = axios.create({ /*...*/ }); SuperTokens.addAxiosInterceptors(axiosInstance); async function callAPI() { // use axios as you normally do let response = await axiosInstance.get("https://yourapi.com"); } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens import com.supertokens.session.SuperTokensHttpURLConnection import com.supertokens.session.SuperTokensPersistentCookieStore import java.net.URL import java.net.HttpURLConnection class MainApplication: Application() { override fun onCreate() { super.onCreate() // TODO: Make sure to call SuperTokens.init } fun makeRequest() { val url = URL("") val connection = SuperTokensHttpURLConnection.newRequest(url, object: SuperTokensHttpURLConnection.PreConnectCallback { override fun doAction(con: HttpURLConnection?) { // TODO: Use `con` to set request method, headers etc } }) // Handle response using connection object, for example: if (connection.responseCode == 200) { // TODO: implement } } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import Foundation import SuperTokensIOS fileprivate class NetworkManager { func setupSuperTokensInterceptor() { URLProtocol.registerClass(SuperTokensURLProtocol.self) } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/http.dart' as http; // SuperTokens wraps the package:http API. Future makeRequest() async { Uri uri = Uri.parse("http://localhost:3001/api"); var response = await http.get(uri); // handle response } ``` ###### Using the global Axios instance :::note[You must call `addAxiosInterceptors` on all `axios` imports.] ::: :::note[When making network requests you do not need to call `HttpURLConnection.connect` because SuperTokens does this for you.] ::: ###### OkHttp or Retrofit ###### Using a custom `URLSession` instance ###### Using a custom HTTP client If you use a custom HTTP client and want to use SuperTokens, you can simply provide the SDK with your client. All requests continue to use your client along with the session logic that SuperTokens provides. ```tsx title="Mobile" option="mobile-frameworks:reactnative" import axios from "axios"; import SuperTokens from "supertokens-react-native"; SuperTokens.addAxiosInterceptors(axios); async function callAPI() { // use axios as you normally do let response = await axios.get("https://yourapi.com"); } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.content.Context import com.supertokens.session.SuperTokens import com.supertokens.session.SuperTokensInterceptor import okhttp3.OkHttpClient import retrofit2.Retrofit class NetworkManager { fun getClient(context: Context): OkHttpClient { val clientBuilder = OkHttpClient.Builder() clientBuilder.addInterceptor(SuperTokensInterceptor()) // TODO: Make sure to call SuperTokens.init val client = clientBuilder.build() // REQUIRED FOR RETROFIT ONLY val instance = Retrofit.Builder() .baseUrl("") .client(client) .build() return client } fun makeRequest(context: Context) { val client = getClient(context) // Use client to make requests normally } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import Foundation import SuperTokensIOS fileprivate class NetworkManager { func setupSuperTokensInterceptor() { let configuration = URLSessionConfiguration.default configuration.protocolClasses = [SuperTokensURLProtocol.self] let session = URLSession(configuration: configuration) // Use session when making network requests } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:http/http.dart' as base_http; import 'package:supertokens_flutter/http.dart' as supertokens_http; Future makeRequest() async { Uri uri = Uri.parse("http://localhost:3001/api"); var customClient = base_http.Client(); var httpClient = supertokens_http.Client(client: customClient); var response = await httpClient.get(uri); // handle response } ``` ###### Fetch :::success[When using `fetch`, network interceptors are added automatically when you call `supertokens.init`. So no action needed here.] ::: :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ###### Alamofire ###### Dio ###### Add the SuperTokens interceptor Use the extension method provided by the SuperTokens SDK to enable interception on your `Dio` client. This allows the SuperTokens SDK to handle session tokens for you. ```swift title="Mobile" option="mobile-frameworks:ios" import Foundation import SuperTokensIOS import Alamofire fileprivate class NetworkManager { func setupSuperTokensInterceptor() { let configuration = URLSessionConfiguration.af.default configuration.protocolClasses = [SuperTokensURLProtocol.self] + (configuration.protocolClasses ?? []) let session = Session(configuration: configuration) // Use session when making network requests } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/dio.dart'; import 'package:dio/dio.dart'; void setup() { Dio dio = Dio(); // Create a Dio instance. dio.addSupertokensInterceptor(); } ``` :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ###### Making network requests You can make requests as you normally would with `dio`. ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/dio.dart'; import 'package:dio/dio.dart'; void setup() { Dio dio = Dio( // Provide your config here ); dio.addSupertokensInterceptor(); var response = dio.get("http://localhost:3001/api"); // handle response } ``` :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] ::: ##### Without the Frontend SDK :::warning[We highly recommend using our frontend SDK to handle session token management. It saves you a lot of time.] ::: In this case, you need to manually handle the tokens and session refreshing, and decide if you are going to use header or cookie-based sessions. For browsers, we recommend cookies, while for mobile apps (or if you don't want to use the built-in cookie manager) you should use header-based sessions. **Cookie** ###### During the Login Action You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "cookie". The login API returns the following headers: - `Set-Cookie`: This contains the `sAccessToken`, `sRefreshToken` cookies which are `httpOnly` and are automatically managed by the browser. For mobile apps, you need to setup cookie handling yourself, use our SDK or use a header based authentication mode. - `front-token` header: This contains information about the access token: - The userID - The expiry time of the access token - The payload added by you in the access token. Here is the structure of the token: ```tsx let frontTokenFromRequestHeader = "..."; let frontTokenDecoded = JSON.parse(decodeURIComponent(escape(atob(frontTokenFromRequestHeader)))); console.log(frontTokenDecoded); /* { ate: 1665226412455, // time in milliseconds for when the access token expires, and then a refresh is required uid: "....", // user ID up: { sub: "..", iat: .., ... // other access token payload } } */ ``` This token is mainly used for cookie-based authentication because you don't have access to the actual access token on the frontend. You may still want to read its payload, for example to adjust the UI based on the user's role. The token is not signed and must not be used for authorization. If you cache it, treat its contents as untrusted and clear it when the session ends. - `anti-csrf` header (optional): By default it's not required, so it's not sent. But if this is sent, you should save this token as well for use when making requests. ###### When You Make Network Requests to Protected APIs The `sAccessToken` gets attached to the request automatically by the browser. Other than that, you need to add the following headers to the request: - `rid: "anti-csrf"` - this prevents against anti-CSRF requests. If your `apiDomain` and `websiteDomain` values are exactly the same, then this is not necessary. - `anti-csrf` header (optional): If this was provided to you during login, then you need to add that token as the value of this header. - For cross-origin browser requests, set the Fetch `credentials` request option to `"include"` (or the equivalent option in your HTTP library). `credentials` is not an HTTP header and does not accept `true` in Fetch. An API call can potentially update the `sAccessToken` and `front-token` tokens, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for: - `sAccessToken`: This is as a new `Set-Cookie` header and is managed by the browser automatically. - `front-token`: This should be read and saved by you in the same way as it's being done during login. ###### Handling session refreshing If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. You can call the refresh API as follows: ```bash curl --location --request POST '/auth/session/refresh' \ --header 'Cookie: sRefreshToken=...' ``` :::note[You may also need to add the `anti-csrf` header to the request if that was provided to you during sign in.] - The cURL command above shows the `sRefreshToken` cookie as well, but this is added by the web browser automatically, so you don't need to add it explicitly. ::: The result of a session refresh is either: - Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. - Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `front-token` that you saved on the frontend earlier. **Header (Authorization Bearer)** ###### During the Login Action You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "header". The login API returns the following headers: - `st-access-token`: This contains the current access token associated with the session. - `st-refresh-token`: This contains the current refresh token associated with the session. Do not persist these tokens in browser `localStorage`, because injected scripts can read them. Prefer the Web SDK's cookie-based mode for browsers. Native applications should use platform-provided secure storage. If you manually use header-based authentication in a browser, keep tokens in memory and account for the session ending when the page reloads. ###### When You Make Network Requests to Protected APIs You need to add the following headers to request: - `authorization: Bearer {access-token}` - Header-based requests do not require the Fetch API's `credentials` option unless the request also relies on cookies or HTTP authentication. An API call can potentially update the `access-token`, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for `st-access-token` These should be read and saved by you in the same way as it's being done during login. ###### Handling session refreshing If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. You can call the refresh API as follows: ```bash curl --location --request POST '/auth/session/refresh' \ --header 'authorization: Bearer {refresh-token}' ``` The result of a session refresh is either: - Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. - Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `st-refresh-token` and `st-access-token` that you saved on the frontend earlier. #### 1.5 Protect frontend routes You can use the `doesSessionExist` function to check if a session exists. ```tsx title="Web" option="install-method:npm" import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { if (await Session.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```tsx title="Mobile" option="mobile-frameworks:reactnative" import SuperTokens from "supertokens-react-native"; async function doesSessionExist() { if (await SuperTokens.doesSessionExist()) { // user is logged in } else { // user has not logged in yet } } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun doesSessionExist() { if (SuperTokens.doesSessionExist(this.applicationContext)) { // user is logged in } else { // user has not logged in yet } } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func doesSessionExist() { if SuperTokens.doesSessionExist() { // User is logged in } else { // User is not logged in } } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/supertokens.dart'; Future doesSessionExist() async { return await SuperTokens.doesSessionExist(); } ``` #### 1.6 Add a sign-out action The `signOut` method revokes the session on the frontend and on the backend. Calling this function without a valid session also yields a successful response. ```tsx title="Web" option="install-method:npm" import Session from "supertokens-web-js/recipe/session"; async function logout() { await Session.signOut(); window.location.href = "/auth"; // or to wherever your logic page is } ``` ```tsx title="Mobile" option="mobile-frameworks:reactnative" import SuperTokens from "supertokens-react-native"; async function logout() { await SuperTokens.signOut(); // navigate to the login screen.. } ``` ```kotlin title="Mobile" option="mobile-frameworks:android" import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun logout() { SuperTokens.signOut(this); // navigate to the login screen.. } } ``` ```swift title="Mobile" option="mobile-frameworks:ios" import UIKit import SuperTokensIOS fileprivate class ViewController: UIViewController { func signOut() { SuperTokens.signOut(completionHandler: { error in if error != nil { // handle error } else { // Signed out successfully } }) } } ``` ```dart title="Mobile" option="mobile-frameworks:flutter" import 'package:supertokens_flutter/supertokens.dart'; Future signOut() async { await SuperTokens.signOut( completionHandler: (error) { // handle error if any } ); } ``` - On success, the `signOut` function does not redirect the user to another page, so you must redirect the user yourself. - The `signOut` function calls the sign out API exposed by the session recipe on the backend. - If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, our SDKs do an automatic session refresh before revoking the session. ### 2. Integrate the backend SDK Let's go through the changes required so that your backend can expose the **SuperTokens** authentication features. 2.1 Install the backend SDK Run the following command in your terminal to install the package. ```bash title="Node.js" option="package-managers:npm" npm i -s supertokens-node ``` ```bash title="Node.js" option="package-managers:yarn" yarn add supertokens-node ``` ```bash title="Node.js" option="package-managers:pnpm" pnpm add supertokens-node ``` ```bash title="Node.js" option="package-managers:bun" bun add supertokens-node ``` ```bash title="Go" go get github.com/supertokens/supertokens-golang ``` ```bash title="Python" pip install supertokens-python ``` :::info[Official backend SDKs are available for **Node.js**, **Python**, and **Go**.] For other languages, create a separate authentication service. Our [other frameworks guide](/references/backend-sdks/other-frameworks) explains this approach. ::: #### 2.2 Initialize the backend SDK You will have to initialize the **Backend SDK** alongside the code that starts your server. The init call will include [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app, how the backend will connect to the **SuperTokens Core**, as well as the **Recipes** that will be used in your setup. ```tsx title="Node.js" option="node-frameworks:express" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "express", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:hapi" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "hapi", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:fastify" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "fastify", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:koa" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "koa", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```tsx title="Node.js" option="node-frameworks:loopback" import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ framework: "loopback", supertokens: { // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. connectionURI: "https://try.supertokens.io", // apiKey: }, appInfo: { // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration appName: "", apiDomain: "", websiteDomain: "", apiBasePath: "/auth", websiteBasePath: "/auth", }, recipeList: [ EmailPassword.init(), // initializes signin / sign up features Session.init(), // initializes session features ], }); ``` ```go title="Go" import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { apiBasePath := "/auth" websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ // We use try.supertokens for demo purposes. // At the end of the tutorial we will show you how to create // your own SuperTokens core instance and then update your config. ConnectionURI: "https://try.supertokens.io", // APIKey: }, AppInfo: supertokens.AppInfo{ AppName: "", APIDomain: "", WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ emailpassword.Init(nil), session.Init(nil), }, }) if err != nil { panic(err.Error()) } } ``` ```python title="Python" option="python-frameworks:fastapi" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='fastapi', recipe_list=[ session.init(), # initializes session features emailpassword.init() ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` ```python title="Python" option="python-frameworks:flask" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='flask', recipe_list=[ session.init(), # initializes session features emailpassword.init() ] ) ``` ```python title="Python" option="python-frameworks:django" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import emailpassword, session init( app_info=InputAppInfo( app_name="", api_domain="", website_domain="", api_base_path="/auth", website_base_path="/auth" ), supertokens_config=SupertokensConfig( # We use try.supertokens for demo purposes. # At the end of the tutorial we will show you how to create # your own SuperTokens core instance and then update your config. connection_uri="https://try.supertokens.io", # api_key: ), framework='django', recipe_list=[ session.init(), # initializes session features emailpassword.init() ], mode='asgi' # use wsgi if you are running django server in sync mode ) ``` :::info[Multiple frontend domains] To handle clients from different domains with the same SuperTokens instance, use the `origin` property in the `appInfo` object instead of `websiteDomain`. The property accepts a function that receives the original request as an input and should return a valid domain. Make sure to whitelist all the domains during CORS configuration. Keep in mind that with this setup, each frontend application will not share authentication sessions. Users will have to authenticate separately for each domain. To configure a shared authentication experience between multiple services check the [Unified Login](/authentication/unified-login/introduction) documentation. ::: #### 2.3 Add the SuperTokens APIs and configure CORS Now that the SDK is initialized you need to expose the endpoints that will be used by the frontend SDKs. Besides this, your server's CORS, Cross-Origin Resource Sharing, settings should be updated to allow the use of the authentication headers required by **SuperTokens**. Register the `plugin`. Register the `plugin`. Also register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody) plugin. :::note[Add the `middleware` BEFORE all your routes.] ::: :::note[Add the `middleware` BEFORE all your routes.] ::: Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. Use the `Middleware` (**BEFORE all your routes**) and the `get_all_cors_headers()` functions as shown below. - Use the `Middleware` (**BEFORE all your routes and after calling init function**) and the `get_all_cors_headers()` functions as shown below. - Add a route to catch all paths and return a 404. This is needed because if we don't add this, then OPTIONS request for the APIs exposed by the `Middleware` will return a `404`. Configure Django CORS Use the `Middleware` and the `get_all_cors_headers()` functions as shown below in your `settings.py`. ```tsx title="Node.js" option="node-frameworks:express" import express from "express"; import cors from "cors"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/express"; let app = express(); app.use( cors({ origin: "", allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }), ); // IMPORTANT: CORS should be before the below line. app.use(middleware()); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:hapi" import Hapi from "@hapi/hapi"; import supertokens from "supertokens-node"; import { plugin } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000, routes: { cors: { origin: [""], additionalHeaders: [...supertokens.getAllCORSHeaders()], credentials: true, }, }, }); (async () => { await server.register(plugin); await server.start(); })(); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:fastify" import cors from "@fastify/cors"; import supertokens from "supertokens-node"; import { plugin } from "supertokens-node/framework/fastify"; import formDataPlugin from "@fastify/formbody"; import fastifyImport from "fastify"; let fastify = fastifyImport(); // ...other middlewares fastify.register(cors, { origin: "", allowedHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()], credentials: true, }); (async () => { await fastify.register(formDataPlugin); await fastify.register(plugin); await fastify.listen({ port: 8000 }); })(); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:koa" import Koa from "koa"; import cors from "@koa/cors"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/koa"; let app = new Koa(); app.use( cors({ origin: "", allowHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }), ); app.use(middleware()); // ...your API routes ``` ```tsx title="Node.js" option="node-frameworks:loopback" import { RestApplication } from "@loopback/rest"; import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/loopback"; let app = new RestApplication({ rest: { cors: { origin: "", allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, }, }, }); app.middleware(middleware); // ...your API routes ``` ```go title="Go" option="go-frameworks:http" import ( "net/http" "strings" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... http.ListenAndServe("SERVER ADDRESS", corsMiddleware( supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // TODO: Handle your APIs.. })))) } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) { response.Header().Set("Access-Control-Allow-Origin", "") response.Header().Set("Access-Control-Allow-Credentials", "true") if r.Method == "OPTIONS" { // we add content-type + other headers used by SuperTokens response.Header().Set("Access-Control-Allow-Headers", strings.Join(append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...), ",")) response.Header().Set("Access-Control-Allow-Methods", "*") response.Write([]byte("")) } else { next.ServeHTTP(response, r) } }) } ``` ```go title="Go" option="go-frameworks:gin" import ( "net/http" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... router := gin.New() // CORS router.Use(cors.New(cors.Config{ AllowOrigins: []string{""}, AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "OPTIONS"}, AllowHeaders: append([]string{"content-type"}, supertokens.GetAllCORSHeaders()...), AllowCredentials: true, })) // Adding the SuperTokens middleware router.Use(func(c *gin.Context) { supertokens.Middleware(http.HandlerFunc( func(rw http.ResponseWriter, r *http.Request) { c.Next() })).ServeHTTP(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() }) // Add APIs and start server } ``` ```go title="Go" option="go-frameworks:chi" import ( "github.com/go-chi/chi" "github.com/go-chi/cors" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... r := chi.NewRouter() // CORS r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{""}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...), AllowCredentials: true, })) // SuperTokens Middleware r.Use(supertokens.Middleware) // Add APIs and start server } ``` ```go title="Go" option="go-frameworks:mux" import ( "net/http" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { // SuperTokens init... // TODO: Add APIs router := mux.NewRouter() // Adding handlers.CORS(options)(supertokens.Middleware(router))) http.ListenAndServe("SERVER ADDRESS", handlers.CORS( handlers.AllowedHeaders(append([]string{"Content-Type"}, supertokens.GetAllCORSHeaders()...)), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), handlers.AllowedOrigins([]string{""}), handlers.AllowCredentials(), )(supertokens.Middleware(router))) } ``` ```python title="Python" option="python-frameworks:fastapi" from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from supertokens_python import get_all_cors_headers from supertokens_python.framework.fastapi import get_middleware app = FastAPI() app.add_middleware(get_middleware()) # TODO: Add APIs app.add_middleware( CORSMiddleware, allow_origins=[ "" ], allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) # TODO: start server ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" from supertokens_python import get_all_cors_headers from flask import Flask, abort from flask_cors import CORS from supertokens_python.framework.flask import Middleware app = Flask(__name__) Middleware(app) # TODO: Add APIs CORS( app=app, origins=[ "" ], supports_credentials=True, allow_headers=["Content-Type"] + get_all_cors_headers(), ) # This is required since if this is not there, then OPTIONS requests for # the APIs exposed by the supertokens' Middleware will return a 404 @app.route('/', defaults={'u_path': ''}) @app.route('/') def catch_all(u_path: str): abort(404) # TODO: start server ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" from typing import List from corsheaders.defaults import default_headers from supertokens_python import get_all_cors_headers CORS_ORIGIN_WHITELIST = [ "" ] CORS_ALLOW_CREDENTIALS = True CORS_ALLOWED_ORIGINS = [ "" ] CORS_ALLOW_HEADERS: List[str] = list(default_headers) + [ "Content-Type" ] + get_all_cors_headers() INSTALLED_APPS = [ 'corsheaders', 'supertokens_python' ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ..., 'supertokens_python.framework.django.django_middleware.middleware', ] # TODO: start server ``` You can review all the endpoints that are added through the use of **SuperTokens** by visiting the [API Specs](https://app.swaggerhub.com/apis/supertokens/FDI). #### 2.4 Add the SuperTokens error handler Depending on the language and framework that you are using, you might need to add a custom error handler to your server. The handler will catch all the authentication related errors and return proper HTTP responses that can be parsed by the frontend SDKs. No additional `errorHandler` is required. Add the `errorHandler` **Before all your routes and plugin registration** No additional `errorHandler` is required. No additional `errorHandler` is required. :::info[You can skip this step] ::: :::info[You can skip this step] ::: ```tsx title="Node.js" option="node-frameworks:express" import express, { Request, Response, NextFunction } from "express"; import { errorHandler } from "supertokens-node/framework/express"; let app = express(); // ...your API routes // Add this AFTER all your routes app.use(errorHandler()); // your own error handler app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { /* ... */ }); ``` ```tsx title="Node.js" option="node-frameworks:fastify" import Fastify from "fastify"; import { errorHandler } from "supertokens-node/framework/fastify"; let fastify = Fastify(); fastify.setErrorHandler(errorHandler()); // ...your API routes ``` #### 2.5 Secure application routes Now that your server can authenticate users, the final step that you need to take care of is to prevent unauthorized access to certain parts of the application. For your APIs that require a user to be logged in, use the `verifySession` middleware. For your APIs that require a user to be logged in, use the `VerifySession` middleware. For your APIs that require a user to be logged in, use the `verify_session` middleware. ```tsx title="Node.js" option="node-frameworks:express" 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(), (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }); ``` ```tsx title="Node.js" option="node-frameworks:hapi" 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(), }, ], }, handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //... }, }); ``` ```tsx title="Node.js" option="node-frameworks:fastify" 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(), }, (req: SessionRequest, res) => { let userId = req.session!.getUserId(); //.... }, ); ``` ```tsx title="Node.js" option="node-frameworks:koa" 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(), (ctx: SessionContext, next) => { let userId = ctx.session!.getUserId(); //.... }); ``` ```tsx title="Node.js" option="node-frameworks:loopback" 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()) @response(200) handler() { let userId = (this.ctx as SessionContext).session!.getUserId(); //.... } } ``` ```go title="Go" option="go-frameworks:http" import ( "fmt" "net/http" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { // Wrap the API handler in session.VerifySession session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) }) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go title="Go" option="go-frameworks:gin" import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" ) func main() { router := gin.New() // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(nil), likeCommentAPI) } // This is a function that wraps the supertokens verification function // to work the 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 likeCommentAPI(c *gin.Context) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go title="Go" option="go-frameworks:chi" import ( "fmt" "net/http" "github.com/go-chi/chi" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { r := chi.NewRouter() // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```go title="Go" option="go-frameworks:mux" import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/supertokens/supertokens-golang/recipe/session" ) func main() { router := mux.NewRouter() // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) } func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) userID := sessionContainer.GetUserID() fmt.Println(userID) } ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:fastapi" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @app.post('/like_comment') async def like_comment(session: SessionContainer = Depends(verify_session())): user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @app.route('/update-jwt', methods=['POST']) @verify_session() def like_comment(): session: SessionContainer = g.supertokens user_id = session.get_user_id() print(user_id) ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" from typing import cast from django.http import HttpRequest from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session @verify_session() async def like_comment(request: HttpRequest): session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() print(user_id) ``` The middleware function returns a `401` to the frontend if a session doesn't exist, or if the access token has expired, in which case, our frontend SDK automatically refreshes the session. In case of successful session verification, you get access to a `session` object using which you can get the user's ID, or manipulate the session information. ### 3. Configure the Core Service If you have signed up and deployed a SuperTokens environment already, you can skip this step. Otherwise, please follow these instructions to use the correct **SuperTokens Core** instance in your application. The steps show you how to connect to a **SuperTokens Managed Service Environment**. If you want to self host the core instance please check the [following guide](/deployment/self-host-supertokens). #### 3.1 Sign up for a SuperTokens account Open this [page](https://supertokens.com/auth) in order to access the account creation page. Select the account that you want to use and wait for the action to complete. #### 3.2 Create a deployment After signing in, open the SuperTokens dashboard and select **Managed**. Enter a name for the deployment, select the region closest to your backend services, and click **Deploy Core**. Our internal service will deploy a separate environment based on your selection. After this process is complete, open the new deployment from the list. :::info[The initial setup flow only configures a development environment.] In order to use SuperTokens in production, you will have to create a separate deployment. ::: #### 3.3 Connect the backend SDK with SuperTokens In the SuperTokens dashboard, open the newly created deployment and select **Overview**. In **Connection Information**, copy the **Connection URI** and one of the **API Keys**, then use them as `connectionURI` and `apiKey` in your backend SDK configuration. If no suitable key exists, click **Generate Key** to create one. ```tsx title="Node.js" import supertokens from "supertokens-node"; supertokens.init({ supertokens: { connectionURI: "", apiKey: "", }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [], }); ``` ```go title="Go" import "github.com/supertokens/supertokens-golang/supertokens" func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "", APIKey: "", }, }) } ``` ```python check=false reason="Requires surrounding quickstart application context" title="Python" from supertokens_python import init, InputAppInfo, SupertokensConfig init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='', api_key='' ), framework='...', recipe_list=[ #... ] ) ``` ## Next steps Review this repository's SuperTokens integration for production readiness. Inspect Core deployment configuration, API keys, environment separation, HTTPS, secret handling, session security, CORS, cookies, email or SMS delivery, rate limits, logging, and error handling. Check that frontend and backend recipes match and that protected routes are actually protected. Run the relevant tests, typechecks, and build. Report findings by severity with file references, then make only safe fixes that are clearly required. Now that you have completed the quickstart, continue configuring SuperTokens for your application's authentication and authorization requirements. Add passwordless, social, enterprise, or machine-to-machine authentication. Verify user email addresses during sign-up. Add more authentication factors to your sign-in process. Configure session security, storage, and advanced workflows. Manage users through the SuperTokens Dashboard. Run SuperTokens as a managed service or inside your infrastructure. --- # References Source: https://supertokens.com/docs/references Discover the underlying concepts and the APIs exposed by **SuperTokens**. ## SDK and API References Read through detailed information on entities that you encounter while using **SuperTokens**. Information on how the frontend SDKs have a structure and how to use them. Information on the structure of the backend SDKs and how to use them. Details about the endpoints exposed by the **Frontend Driver Interface**. This is the API enabled by the backend SDKs. Details about the endpoints exposed by the **Core Driver Interface**. This is the API enabled by the SuperTokens Core Service. ## Advanced customisation See how you can fine-tune your authentication flow by using specific SDK features. --- # API Overrides Source: https://supertokens.com/docs/references/backend-sdks/api-overrides ## Overview Overriding APIs allows you to take full control of what happens when the frontend SDK calls the backend authentication endpoints. You can send analytics events, synchronize additional information in the database, or adjust the request input. --- ## General example Like with the [functions override](/references/backend-sdks/function-overrides) feature, the original implementation reference must be called to avoid any errors in the authentication flow. ```ts import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ Session.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, // only overriding the function that signs out a user signOutPOST: async function (input) { if (originalImplementation.signOutPOST === undefined) { throw Error("Should never come here"); } // TODO: some custom logic // or call the default behaviour as show below return await originalImplementation.signOutPOST(input); }, // ... // TODO: override more apis }; }, }, }), EmailPassword.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, emailExistsGET: async function (input) { // send a custom response like this: input.options.res.setStatusCode(200); // or any other status code input.options.res.sendJSONResponse({ message: "my custom response", //... }); // this return doesn't matter. But we must do it // cause the function signature expects a response. return { status: "OK", exists: false, }; }, }; }, }, }), ], }); ``` ```go import ( "encoding/json" "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ Override: &sessmodels.OverrideStruct{ APIs: func(originalImplementation sessmodels.APIInterface) sessmodels.APIInterface { // First we make a copy of the original implementation originalSignOutPOST := *originalImplementation.SignOutPOST // Then we override the default impl (*originalImplementation.SignOutPOST) = func(sessionContainer sessmodels.SessionContainer, options sessmodels.APIOptions, userContext supertokens.UserContext) (sessmodels.SignOutPOSTResponse, error) { // TODO: some custom logic // or call the default behaviour as show below return originalSignOutPOST(sessionContainer, options, userContext) } return originalImplementation }, }, }), emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { (*originalImplementation.EmailExistsGET) = func(email, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.EmailExistsGETResponse, error) { // create a custom response. options.Res.Header().Set("Content-Type", "application/json; charset=utf-8") options.Res.WriteHeader(200) responseJson := map[string]interface{}{ "message": "My custom response", // ... } bytes, _ := json.Marshal(responseJson) options.Res.Write(bytes) // this return doesn't matter. But we must do it // cause the function signature expects a response. return epmodels.EmailExistsGETResponse{ OK: &struct{ Exists bool }{ Exists: false, }, }, nil } return originalImplementation }, }, }), }, }) } ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe.emailpassword.interfaces import APIOptions as EmailPasswordAPIOptions, EmailExistsGetOkResult, APIInterface as EmailPasswordAPIInterface from supertokens_python.recipe.session.interfaces import APIOptions as SessionAPIOptions, APIInterface as SessionAPIInterface from supertokens_python.recipe import emailpassword from supertokens_python.recipe import session from typing import Dict, Any def override_emailpassword_apis(original_implementation: EmailPasswordAPIInterface): async def email_exists_get(email: str, tenant_id: str, api_options: EmailPasswordAPIOptions, user_context: Dict[str, Any]): # send custom response like this api_options.response.set_status_code(200) json_dict = {'message': 'Custom response'} api_options.response.set_json_content(json_dict) # this return doesn't matter. But we must do it # cause the function signature expects a response. return EmailExistsGetOkResult(False) original_implementation.email_exists_get = email_exists_get return original_implementation def override_session_apis(original_implementation: SessionAPIInterface): original_signout_post = original_implementation.signout_post async def signout_post( session: session.SessionContainer, api_options: SessionAPIOptions, user_context: Dict[str, Any], ): # TODO: custom logic # or call the default behaviour as show below return await original_signout_post(session, api_options, user_context) original_implementation.signout_post = signout_post 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(apis=override_session_apis) ), emailpassword.init(override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)) ], ) ``` --- ## Error management If you want to send a custom error message from the API override function, you can send a `GENERAL_ERROR` response. If you are using the pre-built UI, the response renders directly in the frontend UI. For custom UI, you can read this response and display the message in an error UI. The next example shows how to prevent the user from signing up unless their email is pre-approved by the application's admin. ```ts import EmailPassword from "supertokens-node/recipe/emailpassword"; EmailPassword.init({ override: { apis: (oI) => { return { ...oI, signUpPOST: async function (input) { let email = input.formFields.find((i) => i.id === "email")!.value as string; if (emailNotAllowed(email)) { return { status: "GENERAL_ERROR", message: "You are not allowed to sign up. Please contact the app's admin to get permission", }; } return oI.signUpPOST!(input); }, }; }, }, }); function emailNotAllowed(email: string) { // TODO: your impl to check if email is allowed or not return true; } ``` ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { originalSignUp := *originalImplementation.SignUpPOST (*originalImplementation.SignUpPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.SignUpPOSTResponse, error) { email := "" for _, v := range formFields { if v.ID == "email" { valueAsString, asStrOk := v.Value.(string) if !asStrOk { return epmodels.SignUpPOSTResponse{}, errors.New("Should never come here as we check the type during validation") } email = valueAsString } } if (emailNotAllowed(email)) { return epmodels.SignUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "You are not allowed to sign up. Please contact the app's admin to get permission", }, }, nil } return originalSignUp(formFields, tenantId, options, userContext) } return originalImplementation }, }, }) } func emailNotAllowed(email string) bool { // TODO: your impl to check email return true } ``` ```python from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, SignUpPostOkResult, EmailAlreadyExistsError, SignUpPostNotAllowedResponse, APIOptions, ) from supertokens_python.recipe.emailpassword.types import FormField from typing import Any, Dict, Union, List from supertokens_python.types import GeneralErrorResponse from supertokens_python.recipe.session import SessionContainer def override_apis(original_implementation: APIInterface): # copy the original implementation original_sign_up = original_implementation.sign_up_post async def sign_up( 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], ) -> Union[ SignUpPostOkResult, EmailAlreadyExistsError, SignUpPostNotAllowedResponse, GeneralErrorResponse, ]: email = "" for i in range(len(form_fields)): if form_fields[i].id == "email": email = form_fields[i].value if is_not_allowed(email): return GeneralErrorResponse( message="You are not allowed to sign up. Please contact the app's admin to get permission" ) return await original_sign_up( form_fields, tenant_id, session, should_try_linking_with_session_user, api_options, user_context, ) original_implementation.sign_up_post = sign_up return original_implementation def is_not_allowed(email: str): # TODO: your impl to check if the email is allowed return True emailpassword.init(override=emailpassword.InputOverrideConfig(apis=override_apis)) ``` --- ## Disable APIs To disable an API entirely, all you need to do is override the API implementation with `undefined`. For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: To disable an API entirely, all you need to do is override the API implementation with `nil`. For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: To disable an API entirely, all you need to do is override the API disable `bool` value to `True`. For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: ```tsx 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: { apis: (originalImplementation) => { return { ...originalImplementation, signInPOST: undefined, // disable sign in with email & password signUpPOST: undefined, // disable sign up with email & password }; }, }, }), ThirdParty.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: undefined, // disable sign in & up with third party }; }, }, }), ], }); ``` ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, supertokens: { connectionURI: "...", }, recipeList: [ ThirdParty.init({ override: { apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: undefined, }; }, }, }), ], }); ``` ```go 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{ APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { // disable sign in & up with third party originalImplementation.SignInUpPOST = nil return originalImplementation }, }, }), emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // disable sign in with email & password originalImplementation.SignInPOST = nil // disable sign up with email & password originalImplementation.SignUpPOST = nil return originalImplementation }, }, }), }, }) } ``` ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface as ThirdPartyAPIInterface, ) from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, ) def thirdparty_apis_override(original_impl: ThirdPartyAPIInterface): # disable sign in & up with third party original_impl.disable_sign_in_up_post = True return original_impl def emailpassword_apis_override(original_impl: EmailPasswordAPIInterface): # disable sign up with email & password original_impl.disable_sign_up_post = True # disable sign in with email & password original_impl.disable_sign_in_post = True return original_impl init( supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="fastapi", recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig(apis=thirdparty_apis_override), ), emailpassword.init( override=emailpassword.InputOverrideConfig( apis=emailpassword_apis_override ), ), ], ) ``` :::info[Important] You then need to define routes that handle this API call. You can see the [Frontend driver interface API spec here](/references/fdi/introduction) ::: --- ## Read custom request information The `getRequestFromUserContext` function provided by the SDK is used to get the request object from the user context. The `GetRequestFromUserContext` function provided by the SDK is used to get the request object from the user context. The `get_request_from_user_context` function provided by the SDK is used to get the request object from the user context. ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; Session.init({ override: { apis: (oI) => { return { ...oI, signOutPOST: async (input) => { if (oI.signOutPOST === undefined) { throw Error("Signout API is disabled"); } 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.signOutPOST(input); }, }; }, }, }); ``` ```go 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{ APIs: func(originalImplementation sessmodels.APIInterface) sessmodels.APIInterface { originalSignOutPost := *originalImplementation.SignOutPOST *originalImplementation.SignOutPOST = func(sessionContainer sessmodels.SessionContainer, options sessmodels.APIOptions, userContext supertokens.UserContext) (sessmodels.SignOutPOSTResponse, 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 originalSignOutPost(sessionContainer, options, userContext) } return originalImplementation }, }, }) } ``` ```python from supertokens_python import get_request_from_user_context from supertokens_python.recipe import session from supertokens_python.recipe.session.interfaces import APIInterface, APIOptions from typing import Any, Dict def override_session_apis(original_implementation: APIInterface): original_signout_post = original_implementation.signout_post async def signout_post(session: session.SessionContainer, api_options: APIOptions, 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_signout_post(session, api_options, user_context) original_implementation.signout_post = signout_post return original_implementation session.init( override=session.InputOverrideConfig( apis=override_session_apis ), ) ``` --- # Network interceptor Source: https://supertokens.com/docs/references/backend-sdks/backend-sdk-core-interceptor ## Overview This hook intercepts all outgoing requests from the backend SDK to the core. Capture and modify the request before sending it to the core. Users can modify the HTTP method, query params, headers, and body of the request. ## Prerequisites :::info[Important] This feature is only available for SDKs versions: - NodeJS >= `v16.5.0` - Python >= `v0.16.8` - GoLang >= `v0.6.6` ::: ## Example ```tsx import { HttpRequest } from "supertokens-node/types"; import SuperTokens from "supertokens-node"; SuperTokens.init({ supertokens: { connectionURI: "...", apiKey: "...", networkInterceptor: (request: HttpRequest, userContext: any) => { console.log("http request to core: ", request); // this can also be used to return a modified request object. return request; }, }, appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ // ... ], }); ``` ```go import ( "log" "net/http" "github.com/supertokens/supertokens-golang/supertokens" ) func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "...", APIKey: "...", NetworkInterceptor: func(request *http.Request, context supertokens.UserContext) (*http.Request, error) { log.Print("http request to core: %+v", request) return request, nil }, }, AppInfo: supertokens.AppInfo{ AppName: "...", APIDomain: "...", WebsiteDomain: "...", }, RecipeList: []supertokens.Recipe{/*...*/}, }) } ``` ```python from typing import Dict, Any, Optional from supertokens_python import init, InputAppInfo, SupertokensConfig def intercept( url: str, method: str, headers: Dict[str, Any], params: Optional[Dict[str, Any]], body: Optional[Dict[str, Any]], user_context: Optional[Dict[str, Any]], ): print("http request to core: ", url, method, headers, params, body) return url, method, headers, params, body init( app_info=InputAppInfo( app_name="...", api_domain="...", website_domain="...", ), supertokens_config=SupertokensConfig( connection_uri="...", api_key="...", network_interceptor=intercept, ), framework="django", # works with other frameworks as well recipe_list=[ # ... ], ) ``` --- # Function Overrides Source: https://supertokens.com/docs/references/backend-sdks/function-overrides ## 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. :::info See all the [functions that can be overridden here](https://supertokens.com/docs/references/backend-sdks/function-overrides) ::: :::info See all the [functions that can be overridden here](https://pkg.go.dev/github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels#RecipeInterface) ::: :::info See all the [functions that can be overridden here](https://supertokens.com/docs/references/backend-sdks/function-overrides) ::: ```tsx 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 }; }, }, }), ], }); ``` ```go 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 }, }, }), }, }) } ``` ```python 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 ```ts 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); }, }; }, }, }); ``` ```go 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 }, }, }) } ``` ```python 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 ```ts 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); }); ``` ```ts 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(); })(); ``` ```tsx 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 }); ``` ```ts check=false reason="AWS Lambda handler imports application-local backend configuration" 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; }); ``` ```ts 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()); ``` ```ts 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); ``` ```ts 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; } //... } ``` ```tsx 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() } else { throw exception; } } } ``` ```go 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 }, }) } ``` ```python 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) ``` ```python 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 ``` ```python # 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) ``` ```ts check=false reason="Next.js route imports application-local backend configuration" 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. ```tsx 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); }, }; }, }, }); ``` ```go 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 }, }, }) } ``` ```python 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, ), ) ``` --- # Supporting other backend frameworks Source: https://supertokens.com/docs/references/backend-sdks/other-frameworks If your backend framework is not supported by SuperTokens, you can follow this guide on setting up an authentication server to protect your frontend and backend. A key feature of the SuperTokens backend SDK is the middleware it adds to your backend server. This middleware adds authentication routes to handle requests like user creation and login. If your backend framework is not supported by SuperTokens, you need to run a separate server with the SuperTokens backend SDK configured. This could be a service in NodeJS, Python, or Golang. To illustrate how this setup works, consider an example: - The user has a React based frontend and a PHP backend server. - SuperTokens does not have a PHP SDK, and a separate service in NodeJS starts to act as an authentication server. The SuperTokens backend SDK configures this server to handle authentication requests and the issuing of access and refresh tokens. - The architecture of setup should have the following design: SuperTokens setup with an authentication server - In the architecture above the the React Frontend communicates with the PHP and node server through a reverse proxy. - Authentication requests like sign-up or sign-in route to the NodeJS server. These APIs are automatically created and handled by the `supertokens-node` SDK. On the other hand, your application-specific APIs are on the PHP server. - Once a user signs up or signs in, the system creates a session between your API server's domain and the frontend. - Application requests to the PHP server have session tokens attached to them. - The session token is a JWT, and the PHP server can [verify it](/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library). - When verifying a request, if the session token is missing or has expired, the PHP server should return a `401` response. This prompts the SuperTokens Frontend SDK to trigger the automatic refresh flow to generate new session tokens and retry the request. :::info[Important] The method mentioned above assumes that the authentication server is running on the same domain as the PHP server. If instead, your authentication server runs on a separate subdomain, you need to [enable cookie sharing](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints) for cookies to be automatically attached to requests to the PHP server. ::: --- # Reference Source: https://supertokens.com/docs/references/backend-sdks/reference import { PresentedOption } from "../../../components/option-presentation"; ## Overview SuperTokens has support for Node.js, Python, and Golang through its backend SDKs. Use this page to find references to each of them and about specific functionalities. ## Customization ## SDKs

## SDK configuration The `appInfo` object is the parameter used to configure the SDKs during initialization. ```ts check=false reason="configuration type excerpt references the SDK UserContext type" let appInfo: { appName: string; apiDomain: string; websiteDomain?: string; websiteBasePath?: string; origin?: ((input: { request?: Request; userContext: UserContext }) => string) | string; apiBasePath?: string; apiGatewayPath?: string; }; ``` This is the name of your application. Use it when sending password reset or email verification emails (in the default email design). An example of this is `appName: "GitHub"`.
This is the domain part of your website. This is where the login UI appears. For example: - For local development, you are likely using `localhost` with some port (ex `8080`). Then the value of this should be `"http://localhost:8080"`. - If your website is `https://www.example.com`, then the value of this should be `"https://www.example.com"`. - If your website is `https://example.com`, then the value of this should be `"https://example.com"`. - If you have multiple sub domains, and your users login via `https://auth.example.com`, then the value of this should be `"https://auth.example.com"`. By default, the login UI appears on `{websiteDomain}/auth/*`. You can change this by using the `websiteBasePath` configuration. On the frontend, you need the domain for routing purposes, and on the backend, it generates correct email verification and password reset links.
This is the domain part of your API endpoint that the frontend talks to. For example: - For local development, you are likely using `localhost` with some port (ex `9000`). Then the value of this should be `"http://localhost:9000"`. - If your frontend queries `https://api.example.com/*`, then the value of this should be `"https://api.example.com"` - If your API endpoint reaches `/api/*`, then the value of this is the same as the `websiteDomain` - since `/api/*` is equal to querying `{websiteDomain}/api/*`. By default, the login widgets query `{apiDomain}/auth/*`. You can change this by using the `apiBasePath` configuration.
By default, the login UI appears on `{websiteDomain}/auth`. Other authentication-related user interfaces appear on `{websiteDomain}/auth/*`. If you want to change the `/auth` to something else, then you must set this value. For example: - If you want the login UI to show on `{websiteDomain}/user/*`, then the value of this should be `"/user"`. - If you are using a dedicated sub domain for auth, like `https://auth.example.com`, then you probably want the login UI to show up on `https://auth.example.com`. In this case, set this value to `"/"`. :::note[Remember to set the same value for this parameter on the backend and the frontend.] :::
Can be used instead of `websiteDomain` to handle multiple frontend domains within the same SuperTokens instance. The property accepts a function that receives the original request as an input and should return a valid domain. Make sure to whitelist all the domains during CORS configuration.
By default, the frontend SDK queries `{apiDomain}/auth/*`. If you want to change the `/auth` to something else, then you must set this value. For example: - If you have versioning in your API path and want to query `{apiDomain}/v0/auth/*`, then the value of this should be `"/v0/auth"`. - If you want to scope the APIs not via `/auth` but via some other string like `/supertokens`, then you can set the value of this to `"/supertokens"`. This means, the APIs appear on `{apiDomain}/supertokens/*`. - If you do not want to scope the APIs at all, then you can set the values of this to be `"/"`. This means the APIs are available on `{apiDomain}/*` :::note[Remember to set the same value for this parameter on the backend and the frontend.] ::: :::warning[Note that setting a custom `apiBasePath` updates the refresh API path, this can cause an issue where previously issued refresh tokens no longer get sent to the new API endpoint and the user logs out.] For example, the default `apiBasePath` value is `/auth`, if it changes to `/supertokens`, then your refresh endpoint updates from `/auth/session/refresh` to `/supertokens/session/refresh`. Previously issued refresh tokens do not get sent to the new API endpoint and the user logs out. :::
:::note[Most relevant if you are using an API gateway or reverse proxy] ::: If you are using an API gateway (like the one provided by AWS) or a reverse proxy (like Nginx), it may add a path to your API endpoints to scope them for different development environments. For example, your APIs for development appear via `{apiDomain}/dev/*`, and for production, they may appear via `{apiDomain}/prod/*`. Whilst the frontend would need to use the `/dev/` and `/prod/`, your backend code would not see that sub path (that is `/dev/` and `/prod/` because the gateway removes them). For these situations, you should set the `apiGatewayPath` to `/dev` or `/prod`. For example: - If your API gateway is using `/development` for scoping, and you want to expose the SuperTokens APIs on `/supertokens/*`, then set `apiGatewayPath: "/development"` & `apiBasePath: "/supertokens"`. This means that the frontend SDK queries `{apiDomain}/development/supertokens/*` to reach the endpoints exposed by the service. - If you set this and not `apiBasePath`, then the frontend SDK queries `{apiDomain}{apiGatewayPath}/auth/*` to reach the endpoints exposed by the service. The reason for this distinction between `apiGatewayPath` and `apiBasePath` is that when routing, the backend SDK does not see the `apiGatewayPath` path from the request because the gateway removes them. Taking the above example, whilst the frontend queries `{apiDomain}/development/supertokens/*`, the backend SDK sees `{apiDomain}/supertokens/*`.
--- # User Context Source: https://supertokens.com/docs/references/backend-sdks/user-context ## 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 :::info[Important] This feature is only available for SDKs versions: - NodeJS >= `v9.0` - Python >= `v0.5` - GoLang >= `v0.5` ::: ## 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. ```tsx 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; }, }; }, }, }), ], }); ``` ```go 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 }, }, }), }, }) } ``` ```python 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`. ```tsx 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); }, }; }, }, }), ], }); ``` ```go 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 }, }, }), }, }) } ``` ```python 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. --- # User Object Source: https://supertokens.com/docs/references/backend-sdks/user-object ## Overview The user object represents the entity that exposes user information during an authentication flow. ## Prerequisites :::info[Important] This is only applicable for NodeJS SDK >= 16.0 and for Python SDK >= 0.25.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: ## Structure ### `User` | Property | Type | Description | |----------|------|-------------| | `id` | `string` | The primary user ID. It can change if the user links to another user. | | `timeJoined` | `number` | Time (in MS since epoch) when user first signed up. It does not update when new login methods appear. | | `isPrimaryUser` | `boolean` | `true` if the user can accept other login methods from other users. Important for account linking. | | `tenantIds` | `string[]` | List of tenantIds the user belongs to. Union of all login methods' tenantIds. | | `emails` | `string[]` | List of all emails associated with this user. | | `phoneNumbers` | `string[]` | List of all phone numbers associated with this user. | | `thirdParty` | `ThirdParty` | List of third party provider information. See Third Party Properties table below. | | `loginMethods` | `LoginMethod[]` | List of all login methods. See Login Method Properties table below. | ### `ThirdParty` | Property | Type | Description | |----------|------|-------------| | `id` | `string` | Unique identifier for the third party provider (for example, "google") | | `userId` | `string` | User ID from the third party provider | ### `LoginMethod` | Property | Type | Description | |----------|------|-------------| | `recipeId` | `string` | ID representing the login method (for example, "email password", "third party", "passwordless") | | `tenantIds` | `string[]` | List of tenantIds for this login method | | `timeJoined` | `number` | Time (in MS since epoch) when user signed up with this method | | `recipeUserId` | `RecipeUserId` | Recipe user ID for this login method | | `verified` | `boolean` | `true` if email/phone verifies for this method | | `email` | `string` | Email for this login method (undefined if not applicable) | | `phoneNumber` | `string` | Phone number for this method (undefined if not applicable) | | `thirdParty` | `ThirdParty` | Third party info for this method (undefined if not third party) | | `hasSameEmailAs` | `function` | Helper function to compare normalized emails | | `hasSamePhoneNumberAs` | `function` | Helper function to compare normalized phone numbers | | `hasSameThirdPartyInfoAs` | `function` | Helper function to compare third party info |
```ts type User = { id: string; timeJoined: number; isPrimaryUser: boolean; tenantIds: string[]; emails: string[]; phoneNumbers: string[]; thirdParty: { id: string; userId: string; }[]; loginMethods: { recipeId: "emailpassword" | "thirdparty" | "passwordless"; tenantIds: string[]; timeJoined: number; recipeUserId: RecipeUserId; verified: boolean; email?: string; phoneNumber?: string; thirdParty?: { id: string; userId: string; }; hasSameEmailAs: (email: string | undefined) => boolean; hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; }[]; }; class RecipeUserId { private recipeUserId: string; constructor(recipeUserId: string) { this.recipeUserId = recipeUserId; } public getAsString = () => { return this.recipeUserId; }; } ``` ## Primary and recipe user ID In SuperTokens, each user can have multiple login methods. For example, one user may be able to login with both, email password and social login. Each of these login methods gives the user a **unique user ID** - this is a `recipeUserId`. When the user logs in with either of the two methods, **SuperTokens** resolves a common ID. This user ID is the **primary user ID**. The value of the **primary user ID** is equal to the **recipe user ID** of the initial login method registered by the user. ### Example - A user first signs up with the email password recipe. - This gives them the **recipe user ID** `r1`. As well, their **primary user ID** is also `r1`. - They sign in with Google with the same email. - This creates a different **recipe user ID**, `r2`. - If you have enabled the automatic account linking, then the two recipe `userIds` link, and `r2`'s **primary user ID** becomes `r1`. - If you have not enabled automatic account linking, then you have two distinct users, with two different **primary user IDs**. ## Examples ### Email password user without account linking In a simple case, if there is a user who signed up with email password (with `test@example.com`), and automatic account linking is not enabled, their user object would look like: ```text { id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; timeJoined: 1693286254150; isPrimaryUser: false; tenantIds: ["public"]; emails: ["test@example.com"]; phoneNumbers: []; thirdParty: []; loginMethods: [{ recipeId: "emailpassword"; tenantIds: ["public"]; timeJoined: 1693286254150; recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); verified: false; email: "test@example.com"; hasSameEmailAs: (email: string | undefined) => boolean; hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; }]; }; ``` - Notice that the value of `isPrimaryUser` is `false`. This means that if this user links to another user, this user's primary user ID changes to the other user's primary user ID. - This user has one login method (email password), since it's not linked to any other user. ### Email password user linked with a social login user We have a user who signed up with email password (with `test@example.com`), and then with Google (with the same email). Automatic account linking is active, therefore these two users get linked. ```text { id: "3f23dca5-79da-4d84-9a72-90286ef6ea0d"; timeJoined: 1693286254150; isPrimaryUser: true; tenantIds: ["public"]; emails: ["test@example.com"]; phoneNumbers: []; thirdParty: [{ id: "google"; userId: "1234567890"; }]; loginMethods: [{ recipeId: "emailpassword"; tenantIds: ["public"]; timeJoined: 1693286254150; recipeUserId: new RecipeUserId("3f23dca5-79da-4d84-9a72-90286ef6ea0d"); verified: false; email: "test@example.com"; hasSameEmailAs: (email: string | undefined) => boolean; hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; }, { recipeId: "thirdparty"; tenantIds: ["public"]; timeJoined: 1693286254250; recipeUserId: new RecipeUserId("6ffc0ac5-d840-4a5b-92e8-86965f67c2ea"); verified: true; email: "test@example.com"; thirdParty: { id: "google"; userId: "1234567890"; }; hasSameEmailAs: (email: string | undefined) => boolean; hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; }]; }; ``` - Notice that the value of `isPrimaryUser` is `true`. This means that if this user links to another user, this user's primary user ID does not change. - This user has two login methods (email password and third party). The top level `timeJoined` value is the min of the two `timeJoined` values in the `loginMethods`. - In this example, the top level `emails` array has one item since both the login methods emails are the same, but if they were different, there would be two items in this array. --- # Core Driver Interface Source: https://supertokens.com/docs/references/cdi This is the API exposed by the SuperTokens Core. To be consumed by your backend only. `appid-{appId}` and `{tenantId}` in all the APIs are optional. Their default values are `appid-public` and `public` respectively. Those that do not have `{tenantId}` in the path will enforce that the API is called from `public` tenant only. ## Account Linking Recipe ## Totp Recipe ## UserIdMapping Recipe ## Passwordless Recipe ## EmailPassword Recipe ## ThirdParty Recipe ## EmailVerification Recipe ## User Metadata Recipe ## User Roles Recipe ## Session Recipe ## JWT Recipe ## Core ## Dashboard Recipe ## Multitenancy Recipe ## OAuth2Provider Recipe ## Bulk Import ## WebAuthn Recipe --- # Check primary user creation possibility Source: https://supertokens.com/docs/references/cdi/account-linking-recipe/cancreateprimaryuser Check if primary user can be created for given user id --- # Check account linking possibility Source: https://supertokens.com/docs/references/cdi/account-linking-recipe/canlinkaccounts Check if accounts can be linked for given primary and recipe user id --- # Create primary user account Source: https://supertokens.com/docs/references/cdi/account-linking-recipe/createprimaryuser Create a primary user for given user id --- # Link user accounts together Source: https://supertokens.com/docs/references/cdi/account-linking-recipe/linkaccounts Link accounts for given primary and recipe user id --- # Unlink user accounts Source: https://supertokens.com/docs/references/cdi/account-linking-recipe/unlinkaccounts Unlink accounts for given recipe user id --- # Add bulk import users Source: https://supertokens.com/docs/references/cdi/bulk-import/addbulkimportusers Add users for bulk import. Maximum 10000 users can be added in one request. --- # Count bulk import users Source: https://supertokens.com/docs/references/cdi/bulk-import/countbulkimportusers Count users in the bulk import processing queue, by status or all of them (by passing `null` status). --- # Delete bulk import users Source: https://supertokens.com/docs/references/cdi/bulk-import/deletebulkimportusers Delete bulk import users by id. Multiple ids can be passed in the request body. --- # List bulk import users Source: https://supertokens.com/docs/references/cdi/bulk-import/getbulkimportusers Paginated API to get bulk import users --- # Import one user directly Source: https://supertokens.com/docs/references/cdi/bulk-import/importoneuserwithbulkimport Import one user immediately with the Bulk Import functionality. --- # Delete hello message Source: https://supertokens.com/docs/references/cdi/core/deletehello Return a simple hello message --- # Delete license key Source: https://supertokens.com/docs/references/cdi/core/deletelicense --- # Delete user Source: https://supertokens.com/docs/references/cdi/core/deleteuser --- # Get active users count Source: https://supertokens.com/docs/references/cdi/core/getactiveuserscount Get number of active users. --- # Get API version Source: https://supertokens.com/docs/references/cdi/core/getapiversion Get a list of compatible CDI versions --- # Get config file path Source: https://supertokens.com/docs/references/cdi/core/getconfig Get path to the loaded config file --- # Get enterprise features Source: https://supertokens.com/docs/references/cdi/core/getfeatureflag Get a list of the enabled enterprise features --- # Get hello message Source: https://supertokens.com/docs/references/cdi/core/gethello Return a simple hello message --- # Get hello message on root path Source: https://supertokens.com/docs/references/cdi/core/gethelloonrootpath Return a simple hello message --- # Get license key Source: https://supertokens.com/docs/references/cdi/core/getlicense Retrieve license key --- # Get requests stats Source: https://supertokens.com/docs/references/cdi/core/getrequestsstats Get requests stats for last 24 hours `averageRequestsPerSecond` and `peakRequestsPerSecond` would countain `1440` values corresponding to `now - 1440 minutes` until `now - 1 minute`. A value of `-1` would mean that there is no data for that minute. --- # Get search tags Source: https://supertokens.com/docs/references/cdi/core/getsearchtags Retrieve available tags for search --- # Get telemetry ID Source: https://supertokens.com/docs/references/cdi/core/gettelemetry Returns the telemetryID if it exists --- # Get user ID Source: https://supertokens.com/docs/references/cdi/core/getuserid Get user id from email or phone number --- # Get users Source: https://supertokens.com/docs/references/cdi/core/getusers Get users. API is tenant specific if `includeAllTenants` is false. Else, `tenantId` is ignored. --- # Get users by account info Source: https://supertokens.com/docs/references/cdi/core/getusersbyaccountinfo Get users by email, phoneNumber or thirdParty info --- # Get users count Source: https://supertokens.com/docs/references/cdi/core/getuserscount Get number of users. API is tenant specific if `includeAllTenants` is false. Else, `tenantId` is ignored. --- # Get well-known JWT keys Source: https://supertokens.com/docs/references/cdi/core/getwellknownjwks Retrieve JWKs for JWT verification, containing both static and dynamic keys. --- # Post hello message Source: https://supertokens.com/docs/references/cdi/core/posthello Return a simple hello message --- # Put hello message Source: https://supertokens.com/docs/references/cdi/core/puthello Return a simple hello message --- # Set license key Source: https://supertokens.com/docs/references/cdi/core/setlicense Set or sync license key --- # Create dashboard user Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/createdashboarduser Create a dashboard user --- # Delete dashboard user Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/deletedashboarduser Delete a dashboard user with their userId or email --- # Get all dashboard users Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/getalldashboardusers Get a list of call the dashboard users --- # Get all sessions for dashboard user Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/getallsessionsfordashboarduser Get a list of the sessions for the dashboard user --- # Revoke dashboard user session Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/revokedashboarduserssession Revoke a Dashboard user's session --- # Sign in dashboard user Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/signindashboarduser Signin a Dashboard user --- # Get the core config Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/tenantcoreconfigfordashboardget Get the core config of the tenant specified by the url prefix, along with the metadata of each of the fields. --- # Update dashboard user Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/updatedashboarduser Update a user's email or password --- # Verify dashboard user session Source: https://supertokens.com/docs/references/cdi/dashboard-recipe/verifydashboardusersession Verify a Dashboard user's sessionId --- # Reset user password Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordgetpasswordreset Reset a password using password reset token --- # Generate password reset token Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordgetpasswordresettoken Generate a new reset password token for this user --- # Get email password user Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordgetuser Get a user's information API is tenant specific if querying by email. If querying by userId, tenantId will be ignored. --- # Update user info Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordputuser Update a user's information --- # Consume password reset token Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordresetpasswordconsumetoken Consume a password reset token --- # Sign in user Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordsignin Signin a user with email ID and password --- # Sign up user Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/emailpasswordsignup Signup a user with email ID and password --- # Import user with hash Source: https://supertokens.com/docs/references/cdi/emailpassword-recipe/userimport Import a user with email ID and password hash --- # Verify email Source: https://supertokens.com/docs/references/cdi/emailverification-recipe/emailverificationverify Verify an email --- # Check email verification Source: https://supertokens.com/docs/references/cdi/emailverification-recipe/emailverificationverifyget Check if an email is verified --- # Unverify email Source: https://supertokens.com/docs/references/cdi/emailverification-recipe/emailverificationverifyremove Unverify an email --- # Generate email verification token Source: https://supertokens.com/docs/references/cdi/emailverification-recipe/emailverificationverifytoken Generate a new email verification token for this user --- # Remove email verification tokens Source: https://supertokens.com/docs/references/cdi/emailverification-recipe/emailverificationverifytokenremove Remove all unused email verification tokens for this user --- # Introduction Source: https://supertokens.com/docs/references/cdi/introduction ## Overview The **CDI**, Core Driver Interface, is the API exposed by the **SuperTokens Core** service. It is meant to be consumed only by your backend only. :::info In most cases, you don't need to directly interact with the API, since the existing [backend SDKS](/references/backend-sdks/reference) are built on top of it. ::: ### URL Structure Most of the endpoints take in two path parameters: `appid-{appId}` and `{tenantId}`. Both are optional. If not set, the default app and tenant will be used. Given the following endpoint: `/appid-{appId}/{tenantId}/recipe/totp/device/verify`: - You can call it without the actual path parameters, using `/recipe/totp/device/verify` in your action. - You can set both values and end up with a path that looks like this: `/appid-myApp/myTenant/recipe/totp/device/verify`. ### Versioning At the moment, the documentation pages only show the latest version of the API. If you want to check an older release, please access the [Swagger page](https://app.swaggerhub.com/apis/supertokens/CDI) To know which version you should see: 1. Check the version of the core you are running (for managed service, visit the dashboard, else run `supertokens --version` command) 2. Go to the [**SuperTokens Core** GitHub page](https://github.com/supertokens/supertokens-core) 3. Switch to the branch that matches the version of the core your running 4. Open the file called `coreDriverInterfaceSupported.json` 5. In there, you see an array of `X.Y` values, pick the latest one, and see the API spec for that. ## Authentication The API uses JWT tokens for authentication. In order for the core service to accept a request you need to set the `api-key` header with the value of your token. If you are using the managed service, the token can be accessed from the **Connection Information** section on the deployment's **Overview** page in [the SaaS Dashboard](https://supertokens.com/dashboard). In the context of a self-hosted instance, keys are not created by default. You have to explicitly [generate them](/platform-configuration/supertokens-core/api-keys). --- # Create signed JWT Source: https://supertokens.com/docs/references/cdi/jwt-recipe/createsignedjwt Create a signed JWT --- # Get JWT keys Source: https://supertokens.com/docs/references/cdi/jwt-recipe/getjwks Retrieve JWKs for JWT verification, containing both static and dynamic keys. --- # Add user tenant association Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/addusertotenant Add user's association with the tenant. User will be added to the tenant based on the url prefix of the request. Note: To associate a user to a tenant, the tenant must be in the same database and user must already exist in the app --- # List apps (deprecated) Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/applistget This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still returns `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` values for backward compatibility, and it's values are derived from the `firstFactors` and `requiredSecondaryFactors` configured for the tenant. The detailed computation of the values as per CDI version is described here: https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 Get a list of all apps in a connection uri domain. Request must originate from public app and tenant. --- # List apps Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/applistv2get Get a list of all apps in a connection uri domain. The value of `firstFactors` can be as follows: - `null`: When set to `null`, the SDK will use firstFactors defined in the SDK - `[]` (empty array): No first factors would be enabled for the tenant - non-empty array: The first factors that are enabled for the tenant Request must originate from public app and tenant. --- # List connection uri domains (deprecated) Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/connectionuridomainlistget This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still returns `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` values for backward compatibility, and it's values are derived from the `firstFactors` and `requiredSecondaryFactors` configured for the tenant. The detailed computation of the values as per CDI version is described here: https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 Get a list of all connection uri domains. Request must originate from base connection uri domain and public app and tenant. --- # List connection uri domains Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/connectionuridomainlistv2get Get a list of all connection uri domains. The value of `firstFactors` can be as follows: - `null`: When set to `null`, the SDK will use firstFactors defined in the SDK - `[]` (empty array): No first factors would be enabled for the tenant - non-empty array: The first factors that are enabled for the tenant Request must originate from base connection uri domain and public app and tenant. --- # Upsert app (deprecated) Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdateappput This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still accepts those `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs for backward compatibility. Create or update an app. SuperTokens subscription license key is required. If creating a new app, only the login methods set to true will be enabled and rest will be disabled by default. `firstFactors` and `requiredSecondaryFactors` can be set to null to remove all entries in the core, or a non empty string array to be updated in the core. Setting of empty array is disallowed. Note: the create/update will fail if a login method is not enabled and a relavant factor is added to either `firstFactors` or `requiredSecondaryFactors`. For example, `emailPasswordEnabled` cannot be set to `false` if `emailpassword` is present in the `firstFactors` array. If updating an existing app, 1. core will keep the existing state of login methods and only update the ones that are specified in the request body. 2. Core config will be merged into existing config. To delete a key in the config, use a null value Note: the newly created app will use the same connection uri domain from which this request originates and the request must originate from public app and public tenant. Note: Updation of core config is not allowed for the default connectionUriDomain, public app. In order to update config for the default connectionUriDomain and public app, you must edit the config.yaml or the docker env directly. --- # Upsert app Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdateappv2put Create or update an app. SuperTokens subscription license key is required. The `firstFactors` can be set to following values: - `null`: When set to `null`, all the login methods will be available for the app (public tenant). - `[]` (empty array): No login methods will be enabled for the app (public tenant). - `['factor1', 'factor2', ...]` (non-empty array): The list of login methods to enable for the app (public tenant). For eg. if this is set to `['emailpassword', 'thirdparty']`, the users of the tenant will be able to login using emailpassword and third party providers. List of built-in first factors are as follows: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` If first factors are not specified while creating a new app, all the login methods will be enabled by default. The `requiredSecondaryFactors` can be set to following values: - `null`: When set to `null`, no secondary factors will be required for the users of the app (public tenant). - `['factor1', 'factor2', ...]` (non-empty array): The list of factors that the users of the app (public tenant) must complete post the first factor login. For eg. if this is set to `['otp-phone', 'totp']`, the users of the tenant will be required to complete either phone OTP or TOTP post the first factor login. List of built-in secondary factors are as follows: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` - Time based OTP: `totp` If updating an existing app, 1. core will keep the existing state of login methods and only update the ones that are specified in the request body. 2. Core config will be merged into existing config. To delete a key in the config, use a null value Note: the newly created app will use the same connection uri domain from which this request originates and the request must originate from public app and public tenant. Note: Updation of core config is not allowed for the default connectionUriDomain, public app. In order to update config for the default connectionUriDomain and public app, you must edit the config.yaml or the docker env directly. --- # Upsert connection URI domain Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdateconnectionuridomainput This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still accepts those `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs for backward compatibility. Create or update a connection uri domain. SuperTokens subscription license key is required, if not using the base connection uri domain. This request must originate from public app and public tenant on the base connection uri domain. If creating a new connection uri domain, only the login methods set to true will be enabled and rest will be disabled by default. `firstFactors` and `requiredSecondaryFactors` can be set to null to remove all entries in the core, or a non empty string array to be updated in the core. Setting of empty array is disallowed. Note: the create/update will fail if a login method is not enabled and a relavant factor is added to either `firstFactors` or `requiredSecondaryFactors`. For example, `emailPasswordEnabled` cannot be set to `false` if `emailpassword` is present in the `firstFactors` array. If updating an existing connection uri domain, 1. core will keep the existing state of login methods and only update the ones that are specified in the request body. 2. Core config will be merged into existing config. To delete a key in the config, use a null value Note: The core config must contain a unique connection to the storage layer, because sharing of database between 2 different connection uri domains is not allowed. Note: Updation of core config is not allowed for the default connectionUriDomain. In order to update config for the default connectionUriDomain, you must edit the config.yaml or the docker env directly. --- # Upsert connection uri domain Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdateconnectionuridomainv2put Create or update a connection uri domain. SuperTokens subscription license key is required, if not using the base connection uri domain. This request must originate from public app and public tenant on the base connection uri domain. The `firstFactors` can be set to following values: - `null`: When set to `null`, all the login methods will be available for the connection URI domain (public app, public tenant). - `[]` (empty array): No login methods will be enabled for the connection URI domain (public app, public tenant). - `['factor1', 'factor2', ...]` (non-empty array): The list of login methods to enable for the connection URI domain (public app, public tenant). For eg. if this is set to `['emailpassword', 'thirdparty']`, the users of the tenant will be able to login using emailpassword and third party providers. List of built-in first factors are as follows: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` If first factors are not specified while creating a new connection uri domain, all the login methods will be enabled by default. The `requiredSecondaryFactors` can be set to following values: - `null`: When set to `null`, no secondary factors will be required for the users of the connection URI domain (public app, public tenant). - `['factor1', 'factor2', ...]` (non-empty array): The list of factors that the users of the connection URI domain (public app, public tenant) must complete post the first factor login. For eg. if this is set to `['otp-phone', 'totp']`, the users of the tenant will be required to complete either phone OTP or TOTP post the first factor login. List of built-in secondary factors are as follows: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` - Time based OTP: `totp` If updating an existing connection uri domain, 1. core will keep the existing state of login methods and only update the ones that are specified in the request body. 2. Core config will be merged into existing config. To delete a key in the config, use a null value Note: The core config must contain a unique connection to the storage layer, because sharing of database between 2 different connection uri domains is not allowed. Note: Updation of core config is not allowed for the default connectionUriDomain. In order to update config for the default connectionUriDomain, you must edit the config.yaml or the docker env directly. --- # Upsert ThirdParty Provider configuration Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdatetenantconfigput Creates or updates ThirdParty Provider Config for a tenant. If `skipValidation` is set to true, the config will be saved without any validation. If the config already exists for the given `tenantId` and `thirdPartyId`, the config in the core will be completely replaced with the config provided in the request body. **Validations done by the core:** `thirdPartyId` and `name` are required Common to all providers: - `clients` list: - can be undefined or empty - if contains 1 element, clientType can be empty or undefined - if contains more than 1 element, clientType must be defined and unique - for each element in `clients`: - `clientId` must not be empty Built-in provider's specific validation is invoked if the `thirdPartyId` starts with the provider's id Apple (id: apple): - `clients` - if it contains elements, each of them are validated as follows: - `clientSecret` must be empty or undefined - `additionalConfig` should contain the following keys: - `keyId` must be a non empty string - `teamId` must be a non empty string - `privateKey` must be a non empty string Google Workspaces (id: google-workspaces): - `clients` - if it contains elements, each of them are validated as follows: - `additionalConfig` may contain the key `hd` - `hd` is optional - if specified, it must be either `"*"`, or a valid domain Boxy SAML (id: boxy-saml): - `clients` - `additionalConfig` in the each element must contain `boxyURL` and must be non-empty string --- # Upsert tenant (deprecated) Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdatetenantput This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still accepts those `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs for backward compatibility. Creates or updates a tenant. SuperTokens subscription license key is required. If creating a new tenant, only the login methods set to true will be enabled and rest will be disabled by default. `firstFactors` and `requiredSecondaryFactors` can be set to null to remove all entries in the core, or a non empty string array to be updated in the core. Setting of empty array is disallowed. Note: the create/update will fail if a login method is not enabled and a relavant factor is added to either `firstFactors` or `requiredSecondaryFactors`. For example, `emailPasswordEnabled` cannot be set to `false` if `emailpassword` is present in the `firstFactors` array. If updating an existing tenant, 1. core will keep the existing state of login methods and only update the ones that are specified in the request body. 2. Core config will be merged into existing config. To delete a key in the config, use a null value The request must originate from public tenant, and the new tenant will use connectionUriDomain and app from which the request originates. Note: Updation of core config is not allowed for the default connectionUriDomain, public app and tenant. In order to update config for the default connectionUriDomain, public app and tenant, you must edit the config.yaml or the docker env directly. --- # Upsert tenant Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/createorupdatetenantv2put Creates or updates a tenant. SuperTokens subscription license key is required. The `firstFactors` can be set to following values: - `null`: When set to `null`, all the login methods will be available for the tenant. - `[]` (empty array): No login methods will be enabled for the tenant. - `['factor1', 'factor2', ...]` (non-empty array): The list of login methods to enable for the tenant. For eg. if this is set to `['emailpassword', 'thirdparty']`, the users of the tenant will be able to login using emailpassword and third party providers. List of built-in first factors are as follows: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` If first factors are not specified while creating a new tenant, all the login methods will be disabled by default. The `requiredSecondaryFactors` can be set to following values: - `null`: When set to `null`, no secondary factors will be required for the users of the tenant. - `['factor1', 'factor2', ...]` (non-empty array): The list of factors that the users of the tenant must complete post the first factor login. For eg. if this is set to `['otp-phone', 'totp']`, the users of the tenant will be required to complete either phone OTP or TOTP post the first factor login. List of built-in secondary factors are as follows: - Email password auth: `emailpassword` - Social login / enterprise SSO auth: `thirdparty` - Passwordless: - With email OTP: `otp-email` - With SMS OTP: `otp-phone` - With email magic link: `link-email` - With SMS magic link: `link-phone` - Time based OTP: `totp` If updating an existing tenant, 1. core will keep the existing state of login methods and only update the ones that are specified in the request body. 2. Core config will be merged into existing config. To delete a key in the config, use a null value The request must originate from public tenant, and the new tenant will use connectionUriDomain and app from which the request originates. Note: Updation of core config is not allowed for the default connectionUriDomain, public app and tenant. In order to update config for the default connectionUriDomain, public app and tenant, you must edit the config.yaml or the docker env directly. --- # Delete app Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/deleteapppost Delete an app. Request must originate from public app and tenant. Note: No tenants (other than the public tenant) must belong to the app to be able to delete it. --- # Remove connection uri domain Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/deleteconnectionuridomainpost Delete a connection uri domain. Deletion of base connection uri domain is not allowed. The request must originate from base connection uri domain and public app and tenant. Note: There should be no apps or tenants (other than the public app and public tenant) belonging to the connection uri domain to be able to delete it. --- # Delete ThirdParty Provider configuration Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/deletetenantconfigpost Delete ThirdParty Provider configuration for a tenant. --- # Delete a tenant Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/deletetenantpost Delete a tenant. Request must originate from public tenant. --- # Remove user tenant association Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/removeuserfromtenant Remove user's association with the tenant User will be removed from the tenant based on the url prefix of the request. --- # Get tenant configuration (deprecated) Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/tenantconfigget This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still returns `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` values for backward compatibility, and it's values are derived from the `firstFactors` and `requiredSecondaryFactors` configured for the tenant. The detailed computation of the values as per CDI version is described here: https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 Get a tenant config of the tenant specified by the url prefix. SuperTokens subscription license key is required if querying any tenant other than the base tenant. Returns recipes with their enabled flag and recipe specific configs saved in core --- # Get tenant configuration Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/tenantconfigv2get Get a tenant config of the tenant specified by the url prefix. The value of `firstFactors` can be as follows: - `null`: When set to `null`, the SDK will use firstFactors defined in the SDK - `[]` (empty array): No first factors would be enabled for the tenant - non-empty array: The first factors that are enabled for the tenant SuperTokens subscription license key is required if querying any tenant other than the base tenant. Returns recipes with their enabled flag and recipe specific configs saved in core --- # List tenants in an app (deprecated) Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/tenantlistget This API is deprecated. Please use the v2 version of this API. In the v2 version of the API, the login methods are no longer enabled using the `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` inputs. Instead, they are enabled using factorIds (such as emailpassword, otp-email, etc) specified in the `firstFactors` and `requiredSecondaryFactors` inputs. Please refer [Multitenancy Docs](https://supertokens.com/docs/multitenancy/new-tenant) to know the list of factorIds available. Note: This deprecated API still returns `emailPasswordEnabled`, `thirdPartyEnabled` and `passwordlessEnabled` values for backward compatibility, and it's values are derived from the `firstFactors` and `requiredSecondaryFactors` configured for the tenant. The detailed computation of the values as per CDI version is described here: https://github.com/supertokens/supertokens-core/issues/979#issuecomment-2099971371 Get a list of all tenants in an app. Request must originate from public tenant. --- # List tenants in an app Source: https://supertokens.com/docs/references/cdi/multitenancy-recipe/tenantlistv2get Get a list of all tenants in an app. The value of `firstFactors` can be as follows: - `null`: When set to `null`, the SDK will use firstFactors defined in the SDK - `[]` (empty array): No first factors would be enabled for the tenant - non-empty array: The first factors that are enabled for the tenant Request must originate from public tenant. --- # Accept OAuth2 Consent Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/acceptoauth2consentrequest --- # Accept OAuth2 Login Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/acceptoauth2loginrequest --- # Accept OAuth2 Logout Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/acceptoauth2logoutrequest --- # Create OAuth2 Client Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/createoauth2client --- # Get OAuth2 Auth Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/getoauth2auth --- # Get OAuth2 Client Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/getoauth2client --- # Get OAuth2 Consent Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/getoauth2consentrequest --- # Get OAuth2 Login Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/getoauth2loginrequest --- # Get OAuth2 Sessions Logout Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/getoauth2sessionslogout --- # Get OAuth2 Token Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/getoauth2token --- # Introspect OAuth2 Token Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/introspectoauth2token --- # List OAuth2 Clients Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/listoauth2clients --- # Reject OAuth2 Consent Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/rejectoauth2consentrequest --- # Reject OAuth2 Login Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/rejectoauth2loginrequest --- # Reject OAuth2 Logout Request Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/rejectoauth2logoutrequest --- # Remove OAuth2 Client Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/removeoauth2client --- # Revoke OAuth2 Session Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/revokeoauth2session --- # Revoke OAuth2 Token Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/revokeoauth2token --- # Revoke OAuth2 tokens for a client Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/revokeoauth2tokens Revoke OAuth2 Token --- # Update OAuth2 Client Source: https://supertokens.com/docs/references/cdi/oauth2provider-recipe/updateoauth2client --- # Check passwordless code Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlesscheckcode Tries to check if the passed userInputCode+deviceId combo or the linkCode is valid --- # Get passwordless user Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlessgetuser Get a user's information. API is tenant specific if querying by email or phone number. If querying by userId, tenantId will be ignored. Note that there is an invisible character at the end of the path, this was to avoid a conflict with the OpenAPI spec. --- # List passwordless codes Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlesslistcodes Lists all active passwordless codes of the user --- # Update passwordless user Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlessputuser Update a user's information. If the email or phoneNumber is set to null the previous value will be deleted. If they are not set (i.e., undefined/missing from the request), they are left as-is. --- # Revoke all user codes Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlessrevokeallcodesofuser Revokes all codes issued for the user --- # Revoke passwordless code Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlessrevokecode Revokes a code by id --- # Start passwordless sign in Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlessstartsignin Starts a sign in process by requesting a linkCode and a deviceId + userInputCode combination the user can use to sign in. Passing the optional deviceId signifies a resend code flow. --- # Consume passwordless code Source: https://supertokens.com/docs/references/cdi/passwordless-recipe/passwordlesstryusecode Tries to consume the passed userInputCode+deviceId combo or the linkCode to sign the user in --- # Create new session Source: https://supertokens.com/docs/references/cdi/session-recipe/createnewsession Create a new Session --- # Delete session Source: https://supertokens.com/docs/references/cdi/session-recipe/deletesession Delete a sesion If revoking session by `userId`, the sessions are cleared across all tenants by default. Note: If `revokeAcrossAllTenants` is set to `true`, this API can only be called from `public` tenant. --- # Get JWT data Source: https://supertokens.com/docs/references/cdi/session-recipe/getjwtdata Get JWT data for a session --- # Get session data Source: https://supertokens.com/docs/references/cdi/session-recipe/getsessiondata --- # Get session info Source: https://supertokens.com/docs/references/cdi/session-recipe/getsessioninfo Get user and session information for a given session handle --- # Get user session handles Source: https://supertokens.com/docs/references/cdi/session-recipe/getusersessionhandles Get session handles for a user By default, the session handles are fetched across all tenants. Set `fetchAcrossAllTenants` to `false` to get sessionHandles for the user for a particular tenant. Note: If `fetchAcrossAllTenants` is set to `true`, this API can only be called from `public` tenant. --- # Update JWT data Source: https://supertokens.com/docs/references/cdi/session-recipe/putjwtdata Change JWT data for a session --- # Update session data Source: https://supertokens.com/docs/references/cdi/session-recipe/putsessiondata Change session data --- # Refresh session Source: https://supertokens.com/docs/references/cdi/session-recipe/refreshsession Refresh a Session --- # Regenerate session Source: https://supertokens.com/docs/references/cdi/session-recipe/regeneratesession Regenerate a session --- # Verify session Source: https://supertokens.com/docs/references/cdi/session-recipe/verifysession Verify a Session --- # Get third party user Source: https://supertokens.com/docs/references/cdi/thirdparty-recipe/thirdpartygetuser Get a user's information. API is tenant specific if querying by email. If querying by userId, tenantId will be ignored. Note that there is an invisible character at the end of the path, this was to avoid a conflict with the OpenAPI spec. --- # Get users by email Source: https://supertokens.com/docs/references/cdi/thirdparty-recipe/thirdpartygetusersbyemail Get all users accounts associated with given email --- # Sign in/up third party user Source: https://supertokens.com/docs/references/cdi/thirdparty-recipe/thirdpartysigninup Signin/up a user --- # Add TOTP device for user Source: https://supertokens.com/docs/references/cdi/totp-recipe/createtotpdevice Add a TOTP device for a user and enable TOTP if not already enabled. --- # List user TOTP devices Source: https://supertokens.com/docs/references/cdi/totp-recipe/gettotpdevices Retrieve a list of TOTP devices for a user. --- # Import existing TOTP device Source: https://supertokens.com/docs/references/cdi/totp-recipe/importtotpdevice Add a TOTP device for a user and enable TOTP if not already enabled. --- # Remove TOTP device Source: https://supertokens.com/docs/references/cdi/totp-recipe/removetotpdevice Remove a TOTP device for a user. If all devices are removed, TOTP is disabled for the user. --- # Update TOTP device name Source: https://supertokens.com/docs/references/cdi/totp-recipe/updatetotpdevicename Update the name of a TOTP device for a user. --- # Verify TOTP code Source: https://supertokens.com/docs/references/cdi/totp-recipe/verifytotpcode Check if a TOTP code is valid against any of the TOTP devices for a user. --- # Verify TOTP device Source: https://supertokens.com/docs/references/cdi/totp-recipe/verifytotpdevice Mark a TOTP device as verified if the given TOTP code is valid for that device. --- # Remove user metadata Source: https://supertokens.com/docs/references/cdi/user-metadata-recipe/usermetadatadelete Removes the entire metadata JSON stored about the user. --- # Get user metadata Source: https://supertokens.com/docs/references/cdi/user-metadata-recipe/usermetadataread Gets the stored metadata object of the user --- # Update user metadata Source: https://supertokens.com/docs/references/cdi/user-metadata-recipe/usermetadataupdate Updates the metadata object stored about the user by doing a shallow merge of the stored and the update JSONs and removing properties set to null on the root level of the update object. The merged object is then reserialized and stored. e.g.: - stored: `{ "preferences": { "theme":"dark" }, "notifications": { "email": true }, "todos": ["example"] }` - update: `{ "notifications": { "sms": true }, "todos": null }` - result: `{ "preferences": { "theme":"dark" }, "notifications": { "sms": true } }` --- # Add user role Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/adduserrole Creates a User Role mapping --- # Get permission roles Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/getpermissionroles Retrive the roles associated with the permission --- # Get role permissions Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/getrolepermissions Retrive the permissions associated with a role --- # Get all roles Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/getroles Retrive all created roles --- # Get users with role Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/getroleusers Retrive the users associated with the role. --- # Get user roles Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/getuserroles Retrive the roles associated with the user. --- # Create or update role Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/putrole Creates a role with permissions, can also be used to add permissions to a role --- # Delete role Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/removerole Deletes a role --- # Remove role permissions Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/removerolepermissions Removes permissions mapped to a role, if no permissions are passed all permissions mapped to the role are removed --- # Remove user role Source: https://supertokens.com/docs/references/cdi/user-roles-recipe/removeuserrole Removes a User Role mapping --- # Create user ID mapping Source: https://supertokens.com/docs/references/cdi/useridmapping-recipe/useridmappingcreatemapping Create a mapping between a SuperTokens userId and an external userId. --- # Get user ID mapping Source: https://supertokens.com/docs/references/cdi/useridmapping-recipe/useridmappinggetmapping Retrieve a UserIdMapping --- # Remove user ID mapping Source: https://supertokens.com/docs/references/cdi/useridmapping-recipe/useridmappingremovemapping Delete a mapping between a SuperTokens userId and an external userId. --- # Update external user info Source: https://supertokens.com/docs/references/cdi/useridmapping-recipe/useridmappingupateexternaluseridinfo Update or delete externalUserIdInfo --- # Consume recovery token Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/consumewebauthntoken Consume the token to recover the user. --- # Generate registration options Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/generateregistrationoptions Generate the webauthn options for registration. --- # Generate authentication options Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/generatesigninoptions Generate the webauthn options for signin. --- # Generate recovery token Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/generatetokenforrecovery Generate a token to recover the user. --- # Get WebAuthn credential Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/getwebauthncredential Get the WebAuthn credential for the user. --- # Get WebAuthn options Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/getwebauthnoptions Get the WebAuthn options. --- # List WebAuthn credentials Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/listwebauthncredentials List credentials that were created by the user. --- # Recover WebAuthn user Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/recoverwebauthnuser Recover the user using the WebAuthn credential. --- # Register WebAuthn credential Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/registerwebauthncredential Register a new credential for the user --- # Remove WebAuthn credential Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/removewebauthncredential Remove the WebAuthn credential for the user. --- # Remove WebAuthn options Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/removewebauthnoptions Remove the WebAuthn options. --- # Sign in WebAuthn user Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/signinwebauthnuser Sign in the user using the WebAuthn credential. --- # Sign up WebAuthn user Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/signupwebauthnuser Sign up the user using the WebAuthn credential. --- # Update email Source: https://supertokens.com/docs/references/cdi/webauthn-recipe/updateemail Update the email of the user. --- # Frontend Driver Interface Source: https://supertokens.com/docs/references/fdi These are the APIs exposed by our backend SDK. To be consumed by the frontend only. `{tenantId}` in all the APIs are optional. Its default value is `public` ## MultiFactorAuth Recipe ## TOTP Recipe ## Passwordless Recipe ## Session Recipe ## EmailPassword Recipe ## ThirdParty Recipe ## Multitenancy Recipe ## EmailVerification Recipe ## JWT Recipe ## OpenId Recipe ## OAuth2Provider Recipe ## App API ## WebAuthn Recipe --- # Test authentication Source: https://supertokens.com/docs/references/fdi/app-api/exampleappapi Use this endpoint to check if your request are authenticated properly. --- # Check email exists Source: https://supertokens.com/docs/references/fdi/emailpassword-recipe/emailexists Check if an email exists --- # Check email exists (deprecated) Source: https://supertokens.com/docs/references/fdi/emailpassword-recipe/emailexistsdepr Check if an email exists --- # Reset user password Source: https://supertokens.com/docs/references/fdi/emailpassword-recipe/passwordreset Reset a password using password reset token --- # Generate password reset token Source: https://supertokens.com/docs/references/fdi/emailpassword-recipe/passwordresettoken Generate a new reset password token for this user --- # Sign in with email Source: https://supertokens.com/docs/references/fdi/emailpassword-recipe/signin Signin a user with email ID and password --- # Sign up with email Source: https://supertokens.com/docs/references/fdi/emailpassword-recipe/signup Signup a user with email ID and password --- # Check email verification status Source: https://supertokens.com/docs/references/fdi/emailverification-recipe/getverifyemail Checks if an email is verified and adds this information into the access token payload as well --- # Verify email address Source: https://supertokens.com/docs/references/fdi/emailverification-recipe/verifyemail Verify an email --- # Send email verification Source: https://supertokens.com/docs/references/fdi/emailverification-recipe/verifyemailtoken Send email verification email --- # Introduction Source: https://supertokens.com/docs/references/fdi/introduction ## Overview The **FDI**, Frontend Driver Interface, is the API exposed by the **SuperTokens Backend SDKs**. It is meant to be consumed only by your frontend applications. :::info In most cases, you don't need to directly interact with the API, since the existing [frontend SDKS](/references/backend-sdks/reference) are built on top of it. If you are using something that does not have SDK support, then you can make use of the FDI resources. ::: ### URL Structure All the endpoints are prefixed with `{apiBasePath}`. This is the property with the same name that you set during [backend SDK initialization](/quickstart#22-initialize-the-backend-sdk). Tenant specific actions include a `{tenantId}` parameter. If not set, the default tenant will be used. Given the following endpoint: `/{apiBasePath}/{tenantId}/signinup`, and the `apiBasePath` set to `auth` during [initialization](/quickstart#22-initialize-the-backend-sdk): - You can call it without the actual parameter, using `/auth/signinup` in your action. - You can set the values to target a specific tenant: `/auth/myTenant/signinup`. ### Versioning At the moment, the documentation pages only show the latest version of the API. If you want to check an older release, please access the [Swagger page](https://app.swaggerhub.com/apis/supertokens/FDI) To know which version you should see: 1. Go to the GitHub page of the backend or frontend SDK you are using 2. Switch to the branch that matches the version of the SDK 3. Open the file called `frontendDriverInterfaceSupported.json` 4. In there, you see an array of `X.Y` values, pick the latest one, and see the API spec for that. ## Authentication Since this API allows users to sign up or login, most of the endpoints do not require any type of credentials. However, in some situations you have to authenticate your requests. In those cases, you need to choose between using cookies or headers. The method that you have to use depends on the [token transfer method](/post-authentication/session-management/switch-between-cookies-and-header-authentication) used in your application. --- # Get JWT keys Source: https://supertokens.com/docs/references/fdi/jwt-recipe/getjwks Get all JSON web keys for JWT verification --- # Get MFA factors information Source: https://supertokens.com/docs/references/fdi/multifactorauth-recipe/getmfainfo Returns information about the auth factors of the current user and refreshes the related session claim --- # Get enabled login methods Source: https://supertokens.com/docs/references/fdi/multitenancy-recipe/loginmethods Get enabled login methods: Returns recipes with their enabled setting and recipe specific configuration. --- # Start OAuth login Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthauthget Starts the OAuth2 login flow - for a detailed description of all input parameters please see the OAuth2 and OpenID Connect Core specs --- # End session redirect Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthendsessionget Redirects the user to a page where they can log out and revoke the oauth tokens --- # End OAuth session Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthendsessionpost Redirects the user to a page where they can log out and revoke the oauth tokens - for a detailed description of input parameters please see the user initiated logout spec --- # Introspect OAuth token Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthintrospectpost Introspects an access/refresh token --- # Continue OAuth login Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthloginget Continues the OAuth2 login flow after the login page --- # Get OAuth login info Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthlogininfoget Retrieves information about the OAuth2 login --- # Logout OAuth user Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthlogoutpost Logs out the user and revokes the access/refresh tokens based on the id_token_hint passed to the end_session endpoint --- # Revoke OAuth token Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthrevokepost Revokes an access/refresh token - the client id and secret can also be provided in an authorization header using the Basic scheme --- # Exchange OAuth grant Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthtokenpost Exchanges an OAuth2 grant (e.g.: authorization code) for an access token (and optionally a refresh/id token) - for a detailed description of all input parameters please see the OAuth2 and OpenID Connect Core specs --- # Get OAuth user info Source: https://supertokens.com/docs/references/fdi/oauth2provider-recipe/oauthuserinfoget Retrieves user information based on the access token passed in the authorization header --- # Get OpenID config Source: https://supertokens.com/docs/references/fdi/openid-recipe/getopeniddiscoveryconfiguration Get OpenID discovery configuration --- # Check email exists Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlessemailexists Check if an email exists --- # Check email exists (deprecated) Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlessemailexistsdepr Check if an email exists Note that there is an invisible character at the end of the path, this was to avoid a conflict with the OpenAPI spec. --- # Check phone exists Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlessphonenumberexists Check if a phone number exists --- # Check phone exists (deprecated) Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlessphonenumberexistsdepr Check if a phone number exists --- # Complete passwordless sign in/up Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlesssigninupconsume Finish sign in/up process with passwordless --- # Resend passwordless code Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlesssigninupresend --- # Start passwordless sign in/up Source: https://supertokens.com/docs/references/fdi/passwordless-recipe/passwordlesssigninupstart Start sign in/up process with passwordless --- # Refresh user session Source: https://supertokens.com/docs/references/fdi/session-recipe/refresh Refresh the user session --- # Sign out user Source: https://supertokens.com/docs/references/fdi/session-recipe/signout Logout user --- # Get third party auth URL Source: https://supertokens.com/docs/references/fdi/thirdparty-recipe/authorisationurl Get the thirdparty provider's authorisation URL to which the user should be redirected to. --- # Sign in/up with third party Source: https://supertokens.com/docs/references/fdi/thirdparty-recipe/signinup Signin/up a user --- # Handle Apple sign in Source: https://supertokens.com/docs/references/fdi/thirdparty-recipe/thirdpartycallbackapple Handles sign in with the apple. --- # Create TOTP device Source: https://supertokens.com/docs/references/fdi/totp-recipe/createtotpdevice Creates an unverified totp device --- # List TOTP devices Source: https://supertokens.com/docs/references/fdi/totp-recipe/listtotpdevices List the TOTP devices of the current user --- # Remove TOTP device Source: https://supertokens.com/docs/references/fdi/totp-recipe/removetotpdevice Removes a totp device --- # Verify TOTP code Source: https://supertokens.com/docs/references/fdi/totp-recipe/verifytotp Checks that the TOTP sent in the body belongs to a verified totp device of the session user --- # Verify TOTP device Source: https://supertokens.com/docs/references/fdi/totp-recipe/verifytotpdevice Checks that the TOTP sent in the body belongs to the totp device (specified by deviceName, belonging to the session user) --- # Check WebAuthn email exists Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnemailexists Check if a WebAuthn email exists --- # Generate WebAuthn recovery token Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthngeneraterecoveraccounttoken Generate a recovery token for a WebAuthn account --- # Recover WebAuthn account Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnrecoveraccount Recover a WebAuthn account --- # Register WebAuthn credential Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnregistercredential Register a new WebAuthn credential for an existing user --- # Get WebAuthn registration options Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnregisteroptions Get WebAuthn registration options for a user --- # Sign in with WebAuthn Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnsignin Sign in a user with WebAuthn --- # Get WebAuthn sign in options Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnsigninoptions --- # Sign up with WebAuthn Source: https://supertokens.com/docs/references/fdi/webauthn-recipe/webauthnsignup Sign up a user with WebAuthn --- # Function Overrides Source: https://supertokens.com/docs/references/frontend-sdks/function-overrides ## 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 your custom session management. Similarly, if you already have a sign-in/sign-up flow and want to integrate with SuperTokens, overriding allows you to handle the migration process. You can even implement your own `userId` format by mapping your `userIds` to those generated by SuperTokens. ## Before you start This page is relevant if you are using the actual frontend SDK. If you are calling the backend SDK endpoints directly this code does not run in your use case. ## Example The code snippet shows the general flow of overriding a function. You inject your own custom logic while also calling the original implementation of the function. :::info[The next examples include a couple of override samples.] See all the [functions that can be overridden here](https://supertokens.com/docs/references/frontend-sdks/function-overrides) ::: :::info See all the [functions that can be overridden here](https://supertokens.com/docs/references/frontend-sdks/function-overrides) ::: ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ Session.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding the function for checking // if a session exists doesSessionExist: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.doesSessionExist(input); }, }; }, }, }), EmailPassword.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign up button. signUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signUp(input); }, // ... // TODO: override more functions }; }, }, }), ThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign in or sign up button. signInAndUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signInAndUp(input); }, // ... // TODO: override more functions }; }, }, }), ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUISession.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding the function for checking // if a session exists doesSessionExist: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.doesSessionExist(input); }, }; }, }, }), supertokensUIEmailPassword.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign up button. signUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signUp(input); }, // ... // TODO: override more functions }; }, }, }), supertokensUIThirdParty.init({ override: { functions: (originalImplementation) => { return { ...originalImplementation, // we will only be overriding what happens when a user // clicks the sign in or sign up button. signInAndUp: async function (input) { // TODO: some custom logic // or call the default behaviour as show below return originalImplementation.signInAndUp(input); }, // ... // TODO: override more functions }; }, }, }), ], }); ``` --- # Hooks Source: https://supertokens.com/docs/references/frontend-sdks/hooks ## Overview Hooks are a way to trigger custom logic when certain actions happen in the authentication process. --- ## Handle event hook Each frontend recipe emits events when certain actions happen. You can use this hook to trigger side effects when something happens in the authentication process. This can address things like logging or analytics. ```tsx import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; EmailPassword.init({ onHandleEvent: (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { } else if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during step up or second factor auth with email password } } }, }); ThirdParty.init({ onHandleEvent: (context) => { if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during linking a social account to an existing account } } }, }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIEmailPassword.init({ onHandleEvent: (context) => { if (context.action === "PASSWORD_RESET_SUCCESSFUL") { } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { } else if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during step up or second factor auth with email password } } }, }); supertokensUIThirdParty.init({ onHandleEvent: (context) => { if (context.action === "SUCCESS") { if (context.createdNewSession) { let user = context.user; if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { // sign up success } else { // sign in success } } else { // during linking a social account to an existing account } } }, }); ``` :::warning[Not applicable since you need to build custom UI anyway.] When you call the functions from the SDK, or call the API directly, you can run custom logic in your own code. ::: --- ## Pre-API hook This function calls the backend before any API call. You can use this to change the request properties. ```tsx import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; ThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); EmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); supertokensUIEmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); ``` ```tsx import ThirdParty from "supertokens-web-js/recipe/thirdparty"; import EmailPassword from "supertokens-web-js/recipe/emailpassword"; EmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); ThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); ``` ```tsx check=false reason="script-tag installation provides SuperTokens globals at runtime" supertokensEmailPassword.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "EMAIL_EXISTS") { } else if (action === "EMAIL_PASSWORD_SIGN_IN") { } else if (action === "EMAIL_PASSWORD_SIGN_UP") { } else if (action === "SEND_RESET_PASSWORD_EMAIL") { } else if (action === "SUBMIT_NEW_PASSWORD") { } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); supertokensThirdParty.init({ preAPIHook: async (context) => { let url = context.url; let requestInit = context.requestInit; let action = context.action; if (action === "GET_AUTHORISATION_URL") { } else if (action === "THIRD_PARTY_SIGN_IN_UP") { // Note: this could either be sign in or sign up. // we don't know that at the time of the API call // since all we have is the authorisation code from // the social provider } // events such as sign out are in the // session recipe pre API hook (See the info box below) return { requestInit, url, }; }, }); ``` ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ apiDomain: "...", preAPIHook: async (context) => { let requestInit = context.requestInit; if (context.action === "REFRESH_SESSION") { requestInit.headers = { ...requestInit.headers, customHeader: "custom-header", }; } else if (context.action === "SIGN_OUT") { requestInit.headers = { ...requestInit.headers, customHeader: "custom-header", }; } return { ...context, requestInit, }; }, }); ``` ```kotlin import android.app.Application import com.supertokens.session.CustomHeaderProvider import com.supertokens.session.SuperTokens class MainApplication : Application() { override fun onCreate() { super.onCreate() SuperTokens.Builder(applicationContext, "...").customHeaderProvider(object : CustomHeaderProvider { override fun getRequestHeaders(requestType: CustomHeaderProvider.RequestType?): MutableMap { var headers: MutableMap = HashMap() if (requestType == CustomHeaderProvider.RequestType.REFRESH) { headers["custom-header"] = "custom-value" } else if (requestType == CustomHeaderProvider.RequestType.SIGN_OUT) { headers["custom-header"] = "custom-value" } return headers } }).build() } } ``` ```swift import UIKit import SuperTokensIOS fileprivate class MyApplicationDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "...", preAPIHook: { action, request in let mutableRequest = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest if action == .REFRESH_SESSION { mutableRequest.addValue("custom-value", forHTTPHeaderField: "custom-header") } if action == .SIGN_OUT { mutableRequest.addValue("custom-value", forHTTPHeaderField: "custom-header") } return mutableRequest.copy() as! URLRequest } ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error } catch { // Some other error } return true } } ``` ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( apiDomain: "...", preAPIHook: (action, req) { if (action == APIAction.SIGN_OUT) { req.headers["custom-header"] = "custom-value"; } else if (action == APIAction.REFRESH_TOKEN) { req.headers["custom-header"] = "custom-value"; } return req; }, ); } ```
Alternatively you could also declare the pre-API hook when calling the function:
```tsx import EmailPassword from "supertokens-web-js/recipe/emailpassword"; EmailPassword.doesEmailExist({ email: "...", options: { preAPIHook: async (input) => { let url = input.url; let requestInit = input.requestInit; // TODO: add your code here return { url, requestInit }; }, }, }); ``` ```tsx check=false reason="script-tag installation provides SuperTokens globals at runtime" supertokensEmailPassword.doesEmailExist({ email: "...", options: { preAPIHook: async (input) => { let url = input.url; let requestInit = input.requestInit; return { url, requestInit }; }, }, }); ```
--- ## Redirection callback hook Use this function to change where the system redirects the user after certain actions. For example, you can use this to redirect a user to a specific URL post sign in or sign up. If you're embedding the UI components in a popup and wish to disable redirection entirely, return `null`. ```tsx import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { appName: "SuperTokens", apiDomain: "http://localhost:3000", websiteDomain: "http://localhost:3000", }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } if (context.createdNewUser) { // user signed up return "/onboarding"; } else { // user signed in return "/dashboard"; } } else if (context.action === "TO_AUTH") { // called when the user is not authenticated and needs to be redirected to the auth page. return "/auth"; } // return undefined to let the default behaviour play out return undefined; }, recipeList: [ EmailPassword.init({ getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { // called when the user clicked on the forgot password button } // return undefined to let the default behaviour play out return undefined; }, }), ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { appName: "SuperTokens", apiDomain: "http://localhost:3000", websiteDomain: "http://localhost:3000", }, getRedirectionURL: async (context) => { if (context.action === "SUCCESS" && context.newSessionCreated) { // called on a successful sign in / up. Where should the user go next? let redirectToPath = context.redirectToPath; if (redirectToPath !== undefined) { // we are navigating back to where the user was before they authenticated return redirectToPath; } if (context.createdNewUser) { // user signed up return "/onboarding"; } else { // user signed in return "/dashboard"; } } else if (context.action === "TO_AUTH") { // called when the user is not authenticated and needs to be redirected to the auth page. return "/auth"; } // return undefined to let the default behaviour play out return undefined; }, recipeList: [ supertokensUIEmailPassword.init({ getRedirectionURL: async (context) => { if (context.action === "RESET_PASSWORD") { // called when the user clicked on the forgot password button } // return undefined to let the default behaviour play out return undefined; }, }), ], }); ``` :::warning[Not applicable since you need to build custom UI anyway.] When you call the functions from the SDK, or call the API directly, you can run custom logic in your own code. ::: --- # Change colours Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/changing-colours ## Overview You can update the default theme with your colors to make it fit with your website. Define a few CSS variables in the `style` property to the `EmailPassword.init` call. Specify the colors as RGB (see the following example), because the `rgb` and `rgba` functions apply them. For example, if your website uses a dark theme, here is how you can customize it: ## Before you start :::warning This example is relevant only if you use the prebuilt UI components. ::: ## Example ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=container] { --palette-background: 51, 51, 51; --palette-inputBackground: 41, 41, 41; --palette-inputBorder: 41, 41, 41; --palette-textTitle: 255, 255, 255; --palette-textLabel: 255, 255, 255; --palette-textPrimary: 255, 255, 255; --palette-error: 173, 46, 46; --palette-textInput: 169, 169, 169; --palette-textLink: 114,114,114; --palette-textGray: 158, 158, 158; } `, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=container] { --palette-background: 51, 51, 51; --palette-inputBackground: 41, 41, 41; --palette-inputBorder: 41, 41, 41; --palette-textTitle: 255, 255, 255; --palette-textLabel: 255, 255, 255; --palette-textPrimary: 255, 255, 255; --palette-error: 173, 46, 46; --palette-textInput: 169, 169, 169; --palette-textLink: 114,114,114; --palette-textGray: 158, 158, 158; } `, recipeList: [ /* ... */ ], }); ``` Prebuilt form UI with custom color palette :::note Changes to the palette apply to all the UI components provided. If you want to change a specific component, please see [this section](changing-style). ::: ### Palette values | Variable Name | Description | Default Value | |--------------|-------------|---------------| | `background` | Background color of all forms | `255, 255, 255` (white) | | `inputBackground` | Background color of input fields | `250, 250, 250` (light grey) | | `inputBorder` | Border color of input fields | `224, 224, 224` (light grey) | | `primary` | Primary color for focused inputs, success states and button backgrounds | `28, 34, 42` | | `primaryBorder` | Border color for primary buttons | `45, 54, 68` | | `success` | Color used for success events | `65, 167, 0` (green) | | `successBackground` | Background color for success notifications | `217, 255, 191` (green) | | `error` | Color for error highlights and messages | `255, 23, 23` (red) | | `errorBackground` | Background color for error notifications | `255, 241, 235` (red) | | `textTitle` | Color of form titles | `0, 0, 0` (black) | | `textLabel` | Color of form field labels | `0, 0, 0` (black) | | `textInput` | Color of text in form fields | `0, 0, 0` (black) | | `textPrimary` | Color of subtitles and footer text | `128, 128, 128` (grey) | | `textLink` | Color of links | `0, 122, 255` (blue) | | `buttonText` | Color of text in main buttons | `255, 255, 255` (white) | | `superTokensBrandingBackground` | Color of SuperTokens branding element | `242, 245, 246` (Alice blue) | | `superTokensBrandingText` | Color of "Powered by SuperTokens" text | `173, 189, 196` (heather grey) | --- # Change styles via CSS Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/changing-style ## Overview Updating the CSS allows you to change the UI of the components to meet your needs. This section guides you through an example of updating the look of buttons. Note that the process can update any HTML tag from within SuperTokens components. ## Before you start :::warning This example is relevant only if you use the prebuilt UI components. ::: --- ## Global style changes First, open the website at `/auth`. The Sign-in widget should show up. Use the browser console to find out the class name that you'd like to overwrite. Inspecting submit button in prebuilt form Highlighting attribute for customization Each stylable component contains `data-supertokens` attributes (in this example `data-supertokens="button"`). Let's customize elements with the `button` attribute. The syntax for styling is plain CSS. ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } `, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } `, recipeList: [ /* ... */ ], }); ``` The above results in: Prebuilt form with custom submit button ### Changing fonts By default, SuperTokens uses the `Arial` font. The best way to override this is to add a `font-family` styling to the `container` component in the recipe configuration. ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=container] { font-family: cursive; } `, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=container] { font-family: cursive; } `, recipeList: [ /* ... */ ], }); ``` ### Using media queries You may want to have different CSS for different `viewports`. This can happen via media queries like this: ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } @media (max-width: 440px) { [data-supertokens~=button] { width: 90%; } } `, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; width: 30%; margin: 0 auto; } @media (max-width: 440px) { [data-supertokens~=button] { width: 90%; } } `, recipeList: [ /* ... */ ], }); ``` --- ## Customize the sign up and sign in forms These are the screens shown when the user tries to log in or sign up for the application. ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: `[data-supertokens~=authPage] { ... }`, recipeList: [ /* ... */ ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, style: `[data-supertokens~=authPage] { ... }`, recipeList: [ /* ... */ ], }); ``` --- ## Customize the password reset forms ### Send password reset email form This form appears when the user clicks on "forgot password" in the sign in form. ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ resetPasswordUsingTokenFeature: { enterEmailForm: { style: ` ... `, }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ resetPasswordUsingTokenFeature: { enterEmailForm: { style: ` ... `, }, }, }), supertokensUISession.init(), ], }); ``` ### Submit new password form This screen appears when the user clicks the password reset link on their email - to enter their new password ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ EmailPassword.init({ resetPasswordUsingTokenFeature: { submitNewPasswordForm: { style: ` ... `, }, }, }), Session.init(), ], }); ``` ```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) supertokensUIInit({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, recipeList: [ supertokensUIEmailPassword.init({ resetPasswordUsingTokenFeature: { submitNewPasswordForm: { style: ` ... `, }, }, }), supertokensUISession.init(), ], }); ``` --- # Embed the authentication form in a page Source: https://supertokens.com/docs/references/frontend-sdks/prebuilt-ui/embed-sign-in-up-form ## Before you start :::warning This example is relevant only if you use the React SDK with prebuilt UI components. ::: --- ## Render the form in a page The following example shows the scenario where you have a dedicated route, such as `/auth`, for rendering the Auth Widget. Upon a successful login, the user automatically redirects to the return value of `getRedirectionURL` (defaulting to `/`). ```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { AuthPage } from "supertokens-auth-react/ui"; import Header from "./header"; import Footer from "./footer"; import { useNavigate } from "react-router-dom"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "TO_AUTH") { return "/auth"; // return the path where you are rendering the Auth UI } else if (context.action === "SUCCESS" && context.newSessionCreated) { return "/dashboard"; // defaults to "/" } }, disableAuthRoute: true, recipeList: [ /* ... */ ], }); function MyAuthPage() { const navigate = useNavigate(); return (
); } ```
```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { AuthPage } from "supertokens-auth-react/ui"; import Header from "./header"; import Footer from "./footer"; import { useHistory } from "react-router-dom5"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "TO_AUTH") { return "/auth"; // return the path where you are rendering the Auth UI } else if (context.action === "SUCCESS" && context.newSessionCreated) { return "/dashboard"; // defaults to "/" } }, disableAuthRoute: true, recipeList: [ /* ... */ ], }); function MyAuthPage() { const history = useHistory(); return (
); } ```
```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { AuthPage } from "supertokens-auth-react/ui"; import Header from "./header"; import Footer from "./footer"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, getRedirectionURL: async (context) => { if (context.action === "TO_AUTH") { return "/auth"; // return the path where you are rendering the Auth UI } else if (context.action === "SUCCESS" && context.newSessionCreated) { return "/dashboard"; // defaults to "/" } }, disableAuthRoute: true, recipeList: [ /* ... */ ], }); function MyAuthPage() { return (
); } ```
In the above code snippet: 1. Disabled the default Auth UI by setting `disableAuthRoute` to `true`. 2. Override the `getRedirectionURL` function inside the SuperTokens configuration to redirect to `/auth` when login becomes necessary and to redirect to `/dashboard` upon successful login. Feel free to customize the redirection URLs as needed. :::note[When the user visits the `/auth` page, they see the SignIn UI by default. To render the SignUp UI, append `show=signup` as a query parameter to the URL, like`/auth?show=signup`.] ::: --- ## Render the form in a page with no redirection The following example shows the scenario where you have a dedicated route, such as `/auth`, for rendering the Auth Widget. However, upon a successful login, the user sees a logged in UI instead of getting redirected. ```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { AuthPage } from "supertokens-auth-react/ui"; import Session from "supertokens-auth-react/recipe/session"; import Header from "./header"; import Footer from "./footer"; import { useNavigate } from "react-router-dom"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "...", }, disableAuthRoute: true, recipeList: [ /* ... */ ], getRedirectionURL: async (context) => { if (context.action === "SUCCESS") { return null; // this will not navigate the user away after successful login } }, }); function LandingPage() { let sessionContext = Session.useSessionContext(); const navigate = useNavigate(); if (sessionContext.loading) { return null; } if (sessionContext.doesSessionExist) { // We wrap this with so that // all claims are validated before showing the logged in UI. // For example, if email verification is switched on, and // the user's email is not verified, then // will redirect to the email verification page. return (
You are logged in!