Using Lambda Authorizers
CAUTION
This guide only applies to scenarios which involve SuperTokens Session Access Tokens.
If you are implementing either, Unified Login or Microservice Authentication, features that make use of OAuth2 Access Tokens, please check the separate page that shows you how to verify those types of tokens.
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 dependenciesRefer 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 GatewayIn 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-Origin
with value'<YOUR_WEBSITE_DOMAIN>'
- Add
Access-Control-Allow-Credentials
with value'true'
. Don't miss out on those quotes else it won't get configured correctly. - Add "Set-Cookie" with value
context.authorizer.setCookie
no 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-Origin
with value'<YOUR_WEBSITE_DOMAIN>'
- Add
Access-Control-Allow-Credentials
with 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