---
title: Protect frontend and backend routes
description: Protect API and website routes by implementing email verification checks for secure access.
sidebar:
  order: 30
---

## Overview

The `EmailVerification` claim shows the status of the email verification process.
Follow this page to understand how to limit access based on whether the user has confirmed their email address.

## Before you start

:::info[Access token guidance]

If you are implementing [**Unified Login**](/authentication/unified-login/introduction), you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**.
Please read the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify the token.

:::

---

## Protect backend routes

### Add email verification checks on all routes

If you want to protect all your backend API routes with email verification checks, set the `mode` to `REQUIRED` in the `EmailVerification` configuration.
Routes protected with the `verifySession` middleware additionally check for email verification status.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
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({
      // This means that verifySession will now only allow calls if the user has verified their email
      mode: "REQUIRED",
    }),
    Session.init(),
  ],
});
```
</Tab>
<Tab title="Go" value="go">
```go
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{
                // This means that VerifySession will now only allow calls if the user has verified their email
				Mode: evmodels.ModeRequired,
			}),
			session.Init(&sessmodels.TypeInput{}),
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="initialization excerpt omits deployment connection config"
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=[
        # This means that VerifySession will now only allow calls if the user has verified their email
        emailverification.init(mode='REQUIRED'),
        session.init()
    ]
)
```
</Tab>
</CodeGroup>

In case you have set the email verification mode to `REQUIRED` but want to disable the check for a specific route, you can make the following changes to the `verifySession` middleware:

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
<DependentContent passive group="node-frameworks">
<ContentOption title="Next.js" value="nextjs">
<NextjsRouterTypeSelect />
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Express" value="express">
```tsx
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.filter((v) => v.id !== EmailVerificationClaim.key),
  }),
  async (req: SessionRequest, res) => {
    // The session and remaining claim validators have passed; email verification was skipped
  },
);
```
</ContentOption>
<ContentOption title="Hapi" value="hapi">
```tsx
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.filter((v) => v.id !== EmailVerificationClaim.key),
        }),
      },
    ],
  },
  handler: async (req: SessionRequest, res) => {
    // The session and remaining claim validators have passed; email verification was skipped
  },
});
```
</ContentOption>
<ContentOption title="Fastify" value="fastify">
```tsx
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.filter((v) => v.id !== EmailVerificationClaim.key),
    }),
  },
  async (req: SessionRequest, res) => {
    // The session and remaining claim validators have passed; email verification was skipped
  },
);
```
</ContentOption>
<ContentOption title="Aws Lambda" value="aws-lambda">
```tsx
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) {
  // The session and remaining claim validators have passed; email verification was skipped
}

exports.handler = verifySession(updateBlog, {
  overrideGlobalClaimValidators: async (globalValidators) =>
    globalValidators.filter((v) => v.id !== EmailVerificationClaim.key),
});
```
</ContentOption>
<ContentOption title="Koa" value="koa">
```tsx
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.filter((v) => v.id !== EmailVerificationClaim.key),
  }),
  async (ctx: SessionContext, next) => {
    // The session and remaining claim validators have passed; email verification was skipped
  },
);
```
</ContentOption>
<ContentOption title="LoopBack" value="loopback">
```tsx
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.filter((v) => v.id !== EmailVerificationClaim.key),
    }),
  )
  @response(200)
  async handler() {
    // The session and remaining claim validators have passed; email verification was skipped
  }
}
```
</ContentOption>
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="pages-router">

```tsx
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.filter((v) => v.id !== EmailVerificationClaim.key),
      })(req, res, next);
    },
    req,
    res,
  );
  // The session and remaining claim validators have passed; email verification was skipped
}
```

</ConditionalContent>
</ContentOption>
<ContentOption title="Nestjs" value="nestjs">
```tsx check=false reason="application example imports local modules defined elsewhere"
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.filter((v) => v.id !== EmailVerificationClaim.key),
    }),
  )
  async postExample(@Session() session: SessionContainer): Promise<boolean> {
    // The session and remaining claim validators have passed; email verification was skipped
    return true;
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Go" value="go">
<DependentContent group="go-frameworks" label="Go framework">
<ContentOption title="Http" value="http">
```go
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) {
				filtered := []claims.SessionClaimValidator{}
				for _, v := range globalClaimValidators {
					// we keep all claim validators except for
					// the email verification claim validator.
					if v.ID != evclaims.EmailVerificationClaim.Key {
						filtered = append(filtered, v)
					}
				}
				return filtered, nil
			},
		}, exampleAPI).ServeHTTP(rw, r)
	})
}

func exampleAPI(w http.ResponseWriter, r *http.Request) {
	// TODO: session is verified and all validators have passed..
}
```
</ContentOption>
<ContentOption title="Gin" value="gin">
```go
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) {
			filtered := []claims.SessionClaimValidator{}
			for _, v := range globalClaimValidators {
				// we keep all claim validators except for
				// the email verification claim validator.
				if v.ID != evclaims.EmailVerificationClaim.Key {
					filtered = append(filtered, v)
				}
			}
			return filtered, 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.
}
```
</ContentOption>
<ContentOption title="Chi" value="chi">
```go
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) {
			filtered := []claims.SessionClaimValidator{}
			for _, v := range globalClaimValidators {
				// we keep all claim validators except for
				// the email verification claim validator.
				if v.ID != evclaims.EmailVerificationClaim.Key {
					filtered = append(filtered, v)
				}
			}
			return filtered, nil
		},
	}, exampleAPI))
}

func exampleAPI(w http.ResponseWriter, r *http.Request) {
	// TODO: session is verified and all claim validators pass.
}

```
</ContentOption>
<ContentOption title="Mux" value="mux">
```go
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) {
			filtered := []claims.SessionClaimValidator{}
			for _, v := range globalClaimValidators {
				// we keep all claim validators except for
				// the email verification claim validator.
				if v.ID != evclaims.EmailVerificationClaim.Key {
					filtered = append(filtered, v)
				}
			}
			return filtered, nil
		},
	}, exampleAPI)).Methods(http.MethodPost)
}

func exampleAPI(w http.ResponseWriter, r *http.Request) {
	// TODO: session is verified and all claim validators pass.
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-frameworks" label="Python framework">
<ContentOption title="FastAPI" value="fastapi">
```python check=false reason="route fragment assumes an existing framework application"
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 keep all validators except for the EmailVerification ones
            override_global_claim_validators=lambda global_validators, session, user_context: [
                validators for validators in global_validators if validators.id != EmailVerificationClaim.key]
        )
)):
    # The session and remaining claim validators have passed; email verification was skipped
    pass
```
</ContentOption>
<ContentOption title="Flask" value="flask">
```python check=false reason="route fragment assumes an existing framework application"
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 keep all validators except for the EmailVerification ones
    override_global_claim_validators=lambda global_validators, session, user_context: [
        validators for validators in global_validators if validators.id != EmailVerificationClaim.key]
)
def like_comment():
    # The session and remaining claim validators have passed; email verification was skipped
    pass
```
</ContentOption>
<ContentOption title="Django" value="django">
```python
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 keep all validators except for the EmailVerification ones
    override_global_claim_validators=lambda global_validators, session, user_context: [
        validators for validators in global_validators if validators.id != EmailVerificationClaim.key]
)
async def like_comment(request: HttpRequest):
    # The session and remaining claim validators have passed; email verification was skipped
    pass
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<CodeGroup passive group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="app-router">

```tsx check=false reason="application example imports local modules defined elsewhere"
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 });
      }
      // We skipped checking the email verification claim
      return NextResponse.json({});
    },
    {
      overrideGlobalClaimValidators: async (globalValidators) =>
        globalValidators.filter((v) => v.id !== EmailVerificationClaim.key),
    },
  );
}
```

</ConditionalContent>
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

### Add email verification checks to specific routes

If you want to protect specific backend API routes with email verification checks, set the `mode` to `OPTIONAL` in the `EmailVerification` configuration.
You then override the `verifySession` middleware protecting the route to check for email verification status. 

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
<DependentContent passive group="node-frameworks">
<ContentOption title="Next.js" value="nextjs">
<NextjsRouterTypeSelect />
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Express" value="express">
```tsx
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
  },
);
```
</ContentOption>
<ContentOption title="Hapi" value="hapi">
```tsx
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
  },
});
```
</ContentOption>
<ContentOption title="Fastify" value="fastify">
```tsx
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
  },
);
```
</ContentOption>
<ContentOption title="Aws Lambda" value="aws-lambda">
```tsx
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(),
  ],
});
```
</ContentOption>
<ContentOption title="Koa" value="koa">
```tsx
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
  },
);
```
</ContentOption>
<ContentOption title="LoopBack" value="loopback">
```tsx
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
  }
}
```
</ContentOption>
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="pages-router">

```tsx
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
}
```

</ConditionalContent>
</ContentOption>
<ContentOption title="Nestjs" value="nestjs">
```tsx check=false reason="application example imports local modules defined elsewhere"
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;
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Go" value="go">
<DependentContent group="go-frameworks" label="Go framework">
<ContentOption title="Http" value="http">
```go
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..
}
```
</ContentOption>
<ContentOption title="Gin" value="gin">
```go
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.
}
```
</ContentOption>
<ContentOption title="Chi" value="chi">
```go
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.
}
```
</ContentOption>
<ContentOption title="Mux" value="mux">
```go
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.
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-frameworks" label="Python framework">
<ContentOption title="FastAPI" value="fastapi">
```python check=false reason="route fragment assumes an existing framework application"
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
```
</ContentOption>
<ContentOption title="Flask" value="flask">
```python check=false reason="route fragment assumes an existing framework application"
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
```
</ContentOption>
<ContentOption title="Django" value="django">
```python
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
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<CodeGroup passive group="backend-language">
<Tab title="Node.js" value="nodejs">
<DependentContent group="node-frameworks" label="Node.js framework">
<ContentOption title="Next.js" value="nextjs">
<ConditionalContent propertyName="nextjsRouterType" condition="app-router">

```tsx check=false reason="application example imports local modules defined elsewhere"
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({});
    },
    {
      overrideGlobalClaimValidators: async (globalValidators) => [
        ...globalValidators,
        EmailVerificationClaim.validators.isVerified(),
      ],
    },
  );
}
```

</ConditionalContent>
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>


<VariantContent storageKey="ui-type" value="prebuilt">

---

## Protect frontend routes

<UITypeSwitch />

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
### Protect all frontend routes

Set the email verification mode to `REQUIRED` and wrap your website routes using `<SessionAuth />`.
If the user's email is not verified, SuperTokens automatically redirects the user to the email verification screen.

### Protect specific frontend routes

Set the email verification mode to `OPTIONAL`.
Create a generic component called `VerifiedRoute` which enforces that its child components can only render if the user has a verified email address.
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-prebuilt-ui">
<Tab title="Reactjs" value="reactjs">
```tsx
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) => {
  return (
    <SessionAuth>
      <InvalidClaimHandler>{props.children}</InvalidClaimHandler>
    </SessionAuth>
  );
};

function InvalidClaimHandler(props: React.PropsWithChildren) {
  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>;
}
```
</Tab>
<Tab title="Angular" value="angular">
```tsx
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;
}
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
In the `VerifiedRoute` component, use the `SessionAuth` wrapper to ensure that the session exists.
The `<SessionAuth>` component automatically adds the `EmailVerificationClaim` validator if you initialize the `EmailVerification` recipe.

Finally, check the validation result in `InvalidClaimHandler`. It displays `"You cannot access this page because your email address is not verified."` if the `EmailVerificationClaim` validator failed. Alternatively you could also redirect the user to the default email verification path to trigger the sending of the verification email.

:::note[You can extend the `VerifiedRoute` component to check for other types of validators as well.]
You can reuse this component to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`).
:::

### Check the verification status manually

If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself:
</ContentOption>
<ContentOption title="Angular" value="angular">
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 reflects in the `validationErrors` variable.
The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe.

### Validation errors

In case the `validationErrors` array is not empty, you can loop through the errors to know which claim has failed:
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-prebuilt-ui">
<Tab title="Reactjs" value="reactjs">
```tsx
import Session from "supertokens-auth-react/recipe/session";
import { EmailVerificationClaim } from "supertokens-auth-react/recipe/emailverification";

function ProtectedComponent() {
  let claimValue = Session.useClaimValue(EmailVerificationClaim);
  if (claimValue.loading || !claimValue.doesSessionExist) {
    return null;
  }
  let isEmailVerified = claimValue.value;
  if (isEmailVerified !== undefined && isEmailVerified) {
    //...
  } else {
    // Redirect the user the email verification path to send the verification email
    // Note: /auth/verify-email is the default email verification path
    window.location.assign("/auth/verify-email");
  }
}
```
</Tab>
<Tab title="Angular" value="angular">
```tsx
import Session from "supertokens-web-js/recipe/session";
import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification";

async function shouldLoadRoute() {
  let validationErrors = await Session.validateClaims(/*{...}*/);
  for (const err of validationErrors) {
    if (err.id === EmailVerificationClaim.id) {
      // email verification claim check failed
    } else {
      // some other claim check failed (from the global validators list)
    }
  }
}
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Angular" value="angular">
### Check the verification status manually

If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself:
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-prebuilt-ui">
<Tab title="Angular" value="angular">
```tsx
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 isVerified = await Session.getClaimValue({ claim: EmailVerificationClaim });
    if (isVerified) {
      // user has verified their email address
      return true;
    }
  }
  // either a session does not exist, or the user has not verified their email address
  return false;
}
```
</Tab>
</CodeGroup>

</VariantContent>

<VariantContent storageKey="ui-type" value="custom">


## Protect frontend routes

<UITypeSwitch />



<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import Session from "supertokens-web-js/recipe/session";
import { EmailVerificationClaim, sendVerificationEmail } 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
          // Send the verification email to the user
          await sendEmail();
        }
      }
    }
  }
  // a session does not exist, or email is not verified
  return false;
}

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.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
<DependentContent group="mobile-frameworks" label="Mobile framework">
<ContentOption title="ReactNative" value="reactnative">
```tsx
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 {
      // You can trigger the sending of the verification email by calling `<YOUR_API_DOMAIN>/auth/user/email/verify/token`
    }
  }
}
```
</ContentOption>
<ContentOption title="Android" value="android">
```kotlin
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 {
            // You can trigger the sending of the verification email by calling `<YOUR_API_DOMAIN>/auth/user/email/verify/token`
        }
    }
}
```
</ContentOption>
<ContentOption title="iOS" value="ios">
```swift
import UIKit
import SuperTokensIOS

fileprivate 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 {
                // You can trigger the sending of the verification email by calling `<YOUR_API_DOMAIN>/auth/user/email/verify/token`
            }
        }
    }
}
```
</ContentOption>
<ContentOption title="Flutter" value="flutter">
```dart
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 {
          // You can trigger the sending of the verification email by calling `<YOUR_API_DOMAIN>/auth/user/email/verify/token`
        }
      }
    }
}
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
:::note[The API for sending an email verification email requires an active session.]
If you are using the frontend SDKs, then the session tokens should automatically get attached to the request.
:::

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 reflects in the `validationErrors` variable.
The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe.

### Handle 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. Loop through the errors to know which claim has failed:
</ContentOption>
<ContentOption title="Mobile" value="mobile">
### Handle 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 `false`, you can send the verification email.
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Web" value="web">
```tsx
import axios from "axios";
import Session from "supertokens-web-js/recipe/session";
import { EmailVerificationClaim } 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 previous section to send the verification email.
          // await sendEmail();
        } else {
          // some other claim check failed (from the global validators list)
        }
      }
    }
  }
}
```
</Tab>
</CodeGroup>


</VariantContent>

---

## See also

<CardGroup cols={3}>
  <Card title="Manual actions" href="/additional-verification/email-verification/manual-actions" />
  <Card title="Hooks and overrides" href="/additional-verification/email-verification/hooks-and-overrides" />
  <Card title="Claim validation" href="/additional-verification/session-verification/claim-validation" />
  <Card title="Backend session validation" href="/additional-verification/session-verification/protect-api-routes" />
</CardGroup>
