Allow users to change their email
caution
SuperTokens does not provide the UI for users to update their email, you will need to create the UI and setup a route on your backend to have this functionality.
In this section we will go over how you can create a route on your backend which can update a user's email. Calling this route will check if the new email is valid and not already in use and proceed to update the user's account with the new email. There are two types of flows here:
#
Flow 1: Update email without verifying the new email.In this flow a user is allowed to update their accounts email without verifying the new email id.
/change-email
route#
Step 1: Creating the - You will need to create a route on your backend which is protected by the session verification middleware, this will ensure that only a authenticated user can access the protected route.
- To learn more about how to use the session verification middleware for other frameworks click here
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express"
import express from "express";
let app = express();
app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => {
// TODO: see next steps
})
import (
"net/http"
"github.com/supertokens/supertokens-golang/recipe/session"
)
// the following example uses net/http
func main() {
_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Wrap the API handler in session.VerifySession
session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r)
})
}
func changeEmailAPI(w http.ResponseWriter, r *http.Request) {
// TODO: see next steps
}
# the following example uses flask
from supertokens_python.recipe.session.framework.flask import verify_session
from flask import Flask
app = Flask(__name__)
@app.route('/change-email', methods=['POST'])
@verify_session()
def change_email():
pass # TODO: see next steps
#
Step 2: Validate the new email and update the account- Validate the input email.
- Check that the account you are trying to update is not social account.
- Update the account with the input email.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
// the following example uses express
import Passwordless from "supertokens-node/recipe/passwordless";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express"
import express from "express";
import supertokens from "supertokens-node";
let app = express();
app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => {
let session = req.session!;
let email = req.body.email;
// Validate the input email
if (!isValidEmail(email)) {
// TODO: handle invalid email error
return
}
// Check that the account to be updated is not a social account
{
let userId = session!.getUserId();
let userAccount = await supertokens.getUser(userId!);
let loginMethod = userAccount?.loginMethods.find(lM => {
return lM.recipeUserId.getAsString() === session.getRecipeUserId().getAsString()
})!
if (loginMethod.recipeId === "thirdparty") {
// TODO: handle error, cannot update email for third party users.
return
}
}
// Update the email
let resp = await Passwordless.updateUser({
recipeUserId: session.getRecipeUserId(),
email: email,
});
if (resp.status === "OK") {
// TODO: send successfully updated email response
return
}
if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") {
// TODO: handle error that email exists with another account.
return
}
if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") {
// This is possible if you have enabled account linking.
// See our docs for account linking to know more about this.
// TODO: tell the user to contact support.
}
throw new Error("Should never come here");
})
function isValidEmail(email: string) {
let regexp = new RegExp(
/^(([^<>()\[\]\\.,;:\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,}))$/
);
return regexp.test(email);
}
import (
"encoding/json"
"log"
"net/http"
"regexp"
"github.com/supertokens/supertokens-golang/recipe/passwordless"
"github.com/supertokens/supertokens-golang/recipe/session"
)
type RequestBody struct {
Email string
}
// the following example uses net/http
func test() {
_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Wrap the API handler in session.VerifySession
session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r)
})
}
func changeEmailAPI(w http.ResponseWriter, r *http.Request) {
sessionContainer := session.GetSessionFromRequestContext(r.Context())
var requestBody RequestBody
err := json.NewDecoder(r.Body).Decode(&requestBody)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
// validate the input email
if !isValidEmail(requestBody.Email) {
// TODO: handle invalid email error
return
}
// check that the account is not a social account
userId := sessionContainer.GetUserID()
// update the email
updateResponse, err := passwordless.UpdateUser(userId, &requestBody.Email, nil)
if err != nil {
// TODO: handle error
}
if updateResponse.OK != nil {
// TODO: send successfully updated email response
return
}
if updateResponse.EmailAlreadyExistsError != nil {
// TODO: handle error that email exists with another account
return
}
if updateResponse.UnknownUserIdError != nil {
// TODO: handle error where this user is not a passwordless user
}
log.Fatal("should not reach here")
}
func isValidEmail(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 {
return false
}
return emailCheck
}
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.passwordless.syncio import update_user
from supertokens_python.syncio import get_user
from supertokens_python.recipe.passwordless.interfaces import (
UpdateUserOkResult,
UpdateUserEmailAlreadyExistsError,
EmailChangeNotAllowedError,
)
from flask import g, request, Flask
from re import fullmatch
app = Flask(__name__)
@app.route("/change-email", methods=["POST"])
@verify_session()
def change_email():
session: SessionContainer = g.supertokens
request_body = request.get_json()
email: str = str(request_body["email"])
if request_body is None:
# TODO: handle invalid body error
return
# validate the input email
if not is_valid_email(email):
# TODO: handle invalid email error
return
# check that the account is not a social account
user_info = get_user(session.get_user_id())
if user_info is None:
# TODO handle error, cannot update email for social account
return
login_method = next(
(
lm
for lm in user_info.login_methods
if lm.recipe_user_id.get_as_string()
== session.get_recipe_user_id().get_as_string()
),
None,
)
assert login_method is not None
if login_method.recipe_id == "thirdparty":
# TODO: handle error, cannot update email for third party account
return
# update the users email
update_response = update_user(session.get_recipe_user_id(), email=email)
if isinstance(update_response, UpdateUserOkResult):
# TODO send successful email update response
return
if isinstance(update_response, UpdateUserEmailAlreadyExistsError):
# TODO handle error, email already exists
return
if isinstance(update_response, EmailChangeNotAllowedError):
# This is possible if you have enabled account linking.
# See our docs for account linking to know more about this.
# TODO: tell the user to contact support.
return
raise Exception("Should never reach here")
def is_valid_email(value: str) -> bool:
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,}))$",
value,
)
is not None
)
#
Flow 2: Updating email after verifying the new email.In this flow the user's account is updated once they have verified the new email.
/change-email
route#
Step 1: Creating the - You will need to create a route on your backend which is protected by the session verification middleware, this will ensure that only a authenticated user can access the protected route.
- To learn more about how to use the session verification middleware for other frameworks click here
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express"
import express from "express";
let app = express();
app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => {
// TODO: see next steps
})
import (
"net/http"
"github.com/supertokens/supertokens-golang/recipe/session"
)
// the following example uses net/http
func main() {
_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Wrap the API handler in session.VerifySession
session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r)
})
}
func changeEmailAPI(w http.ResponseWriter, r *http.Request) {
// TODO: see next steps
}
# the following example uses flask
from supertokens_python.recipe.session.framework.flask import verify_session
from flask import Flask
app = Flask(__name__)
@app.route('/change-email', methods=['POST'])
@verify_session()
def change_email():
pass # TODO: see next steps
#
Step 2: Validate the email and initiate the email verification flow- Validate the input email
- Check that the user's account is not a social account.
- Check if the input email is associated with an account.
- Check if the input email is already verified.
- If the email is NOT verified, create and send the verification email.
- If the email is verified, update the account with the new email.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
// the following example uses express
import Passwordless from "supertokens-node/recipe/passwordless";
import EmailVerification from "supertokens-node/recipe/emailverification";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express"
import express from "express";
import supertokens from "supertokens-node";
import { isEmailChangeAllowed } from "supertokens-node/recipe/accountlinking"
let app = express();
app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => {
let session = req.session!;
let email = req.body.email;
// validate the input email
if (!isValidEmail(email)) {
// TODO: handle error, email is invalid
return
}
// Check if the user's account is not a third party account
{
let userId = session!.getUserId();
let userAccount = await supertokens.getUser(userId);
let loginMethod = userAccount?.loginMethods.find(lM => {
return lM.recipeUserId.getAsString() === session.getRecipeUserId().getAsString()
})!
if (loginMethod.recipeId === "thirdparty") {
// TODO: handle error, cannot update email for third party users.
return
}
}
// Then, we check if the email is verified for this user ID or not.
// It is important to understand that SuperTokens stores email verification
// status based on the user ID AND the email, and not just the email.
let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email);
if (!isVerified) {
if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, false))) {
// this can come here if you have enabled the account linking feature, and
// if there is a security risk in changing this user's email.
return res.status(400).send("Email change not allowed. Please contact support");
}
// Before sending a verification email, we check if the email is already
// being used by another user. If it is, we throw an error.
let user = (await supertokens.getUser(session.getUserId()))!;
for (let i = 0; i < user?.tenantIds.length; i++) {
// Since once user can be shared across many tenants, we need to check if
// the email already exists in any of the tenants.
let usersWithEmail = await supertokens.listUsersByAccountInfo(user.tenantIds[i], {
email
});
for (let y = 0; y < usersWithEmail.length; y++) {
if (usersWithEmail[y].id !== session.getUserId()) {
// TODO handle error, email already exists with another user.
return
}
}
}
// Now we create and send the email verification link to the user for the new email.
await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), session.getRecipeUserId(), email);
// TODO send successful response that email verification email sent.
return
}
// Since the email is verified, we try and do an update
let resp = await Passwordless.updateUser({
recipeUserId: session.getRecipeUserId(),
email: email,
});
if (resp.status === "OK") {
// TODO: send successful response that email has been updated
return
}
if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") {
// TODO: handle error, email already exists for another account
return
}
throw new Error("Should never come here");
})
function isValidEmail(email: string) {
let regexp = new RegExp(
/^(([^<>()\[\]\\.,;:\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,}))$/
);
return regexp.test(email);
}
import (
"encoding/json"
"log"
"net/http"
"regexp"
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/recipe/passwordless"
"github.com/supertokens/supertokens-golang/recipe/session"
)
type RequestBody struct {
Email string
}
// the following example uses net/http
func main() {
_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Wrap the API handler in session.VerifySession
session.VerifySession(nil, changeEmailAPI).ServeHTTP(rw, r)
})
}
func changeEmailAPI(w http.ResponseWriter, r *http.Request) {
sessionContainer := session.GetSessionFromRequestContext(r.Context())
var requestBody RequestBody
err := json.NewDecoder(r.Body).Decode(&requestBody)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
// validate the input email
if !isValidEmail(requestBody.Email) {
// TODO: handle invalid email error
return
}
// check that the account is not a social account
userId := sessionContainer.GetUserID()
userAccount, err := passwordless.GetUserByID(userId)
if err != nil {
// TODO: handle error
return
}
if userAccount == nil {
// TODO: return 400 status code saying that the user is not a passwordless user
return
}
// Then, we check if the email is verified for this user ID or not.
// It is important to understand that SuperTokens stores email verification
// status based on the user ID AND the email, and not just the email.
isVerified, err := emailverification.IsEmailVerified(userId, &requestBody.Email)
if err != nil {
// TODO: handle error
}
if !isVerified {
// Now we create and send the email verification link to the user for the new email.
_, err := emailverification.SendEmailVerificationEmail(sessionContainer.GetTenantId(), userId, &requestBody.Email)
if err != nil {
// TODO: handle error
}
return
}
// Since the email is verified, we try and do an update
updateResponse, err := passwordless.UpdateUser(userId, &requestBody.Email, nil)
if err != nil {
// TODO: handle error
}
if updateResponse.OK != nil {
// TODO: send successfully updated email response
return
}
if updateResponse.EmailAlreadyExistsError != nil {
// TODO: handle error, email already exists for another account
return
}
log.Fatal("should not reach here")
}
func isValidEmail(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 {
return false
}
return emailCheck
}
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.passwordless.syncio import update_user
from supertokens_python.recipe.passwordless.interfaces import (
UpdateUserEmailAlreadyExistsError,
UpdateUserOkResult,
)
from supertokens_python.recipe.emailverification.syncio import (
is_email_verified,
send_email_verification_email,
)
from flask import g, request, Flask
from re import fullmatch
from supertokens_python.syncio import get_user
from supertokens_python.recipe.accountlinking.syncio import is_email_change_allowed
from supertokens_python.syncio import list_users_by_account_info
from supertokens_python.types import AccountInfo
app = Flask(__name__)
@app.route("/change-email", methods=["POST"])
@verify_session()
def change_email():
session: SessionContainer = g.supertokens
request_body = request.get_json()
if request_body is None:
# TODO: handle invalid body error
return
# validate the input email
if not is_valid_email(request_body["email"]):
# TODO: handle invalid email error
return
# check that the account is not a social account
user_info = get_user(session.get_user_id())
if user_info is None:
# TODO handle error, cannot update email for social account
return
login_method = next(
(
lm
for lm in user_info.login_methods
if lm.recipe_user_id.get_as_string()
== session.get_recipe_user_id().get_as_string()
),
None,
)
assert login_method is not None
if login_method.recipe_id == "thirdparty":
# TODO: handle error, cannot update email for third party account
return
# check that the account is not a social account
user_id = session.get_user_id()
# Then, we check if the email is verified for this user ID or not.
# It is important to understand that SuperTokens stores email verification
# status based on the user ID AND the email, and not just the email.
is_verified = is_email_verified(session.get_recipe_user_id(), request_body["email"])
if not is_verified:
if not is_email_change_allowed(
session.get_recipe_user_id(), request_body["email"], False
):
# Email change is not allowed, send a 400 error
return {"error": "Email change not allowed"}, 400
# Before sending a verification email, we check if the email is already
# being used by another user. If it is, we throw an error.
user = get_user(user_id)
if user is not None:
for tenant_id in user.tenant_ids:
users_with_same_email = list_users_by_account_info(
tenant_id, AccountInfo(email=request_body["email"])
)
for curr_user in users_with_same_email:
# Since one user can be shared across many tenants, we need to check if
# the email already exists in any of the tenants that belongs to this user.
if curr_user.id != user_id:
# TODO handle error, email already exists with another user.
return
# Create and send the email verification link to the user for the new email.
send_email_verification_email(
session.get_tenant_id(),
user_id,
session.get_recipe_user_id(),
request_body["email"],
)
# update the users email
update_response = update_user(
session.get_recipe_user_id(), email=request_body["email"]
)
if isinstance(update_response, UpdateUserOkResult):
# TODO send successful email update response
return
if isinstance(update_response, UpdateUserEmailAlreadyExistsError):
# TODO handle error, email already exists
return
raise Exception("Should never reach here")
def is_valid_email(value: str) -> bool:
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,}))$",
value,
)
is not None
)
Multi Tenancy
- Notice that we loop through all the tenants that this user belongs to check that for each of the tenants, there is no other user with the new email. If we did not do this, then calling
updateEmailOrPassword
would fail because the email is already used by another user in one of the tenants that this user belongs to. So in that case, we do not want to go through the verification process either. - We also pass in the tenantId of the current session when calling the
sendEmailVerificationEmail
function, so that the link generated will open the the tenant's UI that the user is currently interacting with. - When calling
updateEmailOrPassword
, it will returnEMAIL_ALREADY_EXISTS_ERROR
if the new email exists in any of the tenants that the user ID is a part of.
verifyEmailPost
API to update the user's account on successful email verification#
Step 3: Override the - Update the accounts email on successful email verification.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
For other backend frameworks, you can follow our guide on how to spin up a separate server configured with the SuperTokens backend SDK to authenticate requests and issue session tokens.
import SuperTokens from "supertokens-node";
import EmailVerification from "supertokens-node/recipe/emailverification";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
// other recipes..
EmailVerification.init({
mode: "REQUIRED",
override: {
apis: (oI) => {
return {
...oI,
verifyEmailPOST: async function (input) {
let response = await oI.verifyEmailPOST!(input);
if (response.status === "OK") {
// This will update the email of the user to the one
// that was just marked as verified by the token.
await Passwordless.updateUser({
recipeUserId: response.user.recipeUserId,
email: response.user.email,
});
}
return response;
},
};
},
},
}),
Session.init(),
],
});
import (
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels"
"github.com/supertokens/supertokens-golang/recipe/passwordless"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
err := supertokens.Init(supertokens.TypeInput{
AppInfo: supertokens.AppInfo{
AppName: "...",
APIDomain: "...",
WebsiteDomain: "...",
},
RecipeList: []supertokens.Recipe{
emailverification.Init(evmodels.TypeInput{
Mode: evmodels.ModeRequired,
Override: &evmodels.OverrideStruct{
APIs: func(originalImplementation evmodels.APIInterface) evmodels.APIInterface {
originalVerifyEmailPOST := *originalImplementation.VerifyEmailPOST
(*originalImplementation.VerifyEmailPOST) = func(token string, sessionContainer sessmodels.SessionContainer, tenantId string, options evmodels.APIOptions, userContext supertokens.UserContext) (evmodels.VerifyEmailPOSTResponse, error) {
response, err := originalVerifyEmailPOST(token, sessionContainer, tenantId, options, userContext)
if response.OK != nil {
// This will update the email of the user to the one
// that was just marked as verified by the token.
_, err := passwordless.UpdateUser(response.OK.User.ID, &response.OK.User.Email, nil)
if err != nil {
// TODO: Handle error
}
}
return response, err
}
return originalImplementation
},
},
}),
session.Init(nil),
},
})
if err != nil {
panic(err.Error())
}
}
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import passwordless, emailverification
from supertokens_python.recipe.emailverification.interfaces import (
APIInterface,
APIOptions,
)
from supertokens_python.recipe.passwordless.asyncio import update_user
from supertokens_python.recipe.emailverification.interfaces import (
EmailVerifyPostOkResult,
)
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python import (
InputAppInfo,
SupertokensConfig,
)
from supertokens_python.recipe.passwordless import ContactEmailOrPhoneConfig
from typing import Optional, Dict, Any
def override_email_verification_apis(original_implementation: APIInterface):
original_email_verification_verify_email_post = (
original_implementation.email_verify_post
)
async def email_verify_post(
token: str,
session: Optional[SessionContainer],
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
):
verification_response = await original_email_verification_verify_email_post(
token, session, tenant_id, api_options, user_context
)
if isinstance(verification_response, EmailVerifyPostOkResult):
await update_user(
verification_response.user.recipe_user_id,
verification_response.user.email,
)
return verification_response
original_implementation.email_verify_post = email_verify_post
return original_implementation
init(
supertokens_config=SupertokensConfig(connection_uri="..."),
app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
framework="flask",
recipe_list=[
passwordless.init(
flow_type="USER_INPUT_CODE_AND_MAGIC_LINK",
contact_config=ContactEmailOrPhoneConfig(),
),
emailverification.init(
"REQUIRED",
override=emailverification.InputOverrideConfig(
apis=override_email_verification_apis
),
),
],
)