---
title: Session Verification
description: Verify user sessions when integrating SuperTokens with AWS Lambda.
sidebar:
  order: 2
---

The following page shows three ways to verify sessions in a Lambda integration.
Choose the one that works best based on the particularities of your use case.

:::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.]

If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens.
:::


## Using Session Verification

When building your own APIs, you may need to verify the session of the user before proceeding further.
SuperTokens SDK exposes a `verifySession` function that can be utilized for this.
In this guide, we will be creating a `/user` `GET` route that will return the current session information.

### 1. Add `/user` `GET` route in your API Gateway

Create a `/user` resource and then `GET` method in your API Gateway. Configure the lambda integration and CORS just like we did [for the auth routes](/integrations/aws-lambda/quickstart-guide#13-attach-lambda-to-the-any-method-of-the-proxy-resource).

### 2. Create a file in your Lambda function to handle the `/user` route

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
An example of this is [here](https://github.com/supertokens/supertokens-node/blob/master/examples/aws/with-emailpassword/backend/user.mjs).
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```javascript title="user.mjs" check=false reason="Requires surrounding framework application context"
import supertokens from "supertokens-node";
import { getBackendConfig } from "./config.mjs";
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import middy from "@middy/core";
import cors from "@middy/http-cors";

supertokens.init(getBackendConfig());

const lambdaHandler = async (event) => {
  return {
    body: JSON.stringify({
      sessionHandle: event.session?.getHandle(),
      userId: event.session?.getUserId(),
      accessTokenPayload: event.session?.getAccessTokenPayload(),
    }),
    statusCode: 200,
  };
};

export const handler = middy(verifySession(lambdaHandler))
  .use(
    cors({
      origin: getBackendConfig().appInfo.websiteDomain,
      credentials: true,
      headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "),
      methods: "OPTIONS,POST,GET,PUT,DELETE",
    }),
  )
  .onError((request) => {
    throw request.error;
  });
```
</Tab>
<Tab title="Python" value="python">
```python title="handler.py" check=false reason="Requires surrounding framework application context"
import nest_asyncio
nest_asyncio.apply()

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from mangum import Mangum

from supertokens_python import init, get_all_cors_headers
from supertokens_python.framework.fastapi import get_middleware

import config

init(
    supertokens_config=config.supertokens_config,
    app_info=config.app_info,
    framework=config.framework,
    recipe_list=config.recipe_list,
    mode="asgi",
)

app = FastAPI(title="SuperTokens Example")

from fastapi import Depends
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.session import SessionContainer

@app.get("/user")
def user(s: SessionContainer = Depends(verify_session())):
    return {
        "sessionHandle": s.get_handle(),
        "userId": s.get_user_id(),
        "accessTokenPayload": s.get_access_token_payload()
    }

app.add_middleware(get_middleware())

app = CORSMiddleware(
    app=app,
    allow_origins=[
        config.app_info.website_domain
    ],
    allow_credentials=True,
    allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
    allow_headers=["Content-Type"] + get_all_cors_headers(),
)

handler = Mangum(app)
```
</Tab>
</CodeGroup>

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
Now, import this function in your `index.mjs` handler file as shown below:
</ContentOption>
<ContentOption title="Python" value="python">
:::note[The `verify_session` middleware automatically returns a 401 Unauthorized error if the session is not valid. You can alter the default behavior by passing `session_required=False` to the `verify_session` middleware.]

If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `init` function and include the `session` recipe in the `recipe_list` for each respective lambda function.
:::
</ContentOption>
</DependentContent>

<CodeGroup passive group="backend-language">
<Tab title="Node.js" value="nodejs">
```javascript title="index.mjs" check=false reason="Requires surrounding framework application context"
import supertokens from "supertokens-node";
import { middleware } from "supertokens-node/framework/awsLambda";
import { getBackendConfig } from "./config.mjs";
import middy from "@middy/core";
import cors from "@middy/http-cors";
import { handler as userHandler } from "./user.mjs";

supertokens.init(getBackendConfig());

export const handler = middy(
  middleware((event) => {
    if (event.path === "/user") {
      return userHandler(event);
    }

    return {
      body: JSON.stringify({
        msg: "Hello!",
      }),
      statusCode: 200,
    };
  }),
)
  .use(
    cors({
      origin: getBackendConfig().appInfo.websiteDomain,
      credentials: true,
      headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "),
      methods: "OPTIONS,POST,GET,PUT,DELETE",
    }),
  )
  .onError((request) => {
    throw request.error;
  });
```
</Tab>
</CodeGroup>

<DependentContent passive group="backend-language">
<ContentOption title="Node.js" value="nodejs">
:::note[The `verifySession` middleware automatically returns a 401 Unauthorized error if the session is not valid. You can alter the default behavior by passing `{ sessionRequired: false }` as the second argument to the `verifySession` middleware.]

If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `supertokens.init` and include the `Session` recipe in the `recipeList` for each respective lambda function.
:::
</ContentOption>
</DependentContent>

---

## Using Lambda Authorizers


You can use a Lambda authorizer with an API Gateway REST API to authorize requests to another integration, such as
AppSync. The authorizer below requires a valid session and returns its user ID as `principalId`. API Gateway can map
`$context.authorizer.principalId` to an integration header. Missing and invalid sessions are rejected; this guide does
not claim support for optional sessions because AWS's behavior for an empty principal is not established here.

### 1. Add configurations and dependencies

Refer to the [frontend](/quickstart#1-integrate-the-frontend-sdk), [lambda layer](/integrations/aws-lambda/quickstart-guide#2-set-up-lambda-layer), and [lambda setup](/integrations/aws-lambda/quickstart-guide#3-set-up-lambda).

### 2. Add code to the lambda function handler

<DependentContent passive group="backend-language">
<ContentOption title="Python" value="python">
Use the code below as the handler for the lambda. Remember that whenever we want to use any functions from the `supertokens-python` lib, we have to call the `init` function at the top of that serverless function file. We can then use `get_session()` to get the session.
</ContentOption>
<ContentOption title="Node.js" value="nodejs">
Use the code below as the handler for the lambda.
Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file.
We can then use `getSession()` to get the session.
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Python" value="python">
```python title="auth.py" check=false reason="Requires surrounding framework application context"
import nest_asyncio
import json
nest_asyncio.apply()

from typing import Optional, Dict, Any

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from mangum import Mangum

from supertokens_python import init, get_all_cors_headers
from supertokens_python.framework.fastapi import get_middleware

import config

init(
    supertokens_config=config.supertokens_config,
    app_info=config.app_info,
    framework=config.framework,
    recipe_list=config.recipe_list,
    mode="asgi",
)
app = FastAPI(title="SuperTokens Example")

def generate_policy(principal_id: str, effect: str, resource: str, context: Optional[Dict[str, Any]]):
    policy_document = {
        "Version": "2012-10-17",
        "Statement": [
            {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource}
        ],
    }
    auth_response = {
        "principalId": principal_id,
        "policyDocument": policy_document,
        "context": context or {},
    }

    return auth_response


def generate_allow(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None):
    return generate_policy(principal_id, "Allow", resource, context)


def generate_deny(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None):
    return generate_policy(principal_id, "Deny", resource, context)


from fastapi import Request
from supertokens_python.recipe.session.syncio import get_session
from supertokens_python.recipe.session.exceptions import (InvalidClaimsError,
                                                          TryRefreshTokenError,
                                                          UnauthorisedError)

@app.get("/{full_path:path}")
def handle_auth(request: Request, full_path: str):
    event = request.scope["aws.event"]
    method_arn = event.get("methodArn")

    try:
        session = get_session(request)
        return generate_allow(session.get_user_id(), method_arn)
    except Exception as e:
        if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError):
            raise Exception("Unauthorized")
        if isinstance(e, InvalidClaimsError):
            claim_validation_errors = [err.to_json() for err in e.payload]
            return generate_deny(
                "invalid-claims",
                method_arn,
                {
                    "body": json.dumps({
                        "message": "invalid claims",
                        "claimValidationErrors": claim_validation_errors,
                    })
                },
            )

        raise e


app.add_middleware(get_middleware())

app = CORSMiddleware(
    app=app,
    allow_origins=[
        config.app_info.website_domain
    ],
    allow_credentials=True,
    allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
    allow_headers=["Content-Type"] + get_all_cors_headers(),
)

def handler(event: Dict[str, Any], context: Any):
    mangum_handler = Mangum(app)
    response: Dict[str, Any] = mangum_handler(event, context)

    if event.get("methodArn"):
        return json.loads(response["body"])

    return response
```
</Tab>
<Tab title="Node.js" value="nodejs">
```javascript title="index.mjs" check=false reason="Requires surrounding framework application context"
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";

import { getBackendConfig } from "./config.mjs";

supertokens.init(getBackendConfig());

export const handler = async function (event) {
  try {
    const session = await Session.getSession(event, event);
    return generateAllow(session.getUserId(), event.methodArn);
  } catch (ex) {
    if (ex.type === "TRY_REFRESH_TOKEN" || ex.type === "UNAUTHORISED") {
      throw new Error("Unauthorized");
    }
    if (ex.type === "INVALID_CLAIMS") {
      return generateDeny("invalid-claims", event.methodArn, {
        body: JSON.stringify({
          message: "invalid claim",
          claimValidationErrors: ex.payload,
        }),
      });
    }
    throw ex;
  }
};

const generatePolicy = function (principalId, effect, resource, context = {}) {
  const policyDocument = {
    Version: "2012-10-17",
    Statement: [],
  };

  const statementOne = {
    Action: "execute-api:Invoke",
    Effect: effect,
    Resource: resource,
  };

  policyDocument.Statement[0] = statementOne;

  const authResponse = {
    principalId: principalId,
    policyDocument: policyDocument,
    context,
  };

  return authResponse;
};

const generateAllow = function (principalId, resource, context) {
  return generatePolicy(principalId, "Allow", resource, context);
};

const generateDeny = function (principalId, resource, context) {
  return generatePolicy(principalId, "Deny", resource, context);
};
```
</Tab>
</CodeGroup>

The authorizer `context` map may contain only scalar values. The invalid-claims body is therefore JSON-serialized in
both examples. Do not join multiple `Set-Cookie` values into one authorizer context string: commas are valid inside
cookie attributes and API Gateway may not reconstruct the original headers. Return auth-route cookies from the Lambda
proxy response as distinct values: REST API payload format 1.0 uses
`multiValueHeaders: { "Set-Cookie": cookies }`, while HTTP API payload format 2.0 uses the top-level `cookies` array.
Before relying on cookie mutation from an authorizer, an E2E fixture must prove no-cookie, one-cookie, multiple-cookie,
refresh, denied, and gateway-error paths for the exact REST/HTTP API payload version in use.

### 3. Configure the authorizer

Create a request-based Lambda authorizer for the REST API and point it to the function above. AWS changes console labels;
capture this configuration in IaC and verify that the deployed authorizer receives the headers and cookies required by
your selected SuperTokens token-transfer method.

### 4. Configure API Gateway


- Require the authorizer on each protected method.
- In the integration request, overwrite `x-user-id` from `context.authorizer.principalId`. Never forward a
  client-supplied identity header.
- If the browser must read gateway-generated `401` or `403` responses, configure them with the exact trusted
  `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials: true`. Do not combine credentials with a wildcard
  origin.
- Deploy and test the API. The IaC fixture must prove that a spoofed identity header cannot reach the integration.

---

## Using JWT Authorizers

:::warning

AWS supports JWT authorizers for HTTP APIs and not REST APIs on the API Gateway service. For REST APIs follow the [Lambda authorizer](/integrations/aws-lambda/session-verification#using-lambda-authorizers) guide

This guide will work if you are using **SuperTokens Session Tokens**.

If you are implementing an **OAuth2** setup, through the [**Unified Login**](/authentication/unified-login/introduction) or the [**Microservice Authentication**](/authentication/m2m/client-credentials) features, you will have to manually set the token audience property.
Please check the referenced pages for more information.

:::

### 1. Add the `aud` claim in the JWT based on the authorizer configuration


<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```javascript title="config.mjs"
import Session from "supertokens-node/recipe/session";

export function getBackendConfig() {
  return {
    framework: "awsLambda",
    supertokens: {
      connectionURI: "<CORE_API_ENDPOINT>",
    },
    appInfo: {
      // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration
      appName: "<YOUR_APP_NAME>",
      apiDomain: "<YOUR_API_DOMAIN>",
      websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
      apiBasePath: "/auth",
      websiteBasePath: "/auth",
      apiGatewayPath: "/dev",
    },
    recipeList: [
      Session.init({
        exposeAccessTokenToFrontendInCookieBasedAuth: true,
        override: {
          functions: function (originalImplementation) {
            return {
              ...originalImplementation,
              createNewSession: async function (input) {
                input.accessTokenPayload = {
                  ...input.accessTokenPayload,
                  /*
                   * AWS requires JWTs to contain an audience (aud) claim
                   * The value for this claim should be the same
                   * as the value you set when creating the
                   * authorizer
                   */
                  aud: "jwtAuthorizers",
                };

                return originalImplementation.createNewSession(input);
              },
            };
          },
        },
      }),
    ],
    isInServerlessEnv: true,
  };
}
```
</Tab>
<Tab title="Python" value="python">
```python title="config.py"
from supertokens_python.recipe import session
from supertokens_python import (
    InputAppInfo,
    SupertokensConfig,
)
from supertokens_python.recipe.session.interfaces import RecipeInterface as SessionRecipeInterface

from typing import Any, Dict, Optional
from supertokens_python.types import RecipeUserId

supertokens_config = SupertokensConfig(
    connection_uri="<CORE_API_ENDPOINT>",
)

app_info = InputAppInfo(
    # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration
    app_name="<YOUR_APP_NAME>",
    api_domain="<YOUR_API_DOMAIN>",
    website_domain="<YOUR_WEBSITE_DOMAIN>",
    api_base_path="/auth",
    website_base_path="/auth",
    api_gateway_path="/dev",
)

framework = "fastapi"

def override_session_functions(oi: SessionRecipeInterface) -> SessionRecipeInterface:
    oi_create_new_session = oi.create_new_session

    async def create_new_session(
        user_id: str,
        recipe_user_id: RecipeUserId,
        access_token_payload: Optional[Dict[str, Any]],
        session_data_in_database: Optional[Dict[str, Any]],
        disable_anti_csrf: Optional[bool],
        tenant_id: str,
        user_context: Dict[str, Any],
    ):
        # AWS requires JWTs to contain an audience (aud) claim
        # The value for this claim should be the same as the
        # value you set when creating the authorizer

        if access_token_payload is None:
            access_token_payload = {}

        access_token_payload["aud"] = "jwtAuthorizers"
        return await oi_create_new_session(user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context)

    oi.create_new_session = create_new_session
    return oi

recipe_list = [
    session.init(
        override=session.InputOverrideConfig(
            functions=override_session_functions,
        ),
        expose_access_token_to_frontend_in_cookie_based_auth=True,
    ),
]
```
</Tab>
</CodeGroup>


### 2. Configure your authorizer

- Go to the "Authorizers" tab in the API Gateway configuration and select the "Manage authorizers" tab
- Click "Create", in the creation screen select "JWT" as the "Authorizer type"
- Enter a name for your authorizer (You can enter any name for this field)
- Use `$request.header.Authorization` for the "Identity source". This means that API requests will contain the JWT as a Bearer token under the request header "Authorization".
- Use the exact normalized issuer emitted by SuperTokens for this configuration: `<YOUR_API_DOMAIN>/dev/auth`. This is `apiDomain + apiGatewayPath + apiBasePath`, with one slash at each boundary.
- Set a value for the "Audience" field, this will be the value you expect the JWT to have under the `aud` claim. In the backend config above the value is set to `"jwtAuthorizers"`

### 3. Add the authorizer to your API
- In the "Authorization" section select the "Attach authorizers to routes" tab
- Click on the route you want to add the authorizer to and select the authorizer you created from the dropdown
- Click "Attach authorizer"
- Deploy your changes and test your API

### 4. Send the access token as a bearer token

Exposing the access token does not automatically copy it to the JWT authorizer's identity source in cookie-based auth.
Set the header explicitly on requests to protected routes:

```javascript
import Session from "supertokens-web-js/recipe/session";

const accessToken = await Session.getAccessToken();

if (accessToken === undefined) {
  throw new Error("No session access token is available");
}

const response = await fetch("<YOUR_API_DOMAIN>/dev/user", {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});
```

Keep the SuperTokens frontend SDK's network interception enabled so session refresh continues to work. Test the
expired-token retry path against the deployed HTTP API.

### 5. Check authorization claims in the JWT

Once the JWT authorizer successfully validates the JWT, the claims of the JWT will be available to your lambda functions via `$event.requestContext.authorizer.jwt.claims`. You should check for the right authorization access here.
For example, if one of your lambda functions requires that the user's email is verified, then it should check for the `jwt` payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. Similar checks need to be done to enforce the right user role or if 2FA is completed or not.
This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in this flow, you will have to check them on your own.
