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- ReactJS
- Angular
- Vue
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
(window as any).supertokensUIEmailVerification.init({
mode: "REQUIRED", // or "OPTIONAL"
}),
(window as any).supertokensUISession.init(),
],
});
// this goes in the auth route config of your frontend app (once the pre built UI script has been loaded)
(window as any).supertokensUIInit("supertokensui", {
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
(window as any).supertokensUIEmailVerification.init({
mode: "REQUIRED", // or "OPTIONAL"
}),
(window as any).supertokensUISession.init(),
],
});
#
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- ReactJS
- Angular
- Vue
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
}
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.
If using REQUIRED
mode
Wrapping your website routes using <SessionAuth />
should enforce email verification. If the user's email is not verified, SuperTokens will automatically redirect the user to the email verification screen.
If using OPTIONAL
mode
import React from "react";
import { SessionAuth, useSessionContext } from 'supertokens-auth-react/recipe/session';
import { EmailVerificationClaim } from 'supertokens-auth-react/recipe/emailverification';
const VerifiedRoute = (props: React.PropsWithChildren<any>) => {
return (
<SessionAuth>
<InvalidClaimHandler>
{props.children}
</InvalidClaimHandler>
</SessionAuth>
);
}
function InvalidClaimHandler(props: React.PropsWithChildren<any>) {
let sessionContext = useSessionContext();
if (sessionContext.loading) {
return null;
}
if (sessionContext.invalidClaims.some(i => i.id === EmailVerificationClaim.id)) {
// Alternatively you could redirect the user to the email verification screen to trigger the verification email
// Note: /auth/verify-email is the default email verification path
// window.location.assign("/auth/verify-email")
return <div>You cannot access this page because your email address is not verified.</div>
}
// We show the protected route since all claims validators have
// passed implying that the user has verified their email.
return <div>{props.children}</div>;
}
Above we are creating a generic component called VerifiedRoute
which enforces that its child components can only be rendered if the user has a verified email address.
In the VerifiedRoute
component, we use the SessionAuth
wrapper to ensure that the session exists. The SessionAuth
wrapper will create a context that contains a prop called invalidClaims
which will contain a list of all claim validations that have failed.
The email verification recipe on the frontend, adds the EmailVerificationClaim
validator automatically, so if the user's email is not verified, the invalidClaims
prop will contain information about that. Alternatively you could also redirect the user to the default email verification path to trigger the sending of the verification email.
We check the result of the validation in the InvalidClaimHandler
component which displays "You cannot access this page because your email address is not verified."
if the EmailVerificationClaim
validator failed.
If all validations pass, we render the props.children
component.
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
}
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.
#
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
- Replacing, customising or embedding the frontend UI
- 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 or certain frontend routes in
REQUIRED
mode.