File length: 10371 # Authentication - Social Login - 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. From a third-party login perspective, you need to maintain an allow list of emails and validate the users based on 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](/docs/quickstart/introduction). ### Prerequisites This guide uses the `UserMetadata` recipe to store the allow list. You need to [enable it](/docs/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps ### 1. Implement the allow list You can store this list in your own database, or use the metadata feature provided by SuperTokens to store this. This may seem like a strange use case of the user metadata recipe provided, but it works. The following code samples show you how to save the allow list in the user metadata. ```tsx async function addEmailToAllowlist(email: string) { let existingData = await UserMetadata.getUserMetadata("emailAllowList"); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, email]; await UserMetadata.updateUserMetadata("emailAllowList", { allowList }); } async function isEmailAllowed(email: string) { let existingData = await UserMetadata.getUserMetadata("emailAllowList"); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(email); } ``` ```go ::: ### 2. Check if the email is allowed 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 declare let isEmailAllowed: (email: string) => Promise // REMOVE_FROM_OUTPUT 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 that the email is new and is a sign up if (!(await isEmailAllowed(input.email))) { // email is not in allow list, so we disallow throw new Error("No sign up") } } // We allow the sign in / up operation return originalImplementation.signInUp(input); } } }, apis: (originalImplementation) => { return { ...originalImplementation, signInUpPOST: async function (input) { try { return await originalImplementation.signInUpPOST!(input); } catch (err: any) { if (err.message === "No sign up") { // this error was thrown from our function override above. // so we send a useful message to the user return { status: "GENERAL_ERROR", message: "Sign ups are disabled. Please contact the admin." } } throw err; } } } } } }) ``` ```go return false, nil } func main() { thirdparty.Init(&tpmodels.TypeInput{ Override: &tpmodels.OverrideStruct{ Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface { ogThirdPartySignInUp := *originalImplementation.SignInUp (*originalImplementation.SignInUp) = func(thirdPartyID, thirdPartyUserID, email string, oAuthTokens map[string]interface{}, rawUserInfoFromProvider tpmodels.TypeRawUserInfoFromProvider, tenantId string, userContext supertokens.UserContext) (tpmodels.SignInUpResponse, error) { existingUsers, err := thirdparty.GetUsersByEmail(tenantId, email) if err != nil { return tpmodels.SignInUpResponse{}, err } if len(existingUsers) == 0 { // this means that the email is new and is a sign up allowed, err := isEmailAllowed(email) if err != nil { return tpmodels.SignInUpResponse{}, err } if !allowed { return tpmodels.SignInUpResponse{}, errors.New("No sign up") } } // We allow the sign in / up operation return ogThirdPartySignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext) } 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 supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) { resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext) if err.Error() == "No sign up" { // 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: "Sign ups are disabled. Please contact the admin.", }, }, nil } return resp, err } return originalImplementation }, }, }) } ``` ```python 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 from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput async def is_email_allowed(email: str): # from previous code snippet.. return False 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(email=email) ) if len(existing_users) == 0: if not await is_email_allowed(email): raise Exception("No sign up") # this means this email is new so we allow sign up 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, ) raise Exception("No sign up") 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], ): 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) == "No sign up": return GeneralErrorResponse( "Seems like you already have an account with another method. Please use that instead." ) raise e 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="...", # type: ignore recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig( apis=override_thirdparty_apis, functions=override_thirdparty_functions ), ) ], ) ```