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