Changes to the emailpassword flow
Our approach will be to replace the email
field in SuperTokens with the username field. This will enable username and password login as well as enforce username uniqueness.
Then we will save the optional email against the userID of the user and use that during sign in and reset password flows. The mapping of email to userID will need to be handled by you and stored in your own database. We will create place holder functions in the code snippets below for you to implement them.
#
Modifying the default email validator functionWhen the sign up / in API is called on the backend, SuperTokens first verifies the syntax of the input email. Since we want to replace emails with usernames, we need to change that function to allow for any string
. We can do this in the following way:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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 supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature
from supertokens_python.recipe.emailpassword.types import InputFormField
from re import fullmatch
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,'
r"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)
])
)
]
)
- The
validate
function above is called during sign up, sign in and reset password APIs. In the sign up API, the input would always be a username, but during sign in and reset password APIs, the input could be a username or an email. Therefore, this function needs to check for either of the two formats. - The new
validate
function first checks if the input is a valid email, and if it is, returns early. Else it checks if the input username has at least three characters and contains only alphanumeric, underscore or hyphen characters. If this criteria doesn't match, then the validator returns an appropriate error string. You can modify this function to be more complex and match your criteria. - You may have noticed that the
id
is still"email"
. This is because from SuperTokens' point of view, we will still be storing the username in the SuperTokens' user's email field. This has no side effect other than you (the developer) having to fetch the user's username using theuser.email
field (whereuser
is the user object returned by SuperTokens).
#
Overriding the sign up API to save the user's emailThe sign up API will take in the username, password and an optional email. In order to support this, we must add a new form field for the email and add a validate
function which checks the uniqueness and syntax of the input email.
Then we must override the sign up API to save this email against the userID of that user.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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 supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature
from supertokens_python.recipe.emailpassword.types import InputFormField
from re import fullmatch
from typing import Dict
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,'
r"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)
])
)
]
)
- We call the new form field
actualEmail
. You can change this if you like, but whatever you set it to, should be used by the frontend as well when calling the sign up API. - You need to implement the
getUserUsingEmail
function to check your database for if there already exists a user with that email. SuperTokens will not have this information since it will be storing the username of the user instead of their email.
Now we must override the sign up API to save the actualEmail
form field value against the user ID.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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 supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.utils import (
InputSignUpFeature,
InputOverrideConfig,
)
from supertokens_python.recipe.emailpassword.types import FormField
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
SignUpPostOkResult,
)
from typing import Dict, List, Union, Any
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),
)
],
)
- We first call the
original.signUpPOST
function which will sign up the user with username and password login. It will also enforce uniqueness of the username - if not unique, it will return an appropriate reply to the frontend. - If the sign up was successful, we will exptract the
"actualEmail"
form field from the input. If the value is""
, it means that the user did not specify their email. Else we will save the userId and email mapping using thesaveEmailForUser
function. saveEmailForUser
is a function that you must implement, just like how you had implemented thegetUserUsingEmail
function.
#
Overriding the sign in backend function to accept email or usernameWe want the user to be able to sign in using their email or username along with their password. In order to implement this, we must override the sign in recipe function as follows:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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 supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.utils import (
InputSignUpFeature,
InputOverrideConfig,
)
from supertokens_python.recipe.emailpassword.interfaces import RecipeInterface
from typing import Dict, Union, Any
from re import fullmatch
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.asyncio import get_user
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,'
r"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
),
)
],
)
- Notice that this time, we provided the override function to
override.function
config and not theoverride.apis
config. This is because this function is called not only duringt the sign in API, but also if you call theEmailpassword.signIn
function manually in your own APIs. - First we check if the input is a valid email - using a regex. If it's not, then we call the
original.signIn
function which will try to do a username + password login. - If the input is an email, then we fetch the userID of that email using the previously implemented
getUserUsingEmail
function. If that cannot find a user mapping, then we let the code fallthrough tooriginal.signIn
. - If a user ID was found, then we query SuperTokens using
EmailPassword.getUserById
to get the SuperTokens user object. Freom there, we change theinput.email
to the username of the user, which is stored insuperTokensUser.email
. After modifying the input, we let theoriginal.signIn
function run which will attempt a username and password login.
#
Overriding the password reset link API to accept username or emailThe password reset flow requires the user to have added an email during sign up. If there is no email associated with the user, we return an appropriate message to them to contact support.
We allow the user to enter either their username or their email when starting the password reset flow. Just like the sign in cusomtisation, we must check if the input is an email, and if it is, fetch the username associated with the user before calling the SuperTokens' default implementation of the generate password reset link API.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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 supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.asyncio import get_user, list_users_by_account_info
from supertokens_python.types import AccountInfo
from supertokens_python.recipe.emailpassword.utils import (
InputSignUpFeature,
InputOverrideConfig,
)
from supertokens_python.recipe.emailpassword.types import FormField
from supertokens_python.recipe.emailpassword.interfaces import (
APIInterface,
APIOptions,
)
from supertokens_python.types import GeneralErrorResponse
from typing import Dict, List, Any
from re import fullmatch
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,'
r"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, AccountInfo(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
),
)
],
)
- If the user has entered their email ID, then we first try and replace it with their username - just like how we did it in the sign in function.
- Once we have the username, or if the user had entered their username originally, we try and check if there exists an email for that username. To do this, we call the
getUserByEmail
function, passing in the username of the user. This will query SuperTokens to get the user object containing the user ID. Once we have the userID, we can see if there exists an email for that userId by calling thegetEmailUsingUserId
function. getEmailUsingUserId
is a function which you must implement by querying your database.- If an email is not returned, it means that the user had signed up without an email. So we send an appropriate message to the frontend. Else we call the
original.generatePasswordResetTokenPOST
function.
The above cusomtisation will allow users to enter an email or a username in the password reset form. However, when the SDK is sending an email, it will try and send it to the username. That of course, won't work. Therefore, we must override the sendEmail
function as well to fetch the email of the user from the input user ID before calling the original.sendEmail
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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 supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailpassword
from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature, InputOverrideConfig
from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig
from typing import Dict, Any
from re import fullmatch
from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars
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,'
r"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)
)
]
)
- In the above code snippet, we override the
sendEmail
function which is called by the SDK to send the reset password email. - The input email to the function is actually the user's username (since SuperTokens stores the username and not the email). We simply fetch the user's actual email from the
getEmailUsingUserId
function which we implemented previously and then assign that to the input before calling the original implementation. - Note that this function will only be called if the user has an email associated with their account since we check for that in the override for
generatePasswordResetTokenPOST
.
note
This completes the changes required on the backend. Below are changes required for the frontend SDK.
We need to make the following customizations to the frontend's default UI:
- Change the email validator to not do validation on the frontend since we allow username or email. The backend will do appropriate checks anyway.
- 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".
These can be made using the following configs in the init
function:
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..
],
});
- The
languageTranslations
config will replace the label for "Email" in the password reset form. - We add the
signInForm
config and change the label and the placeholder associated with the"email"
input. - We add the
signUpForm
config in which we:- Change the label and the placeholder associated with the
"email"
input. The originalemail
field will correspond to the username now. - Add an additional (optiona) form field in which the user can enter their email. This would correspond to the
"actualEmail"
form field on the backend.
- Change the label and the placeholder associated with the
success
This completes the changes required.