Implement allow list based sign up
Discover how to implement an allow list based sign up flow with the passwordless recipe.
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.
Prerequisites
This guide uses the UserMetadata recipe to store the allow list.
You need to enable it 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.
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);
}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
}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_list2. 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.
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);
},
};
},
},
});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
},
},
})
}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,
),
)
],
)