File length: 50641 # Authentication - Email Password - 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](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). ## 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 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 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 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='...', # type: ignore 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 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 } }, { // highlight-start 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 // highlight-end }] } }) ] }); ``` ```go return nil }, }, // highlight-start { 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, }, // highlight-end }, }, }), }, }) } ``` ```python 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] = {} # highlight-start 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 # highlight-end 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='...', # type: ignore recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ InputFormField(id="email", validate=validate), # highlight-start InputFormField(id="actualEmail", validate=validate_actual_email, optional=True) # highlight-end ]) ) ] ) ``` #### 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 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({ // highlight-start 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 } } } }, // highlight-end signUpFeature: { formFields: [ /* ... from previous code snippet ... */] } }) ] }); ``` ```go }, // highlight-start 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 }, }, // highlight-end }), }, }) } ``` ```python 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 # highlight-start 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 # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore 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 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 } // highlight-start 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; } // highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", websiteDomain: "..." }, supertokens: { connectionURI: "...", }, recipeList: [ EmailPassword.init({ override: { // highlight-start 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); } } }, // highlight-end apis: (original) => { return { ...original, // override from previous code snippet } } }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */] } }) ] }); ``` ```go if err != nil || !emailCheck { return false } return true } // highlight-end 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 }, // highlight-start 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 }, // highlight-end }, }), }, }) } ``` ```python 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 # highlight-start 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 # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore 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 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; } // highlight-start 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; } // highlight-end 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... // highlight-start 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); }, // highlight-end } } }, signUpFeature: { formFields: [ /* ... from previous code snippet ... */] } }) ] }); ``` ```go if err != nil || !emailCheck { return false } return true } // highlight-start func getEmailUsingUserId(userId string) (*string, error) { for email, mappedUserId := range emailUserMap { if mappedUserId == userId { return &email, nil } } return nil, nil } // highlight-end 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... // highlight-start 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) } // highlight-end return originalImplementation }, Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // ...override from previous code snippet... return originalImplementation }, }, }), }, }) } ``` ```python 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 ) # highlight-start 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 # highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), framework="...", # type: ignore 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 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 ... */] }, // highlight-start emailDelivery: { override: (original) => { return { ...original, sendEmail: async function (input) { input.user.email = (await getEmailUsingUserId(input.user.id))!; return original.sendEmail(input) } } } }, // highlight-end }) ] }); ``` ```go 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 }, }, // highlight-start 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 }, }, // highlight-end }), }, }) } ``` ```python 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 # highlight-start 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 # highlight-end init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), framework='...', # type: ignore 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 ), # highlight-next-line 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 SuperTokens.init({ appInfo: { appName: "...", apiDomain: "...", websiteDomain: "...", }, // highlight-start languageTranslations: { translations: { "en": { EMAIL_PASSWORD_EMAIL_LABEL: "Username or email" } } }, // highlight-end recipeList: [ EmailPassword.init({ signInAndUpFeature: { // highlight-start 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 }] } // highlight-end } }), // other recipes initialisation.. ], }); ```