Skip to main content

Enable email verification

important

For passwordless login with email, a user's email is automatically marked as verified when they login. Therefore, the only time this flow would be triggered is if a user changes their email during a session.

There are two modes of email verification:

  • REQUIRED: Requires that the user's email is verified before they can access your application's frontend or backend routes (that are protected with a session).
  • OPTIONAL: Adds information about email verification into the session, but leaves it up to you to enforce it on the backend and frontend based on your business logic.

Step 1: Backend setup#

import SuperTokens from "supertokens-node";
import EmailVerification from "supertokens-node/recipe/emailverification";
import Session from "supertokens-node/recipe/session";

SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
EmailVerification.init({
mode: "REQUIRED", // or "OPTIONAL"
}),
Session.init(),
],
});

Step 2: Frontend setup#

import SuperTokens from "supertokens-web-js";
import EmailVerification from "supertokens-web-js/recipe/emailverification";
import Session from "supertokens-web-js/recipe/session";

SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
},
recipeList: [
EmailVerification.init(),
Session.init(),
],
});

Step 3: Checking if the user's email is verified in your APIs#

If using REQUIRED mode

On the backend, when you initialize the email verification recipe in this mode, the verifySession middleware automatically checks if the user's email is verified based on the contents of the session's payload. If the email is not verified, the verifySession middleware will return a 403 status code to the client.

If using OPTIONAL mode

In this mode, you need to check if the email is verified yourself in the APIs in which you want this constraint. The verification status should already be in the session's payload.

import { verifySession } from "supertokens-node/recipe/session/framework/express";
import express from "express";
import { SessionRequest } from "supertokens-node/framework/express";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";

let app = express();

app.post(
"/update-blog",
verifySession({
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()],
}),
async (req: SessionRequest, res) => {
// All validator checks have passed and the user has a verified email address
}
);

We add the SDK's EmailVerificationClaim validator to the verifySession middleware call as shown above, and that will only allow access if the email is verified, else it will return 403 to the frontend.

Step 4: Protecting frontend routes#

import Session from "supertokens-web-js/recipe/session";
import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification";

async function shouldLoadRoute(): Promise<boolean> {
if (await Session.doesSessionExist()) {
let validationErrors = await Session.validateClaims();

if (validationErrors.length === 0) {
// user has verified their email address
return true;
} else {
for (const err of validationErrors) {
if (err.validatorId === EmailVerificationClaim.id) {
// email is not verified
}
}
}
}
// a session does not exist, or email is not verified
return false
}

In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the validationErrors variable. The EmailVerificationClaim validator will be automatically checked by this function since you have initialized the email verification recipe.

Handling 403 responses on the frontend

If your frontend queries a protected API on your backend and it fails with a 403, you can call the validateClaims function and loop through the errors to know which claim has failed:

import axios from "axios";
import Session from "supertokens-web-js/recipe/session";
import { EmailVerificationClaim, sendVerificationEmail } from "supertokens-web-js/recipe/emailverification";

async function callProtectedRoute() {
try {
let response = await axios.get("<YOUR_API_DOMAIN>/protectedroute");
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 403) {
let validationErrors = await Session.validateClaims();
for (let err of validationErrors) {
if (err.validatorId === EmailVerificationClaim.id) {
// email verification claim check failed
// We call the sendEmail function defined in the next section to send the verification email.
// await sendEmail();
} else {
// some other claim check failed (from the global validators list)
}
}

}
}
}

Step 5: Sending the email verification email#

When the email verification validators fail, or post sign up, you want to redirect the user to a screen telling them that a verification email has been sent to them. On this screen, you should call the following API

import { sendVerificationEmail } from "supertokens-web-js/recipe/emailverification";

async function sendEmail() {
try {
let response = await sendVerificationEmail();
if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") {
// This can happen if the info about email verification in the session was outdated.
// Redirect the user to the home page
window.location.assign("/home");
} else {
// email was sent successfully.
window.alert("Please check your email and click the link in it")
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}
note

The API for sending an email verification email requires an active session. If you are using our frontend SDKs, then the session tokens should automatically get attached to the request.

Changing the email verification link domain / path#

By default, the email verification link will point to the websiteDomain that is configured on the backend, on the /auth/verify-email route (where /auth is the default value of websiteBasePath).

If you want to change this to a different path, a different domain, or deep link it to your mobile / desktop app, then you can do so on the backend in the following way:

import SuperTokens from "supertokens-node";
import EmailVerification from "supertokens-node/recipe/emailverification";

SuperTokens.init({
supertokens: {
connectionURI: "...",
},
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "..."
},
recipeList: [
EmailVerification.init({
mode: "OPTIONAL",
emailDelivery: {
override: (originalImplementation) => {
return {
...originalImplementation,
sendEmail(input) {
return originalImplementation.sendEmail({
...input,
emailVerifyLink: input.emailVerifyLink.replace(
// This is: `${websiteDomain}${websiteBasePath}/verify-email`
"http://localhost:3000/auth/verify-email",
"http://localhost:3000/your/path"
)
}
)
},
}
}
}
})
]
});
Multi Tenancy

For a multi tenant setup, the input to the sendEmail function will also contain the tenantId. You can use this to determine the correct value to set for the websiteDomain in the generated link.

Step 6: Verifying the email post link clicked#

Once the user clicks the email verification link, and it opens your app, you can call the following function which will automatically extract the token and tenantId (if using a multi tenant setup) from the link and call the token verification API.

import { verifyEmail } from "supertokens-web-js/recipe/emailverification";

async function consumeVerificationCode() {
try {
let response = await verifyEmail();
if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") {
// This can happen if the verification code is expired or invalid.
// You should ask the user to retry
window.alert("Oops! Seems like the verification link expired. Please try again")
window.location.assign("/auth/verify-email") // back to the email sending screen.
} else {
// email was verified successfully.
window.location.assign("/home")
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}
caution
  • This API doesn't require an active session to succeed.

  • If you are calling the above API on page load, there is an edge case in which email clients might open the verification link in the email (for scanning purposes) and consume the token in the URL. This would lead to issues in which an attacker could sign up using someone else's email and end up with a veriifed status!

    To prevent this, on page load, you should check if a session exists, and if it does, only then call the above API. If a session does not exist, you should first show a button, which when clicked would call the above API (email clients won't automatically click on this button). The button text could be something like "Click here to verify your email".

See also#