Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Implement username login

Enable username login by replacing email with username in the SuperTokens authentication flow.

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 and have a working application integrated with SuperTokens. If you have not, please check the Quickstart Guide.

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.

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.";
              }
            },
          },
        ],
      },
    }),
  ],
});
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
							},
						},
					},
				},
			}),
		},
	})
}
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.

import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword";

let emailUserMap: { [key: string]: string } = {};

async function getUserUsingEmail(email: string): Promise<string | undefined> {
  // 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,
          },
        ],
      },
    }),
  ],
});
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,
						},
					},
				},
			}),
		},
	})
}
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.

import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword";

let emailUserMap: { [key: string]: string } = {};

async function getUserUsingEmail(email: string): Promise<string | undefined> {
  // 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 ... */
        ],
      },
    }),
  ],
});
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
					},
				},
			}),
		},
	})
}
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.

import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword";

let emailUserMap: { [key: string]: string } = {};

async function getUserUsingEmail(email: string): Promise<string | undefined> {
  // 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 ... */
        ],
      },
    }),
  ],
});
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
					},
				},
			}),
		},
	})
}
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.

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<string | undefined> {
  // 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 ... */
        ],
      },
    }),
  ],
});
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
					},
				},
			}),
		},
	})
}
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.

import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword";

let emailUserMap: { [key: string]: string } = {};

async function getUserUsingEmail(email: string): Promise<string | undefined> {
  // 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);
            },
          };
        },
      },
    }),
  ],
});
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
					},
				},
			}),
		},
	})
}
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

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.
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

API reference

API schema and response details