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- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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(),
],
});
import (
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailverification.Init(evmodels.TypeInput{
Mode: evmodels.ModeRequired, // or evmodels.ModeOptional
}),
session.Init(&sessmodels.TypeInput{}),
},
})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import session
from supertokens_python.recipe import emailverification
init(
app_info=InputAppInfo(
api_domain="...", app_name="...", website_domain="..."),
framework='...',
recipe_list=[
emailverification.init(mode='REQUIRED'), # or 'OPTIONAL'
session.init()
]
)
#
Step 2: Frontend setup- Web
- Mobile
- Via NPM
- Via Script Tag
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(),
],
});
Add the following <script>
element along with the other ones in your app
# ...other script tags in the frontend init section
<script src=""></script>
Then call the supertokensEmailVerification.init
function as shown below
supertokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
},
recipeList: [
supertokensEmailVerification.init(),
supertokensSession.init(),
],
});
success
No specific action required here.
#
Step 3: Checking if the user's email is verified in your APIsIf 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.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
- Express
- Hapi
- Fastify
- Koa
- Loopback
- AWS Lambda / Netlify
- Next.js (Pages Dir)
- Next.js (App Dir)
- NestJS
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
}
);
import Hapi from "@hapi/hapi";
import { verifySession } from "supertokens-node/recipe/session/framework/hapi";
import {SessionRequest} from "supertokens-node/framework/hapi";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
let server = Hapi.server({ port: 8000 });
server.route({
path: "/update-blog",
method: "post",
options: {
pre: [
{
method: verifySession({
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()],
}),
},
],
},
handler: async (req: SessionRequest, res) => {
// All validator checks have passed and the user has a verified email address
}
})
import Fastify from "fastify";
import { verifySession } from "supertokens-node/recipe/session/framework/fastify";
import { SessionRequest } from "supertokens-node/framework/fastify";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
let fastify = Fastify();
fastify.post("/update-blog", {
preHandler: 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
});
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import { SessionEvent } from "supertokens-node/framework/awsLambda";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
async function updateBlog(awsEvent: SessionEvent) {
// All validator checks have passed and the user has a verified email address
};
exports.handler = verifySession(updateBlog, {
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()]
});
import KoaRouter from "koa-router";
import { verifySession } from "supertokens-node/recipe/session/framework/koa";
import {SessionContext} from "supertokens-node/framework/koa";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
let router = new KoaRouter();
router.post("/update-blog", verifySession({
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()]
}), async (ctx: SessionContext, next) => {
// All validator checks have passed and the user has a verified email address
});
import { inject, intercept } from "@loopback/core";
import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";
import { verifySession } from "supertokens-node/recipe/session/framework/loopback";
import Session from "supertokens-node/recipe/session";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
class SetRole {
constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { }
@post("/update-blog")
@intercept(verifySession({
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()]
}))
@response(200)
async handler() {
// All validator checks have passed and the user has a verified email address
}
}
import { superTokensNextWrapper } from 'supertokens-node/nextjs'
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
export default async function setRole(req: SessionRequest, res: any) {
await superTokensNextWrapper(
async (next) => {
await verifySession({
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()]
})(req, res, next);
},
req,
res
)
// All validator checks have passed and the user has a verified email address
}
import SuperTokens from "supertokens-node";
import { NextResponse, NextRequest } from "next/server";
import { withSession } from "supertokens-node/nextjs";
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
import { backendConfig } from "@/app/config/backend";
SuperTokens.init(backendConfig());
export async function POST(request: NextRequest) {
return withSession(request, async (err, session) => {
if (err) {
return NextResponse.json(err, { status: 500 });
}
// All validator checks have passed and the user has a verified email address
return NextResponse.json({ message: "Your email is verified!" });
},
{
overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()],
}
);
}
import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common";
import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session";
import { AuthGuard } from './auth/auth.guard';
import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification";
@Controller()
export class ExampleController {
@Post('example')
@UseGuards(new AuthGuard({
overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [...globalValidators, EmailVerificationClaim.validators.isVerified()]
}))
async postExample(@Session() session: SessionContainer): Promise<boolean> {
// All validator checks have passed and the user has a verified email address
return true;
}
}
- Chi
- net/http
- Gin
- Mux
import (
"net/http"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/claims"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
session.VerifySession(&sessmodels.VerifySessionOptions{
OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil))
return globalClaimValidators, nil
},
}, exampleAPI).ServeHTTP(rw, r)
})
}
func exampleAPI(w http.ResponseWriter, r *http.Request) {
// TODO: session is verified and all validators have passed..
}
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/claims"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
router := gin.New()
// Wrap the API handler in session.VerifySession
router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{
OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil))
return globalClaimValidators, nil
},
}), exampleAPI)
}
// This is a function that wraps the supertokens verification function
// to work the gin
func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc {
return func(c *gin.Context) {
session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) {
c.Request = c.Request.WithContext(r.Context())
c.Next()
})(c.Writer, c.Request)
// we call Abort so that the next handler in the chain is not called, unless we call Next explicitly
c.Abort()
}
}
func exampleAPI(c *gin.Context) {
// TODO: session is verified and all claim validators pass.
}
import (
"net/http"
"github.com/go-chi/chi"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/claims"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
r := chi.NewRouter()
// Wrap the API handler in session.VerifySession
r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{
OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil))
return globalClaimValidators, nil
},
}, exampleAPI))
}
func exampleAPI(w http.ResponseWriter, r *http.Request) {
// TODO: session is verified and all claim validators pass.
}
import (
"net/http"
"github.com/gorilla/mux"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evclaims"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/claims"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
router := mux.NewRouter()
// Wrap the API handler in session.VerifySession
router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{
OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil))
return globalClaimValidators, nil
},
}, exampleAPI)).Methods(http.MethodPost)
}
func exampleAPI(w http.ResponseWriter, r *http.Request) {
// TODO: session is verified and all claim validators pass.
}
- FastAPI
- Flask
- Django
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.emailverification import EmailVerificationClaim
from supertokens_python.recipe.session import SessionContainer
from fastapi import Depends
@app.post('/like_comment')
async def like_comment(session: SessionContainer = Depends(
verify_session(
# We add the EmailVerificationClaim's is_verified validator
override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \
[EmailVerificationClaim.validators.is_verified()]
)
)):
# All validator checks have passed and the user has a verified email address
pass
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.emailverification import EmailVerificationClaim
@app.route('/update-jwt', methods=['POST'])
@verify_session(
# We add the EmailVerificationClaim's is_verified validator
override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \
[EmailVerificationClaim.validators.is_verified()]
)
def like_comment():
# All validator checks have passed and the user has a verified email address
pass
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from django.http import HttpRequest
from supertokens_python.recipe.emailverification import EmailVerificationClaim
@verify_session(
# We add the EmailVerificationClaim's is_verified validator
override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \
[EmailVerificationClaim.validators.is_verified()]
)
async def like_comment(request: HttpRequest):
# All validator checks have passed and the user has a verified email address
pass
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- Web
- Mobile
- Via NPM
- Via Script Tag
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.id === EmailVerificationClaim.id) {
// email is not verified
}
}
}
}
// a session does not exist, or email is not verified
return false
}
async function shouldLoadRoute(): Promise<boolean> {
if (await supertokensSession.doesSessionExist()) {
let validationErrors = await supertokensSession.validateClaims();
if (validationErrors.length === 0) {
// user has verified their email address
return true;
} else {
for (const err of validationErrors) {
if (err.id === supertokensEmailVerification.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.id === 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)
}
}
}
}
}
- React Native
- Android
- iOS
- Flutter
import SuperTokens from 'supertokens-react-native';
async function checkIfEmailIsVerified() {
if (await SuperTokens.doesSessionExist()) {
let isVerified: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-ev"].v;
if (isVerified) {
// TODO..
} else {
// TODO..
}
}
}
import android.app.Application
import com.supertokens.session.SuperTokens
import org.json.JSONObject
class MainApplication: Application() {
fun checkIfEmailIsVerified() {
val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this);
val isVerified: Boolean = (accessTokenPayload.get("st-ev") as JSONObject).get("v") as Boolean
if (isVerified) {
// TODO..
} else {
// TODO..
}
}
}
import UIKit
import SuperTokensIOS
class ViewController: UIViewController {
func checkIfEmailIsVerified() {
if let accessTokenPayload: [String: Any] = try? SuperTokens.getAccessTokenPayloadSecurely(), let emailVerificationObject: [String: Any] = accessTokenPayload["st-ev"] as? [String: Any], let isVerified: Bool = emailVerificationObject["v"] as? Bool {
if isVerified {
// Email is verified
} else {
// Email is not verified
}
}
}
}
import 'package:supertokens_flutter/supertokens.dart';
Future<void> checkIfEmailIsVerified() async {
var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely();
if (accessTokenPayload.containsKey("st-ev")) {
Map<String, dynamic> emailVerificationObject = accessTokenPayload["st-ev"];
if (emailVerificationObject.containsKey("v")) {
bool isVerified = emailVerificationObject["v"];
if (isVerified) {
// Email is verified
} else {
// Email is not verified
}
}
}
}
Handling 403 responses on the frontend
If your frontend queries a protected API on your backend and it fails with a 403, you can check the value of the st-ev
claim in the access token payload. If it is set to false you can send the verification email
#
Step 5: Sending the email verification emailWhen 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
- Web
- Mobile
- Via NPM
- Via Script Tag
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.");
}
}
}
import supertokensEmailVerification from "supertokens-web-js/recipe/emailverification";
async function sendEmail() {
try {
let response = await supertokensEmailVerification.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.");
}
}
}
You should create a new screen on your app that asks the user to enter their email to which an email will be sent. This screen should ideally be linked to from the sign in form.
Once the user has enters their email, you can call the following API to send an email verification email to that user:
curl --location --request POST '<YOUR_API_DOMAIN>/auth/user/email/verify/token' \
--header 'Authorization: Bearer ...'
The response body from the API call has a status
property in it:
status: "OK"
: An email was sent to the user successfully.status: "EMAIL_ALREADY_VERIFIED_ERROR"
: This status can be returned if the info about email verification in the session was outdated. Redirect the user to the home page.status: "GENERAL_ERROR"
: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend.
Multi Tenancy
You do not need to add the tenant ID to the path here because the backend fetches the tenantId of the user from the session token.
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 / pathBy 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:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
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"
)
}
)
},
}
}
}
})
]
});
import (
"strings"
"github.com/supertokens/supertokens-golang/ingredients/emaildelivery"
"github.com/supertokens/supertokens-golang/recipe/emailverification"
"github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
supertokens.Init(supertokens.TypeInput{
RecipeList: []supertokens.Recipe{
emailverification.Init(evmodels.TypeInput{
Mode: evmodels.ModeOptional,
EmailDelivery: &emaildelivery.TypeInput{
Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface {
ogSendEmail := *originalImplementation.SendEmail
(*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error {
// This is: `${websiteDomain}${websiteBasePath}/verify-email`
input.EmailVerification.EmailVerifyLink = strings.Replace(
input.EmailVerification.EmailVerifyLink,
"http://localhost:3000/auth/verify-email",
"http://localhost:3000/your/path", 1,
)
return ogSendEmail(input, userContext)
}
return originalImplementation
},
},
}),
},
})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.recipe import emailverification
from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig
from supertokens_python.recipe.emailverification.types import EmailDeliveryOverrideInput, EmailTemplateVars
from typing import Dict, Any
def custom_email_delivery(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput:
original_send_email = original_implementation.send_email
async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None:
# This is: `${websiteDomain}${websiteBasePath}/verify-email`
template_vars.email_verify_link = template_vars.email_verify_link.replace(
"http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path")
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=[
emailverification.init(
mode="OPTIONAL",
email_delivery=EmailDeliveryConfig(override=custom_email_delivery))
]
)
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 clickedOnce 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.
- Web
- Mobile
- Via NPM
- Via Script Tag
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.");
}
}
}
import supertokensEmailVerification from "supertokens-web-js/recipe/emailverification";
async function consumeVerificationCode() {
try {
let response = await supertokensEmailVerification.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.");
}
}
}
- Single tenant setup
- Multi tenant setup
Once the user clicks the email verification link, and it opens as a deep link into your mobile app, you can extract the token from the link and call the verification API as shown below:
curl --location --request POST '<YOUR_API_DOMAIN>/auth/user/email/verify' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"method": "token",
"token": "ZTRiOTBjNz...jI5MTZlODkxw"
}'
Multi Tenancy
For a multi tenancy setup, the <TENANT_ID>
value can be fetched from tenantId
query parameter from the email verification link. If it's not there in the link, you can use the value "public"
(which is the default tenant).
Once the user clicks the email verification link, and it opens as a deep link into your mobile app, you can extract the token from the link and call the verification API as shown below:
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/user/email/verify' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"method": "token",
"token": "ZTRiOTBjNz...jI5MTZlODkxw"
}'
Multi Tenancy
For a multi tenancy setup, the <TENANT_ID>
value can be fetched from tenantId
query parameter from the email verification link. If it's not there in the link, you can use the value "public"
(which is the default tenant).
The response body from the API call has a status
property in it:
status: "OK"
: Email verification was successful.status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"
: This can happen if the verification code is expired or invalid. You should ask the user to retry.status: "GENERAL_ERROR"
: This is only possible if you have overriden the backend API to send back a custom error message which should be displayed on the frontend.
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- Post email verification action
- Change email verification link's lifetime
- Customise email template or email delivery method
- Manually changing email verification status for a user
- Generating email verification links manually
- Learn more about session claim validators and about how to read the email verification status from the session payload
- Exclude email verification check in certain APIs in
REQUIRED
mode.