Skip to main content
Which UI do you use?
Custom UI
Pre built UI

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 function#

When 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:

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."
}
}
}]
}
})
]
});
  • 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 the user.email field (where user is the user object returned by SuperTokens).

Overriding the sign up API to save the user's email#

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

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

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 ... */]
}
})
]
});
  • 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 the saveEmailForUser function.
  • saveEmailForUser is a function that you must implement, just like how you had implemented the getUserUsingEmail function.

Overriding the sign in backend function to accept email or username#

We 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:

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 ... */]
}
})
]
});
  • Notice that this time, we provided the override function to override.function config and not the override.apis config. This is because this function is called not only duringt the sign in API, but also if you call the Emailpassword.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 to original.signIn.
  • If a user ID was found, then we query SuperTokens using EmailPassword.getUserById to get the SuperTokens user object. Freom there, we change the input.email to the username of the user, which is stored in superTokensUser.email. After modifying the input, we let the original.signIn function run which will attempt a username and password login.

Overriding the password reset link API to accept username or email#

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

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 ... */]
}
})
]
});
  • 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 the getEmailUsingUserId 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

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)
}
}
}
},
})
]
});
  • 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 original email 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.
success

This completes the changes required.