Skip to main content

Allow users to change their data

Overview

This guide shows you how to implement a feature that allows users to update their email or password.

Before you start

caution

SuperTokens does not provide the UI for this type of use case. You need to create the UI and set up a route on your backend to have this functionality.

Email update

This section has instructions on how to create a route, on your backend, to update a user's email. Calling this route checks if the new email is valid and not already in use and proceeds to update the user's account with the new email.

Without email verification

In this flow, a user can update their account's email without verifying the new email ID.

1. Create the email update endpoint

  • You need to create a route on your backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route.
  • To learn more about how to use the session verification middleware for other frameworks, click this link
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
})

2. Update the account

  • Validate the input email.
  • Update the account with the input email.
// 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";

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
}

// 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);
}

With email verification

In this flow, the user's account updates once they have verified the new email.

1. Create the email update endpoint

  • You need to create a route on your backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route.
  • To learn more about how to use the session verification middleware for other frameworks, click this link
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
})

2. Initiate the email verification flow

  • Validate the input email
  • Check if the input email associates 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 has been verified, update the account with the new email.

App Info

Adjust these values based on the application that you are trying to configure. To learn more about what each field means check the references page.
This is the URL of your app's API server.
This is the URL of your app's API server.
SuperTokens will expose it's APIs scoped by this base API path.
This is the URL of your website.
The path where the login UI will be rendered
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)) {
return res.status(400).send("Email is invalid");
}

// 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 updated.
return
}
if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") {
// TODO handle error, email already exists with another user.
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);
}
Multi Tenancy
  • Notice that the process loops 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 this step is not done, then calling updateEmailOrPassword would fail because the email is already used by another user in one of the tenants that this user belongs to. In that case, the verification process should not proceed either.
  • We also pass in the tenantId of the current session when calling the sendEmailVerificationEmail function, ensuring that the link generated opens the tenant's UI that the user interacts with.
  • When calling updateEmailOrPassword, it returns EMAIL_ALREADY_EXISTS_ERROR if the new email exists in any of the tenants that the user ID is a part of.

3. Update the account on successful email verification

  • Update the accounts email on successful email verification.
import SuperTokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import EmailVerification from "supertokens-node/recipe/emailverification";
import Session from "supertokens-node/recipe/session";

SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
Passwordless.init({
flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",
contactMethod: "EMAIL_OR_PHONE"
}),
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(),
],
});

Password update

This section has instructions on how to create a route, on your backend, that can update a user's password. Calling this route checks if the old password is valid and updates the user's profile with the new password.

1. Create the password update endpoint

  • You need to create a route on your backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route.
  • To learn more about how to use the session verification middleware for other frameworks, click this link
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-password", verifySession(), async (req: SessionRequest, res: express.Response) => {
// TODO: see next steps
})

2. Update the user password

  • You can use the session object to retrieve the logged-in user's userId.
  • Use the recipe's sign in function and check if the old password is valid
  • Update the user's password.
// the following example uses express
import EmailPassword from "supertokens-node/recipe/emailpassword";
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-password", verifySession(), async (req: SessionRequest, res: express.Response) => {
// get the supertokens session object from the req
let session = req.session

// retrieve the old password from the request body
let oldPassword = req.body.oldPassword

// retrieve the new password from the request body
let updatedPassword = req.body.newPassword

// get the signed in user's email from the getUserById function
let userInfo = await supertokens.getUser(session!.getUserId())

if (userInfo === undefined) {
throw new Error("Should never come here")
}

let loginMethod = userInfo.loginMethods.find((lM) => lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword");
if (loginMethod === undefined) {
throw new Error("Should never come here")
}
const email = loginMethod.email!;

// call signin to check that input password is correct
let isPasswordValid = await EmailPassword.verifyCredentials(session!.getTenantId(), email, oldPassword)

if (isPasswordValid.status !== "OK") {
// TODO: handle incorrect password error
return
}


// update the user's password using updateEmailOrPassword
let response = await EmailPassword.updateEmailOrPassword({
recipeUserId: session!.getRecipeUserId(),
password: updatedPassword,
tenantIdForPasswordPolicy: session!.getTenantId()
})

if (response.status === "PASSWORD_POLICY_VIOLATED_ERROR") {
// TODO: handle incorrect password error
return
}

// TODO: send successful password update response

})

Multi Tenancy

Notice that the tenantId passes as an argument to the signIn and the updateEmailOrPassword functions. This ensures that the current tenant has email password enabled, and it ensures that the user's new password matches the password policy defined for their tenant (if different password policies exist for different tenants).

If this user shares access across multiple tenants, their password changes for all tenants.

3. Revoke all sessions associated with the user Optional

  • Revoking all sessions associated with the user forces them to re-authenticate with their new password.
// the following example uses express
import Session from "supertokens-node/recipe/session";
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-password", verifySession(), async (req: SessionRequest, res: express.Response) => {

let userId = req.session!.getUserId();

/**
*
* ...
* see previous step
* ...
*
* */

// revoke all sessions for the user
await Session.revokeAllSessionsForUser(userId)

// revoke the current user's session, we do this to remove the auth cookies, logging out the user on the frontend.
await req.session!.revokeSession()

// TODO: send successful password update response

})