File length: 11642 # Authentication - Passwordless - Passwordless login via allow list 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](/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. Add a way to keep track of allowed emails or phone numbers Start by maintaining an allow list of emails. You can either 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); } async function addPhoneNumberToAllowlist(phoneNumber: string) { let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList"); let allowList: string[] = existingData.metadata.allowList || []; allowList = [...allowList, phoneNumber]; await UserMetadata.updateUserMetadata("phoneNumberAllowList", { allowList }); } async function isPhoneNumberAllowed(phoneNumber: string) { let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList"); let allowList: string[] = existingData.metadata.allowList || []; return allowList.includes(phoneNumber); } ``` ```go ::: ### 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 declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output declare let isPhoneNumberAllowed: (email: string) => Promise // typecheck-only, removed from output Passwordless.init({ contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output 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.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.phoneNumber))) { return { status: "GENERAL_ERROR", message: "Sign up disabled. Please contact the admin." } } } } return await originalImplementation.createCodePOST!(input); } } } } }) ``` ```go return false, nil } func isPhoneNumberAllowed(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(*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(*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 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(email: str): # from previous code snippet.. return False async def is_phone_number_allowed(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(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(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="...", # type: ignore recipe_list=[ passwordless.init( contact_config="", # type: ignore # typecheck-only, removed from output flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), ) ], ) ``` ---