Using Lambda Authorizers
You can use a lambda as an Authorizer in API Gateways. This will enable you to use SuperTokens in a lambda to authorize requests to other integrations (e.g., AppSync). An Authorizer pointed to this lambda will add context.authorizer.principalId that you can map to a header. For example, you can map this to an "x-user-id" header which will be set to the id of the logged-in user. If there is no valid session for the request, this header won't exist.
1) Add configurations and dependencies#
Refer to the frontend, lambda layer, and lambda setup guides for setting up the frontend, lambda layer, and lambda function.
2) Add code to the lambda function handler#
- NodeJS
- Python
- Other Frameworks
Important
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.
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)
# This route will act as the authorizer for API Gateway
@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, session_required=False)
        if session:
            return generate_allow(session.get_user_id(), method_arn)
        else:
            # You can allow requests without sessions
            return generate_allow("", method_arn)
            # You can also return explicit deny with additional context, but you should configure these as 401s so the frontend SDK can handle them.
            # return generate_deny(None, method_arn)
            # raise Exception("Unauthorized")
    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(
                "",
                method_arn,
                {
                    "body": {
                        "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"):
        gateway_response = json.loads(response["body"])
        # We need to add setCookie to the context, since later we will be mapping this to a Set-Cookie header on the response
        # get_session has to set cookies on the first call after a refresh.
        gateway_response["context"]["setCookie"] = response["headers"]["set-cookie"]
        return gateway_response
    return response
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.
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, { sessionRequired: false });
        if (session) {
            // We need to add setCookie to the context, since later we will be mapping this to a Set-Cookie header on the response
            // getSession has to set cookies on the first call after a refresh.
            return generateAllow(session.getUserId(), event.methodArn, {
                setCookie: event.supertokens.response.cookies.join(', '),
            });
        } else {
            // You can allow requests without sessions
            return generateAllow("", event.methodArn, {
                setCookie: event.supertokens.response.cookies.join(', '),
            });
            // You can also return explicit deny with additional context, but you should configure these as 401s so the frontend SDK can handle them.
            // return generateDeny(undefined, event.methodArn);
            // throw new Error("Unauthorized");
        }
    } catch (ex: any) {
        if (ex.type === "TRY_REFRESH_TOKEN" || ex.type === "UNAUTHORISED") {
            throw new Error("Unauthorized");
        }
        if (ex.type === "INVALID_CLAIMS") {
            return generateDeny("", event.methodArn, {
                body: JSON.stringify({
                    message: "invalid claim",
                    claimValidationErrors: ex.payload,
                }),
                setCookie: event.supertokens.response.cookies.join(", "),
            });
        }
        throw ex;
    }
}
// Help function to generate an IAM policy
const generatePolicy = function (principalId, effect, resource, context) {
    // Required output:
    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,
        // Optional output with custom properties of the String, Number or Boolean type.
        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);
};
3) Configure the Authorizer#
- Go to the "Authorizers" tab in the API Gateway configuration
- Click "Create new Authorizer" and add the new Authorizer- Fill the name field
- Set "Lambda function" to the one created above
- Set "Lambda Event Payload" to Request
- Delete the empty "Identity Source"
- Click "Create"
 
4) Configure API Gateway#
- In your API Gateway, create the resources and methods you require, enabling CORS if necessary (see setup api gateway for details) 
- Select each Method you want to enable the Authorizer on and configure it to use the new Authorizer - Click on "Method Request" 
- Edit the "Authorization" field in Settings and set it to the one we just created. 
- Go back to the method configuration and click on "Integration Request" - Set up the integration you require (see AppSync for an example)
- Add a header mapping to make use of the context set in the lambda.- Open "HTTP Headers"
- Add all headers required (e.g., "x-user-id" mapped to "context.authorizer.principalId")
- Repeat for any values from the context you want to add as a Header
 
 
- Go back to the method configuration and click on "Method Response" - Open the dropdown next to the 200 status code
- Add the "Set-Cookie" header
- Add any other headers that should be present on the response.
 
- Go back to the method configuration and click on "Integration Response" - Open the dropdown
- Open "Header Mappings"
- Add "Set-Cookie" mapped to "context.authorizer.setCookie"
 
 
- In the API Gateway left menu, select "Gateway Responses" - Select "Access Denied"- Click "Edit"
- Add response headers:- Add Access-Control-Allow-Originwith value'<YOUR_WEBSITE_DOMAIN>'
- Add Access-Control-Allow-Credentialswith value'true'. Don't miss out on those quotes else it won't get configured correctly.
- Add "Set-Cookie" with value context.authorizer.setCookieno quotes
 
- Add 
- Under response templates:- Select application/json:
- Set "Response template body" to $context.authorizer.body
 
- Select 
- Click "Save"
 
- Select "Unauthorized"- Add response headers:- Add Access-Control-Allow-Originwith value'<YOUR_WEBSITE_DOMAIN>'
- Add Access-Control-Allow-Credentialswith value'true'. Don't miss out on those quotes else it won't get configured correctly.
 
- Add 
- Click "Save"
 
- Add response headers:
 
- Select "Access Denied"
- Deploy your API and test it