Gate on AWS as Lambda Authorizer with API Gateway
In this quickstart guide, we will show you how to deploy Gate on AWS as a Lambda Authorizer with API Gateway. If you aren't already familiar with Gate, we recommend reading our [getting started page](/docs/gate/getting-started.
In this guide, we will give you ready-to-use Terraform code and a script which uses AWS CLI.
If you are looking for detailed step-by-step instructions, please read the full Lambda Authorizer Guide.
Limitations
Due to the way Lambda Authorizer is integrated with API Gateway, it doesn't have access to the request body, nor can it edit the response. When running as a Lambda Authorizer, Gate cannot access the request body nor modify the response, due to the way Lambda Authorizer is integrated with API Gateway. As such, plugins that change request body or response are not supported. Currently this includes the Authorization Proxy and PII detection plugins.
To alter request or response headers, you need to configure them manually.
Currently, Gate supports AWS Gateway v2 only. Support for v1 will be added in the future.
Deploying
In this guide, we will use Gate as a Lambda Authorizer to protect a Lambda function by validating a JSON Web Token (JWT). This is a basic example to make the tutorial practical. A more extensive list of usecases supported by Gate can be found in the Use Cases page.
AWS API Gateway can be configured to use Gate as an Authorizer using the AWS Console or with Terraform.
A misconfiguration may prevent Gate from functioning correctly, and so we recommend following each step in this guide, which includes information on how to verify each step. If you encounter issues, please check the Troubleshooting page.
Throughout this guide, remember to replace
<YOUR AWS REGION>
with your AWS region (e.g.,us-east-1
)<YOUR ACCOUNT ID>
with your AWS account ID (12 digits)<YOUR ORGANIZATION ID>
with your SlashID organization ID (36 characters)
This guides uses JWTs issued by SlashID. If you don't have a SlashID organization yet, you can create one for free in 30 seconds by visiting SlashID Console.
- Terraform
- AWS CLI
You can learn how to authorize Terraform in AWS in the Terraform documentation.
After that, you can run:
terraform init
terraform apply
With the following Terraform code:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "<YOUR AWS REGION>"
}
variable "organization_id" {
default = "<YOUR ORGANIZATION ID>"
}
data "aws_region" "current" {}
####
resource "aws_iam_role" "lambda_exec" {
name = "serverless_lambda"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
data "aws_iam_policy_document" "lambda_logging" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = ["arn:aws:logs:*:*:*"]
}
}
resource "aws_iam_policy" "lambda_logging" {
name = "lambda_logging"
path = "/"
description = "IAM policy for logging from a lambda"
policy = data.aws_iam_policy_document.lambda_logging.json
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_exec.name
policy_arn = aws_iam_policy.lambda_logging.arn
}
####
data "http" "echo_lambda" {
url = "https://storage.googleapis.com/slashid-sandbox-ad6b-cdn/static/echo-lambda.zip"
}
resource "local_sensitive_file" "echo_lambda" {
content_base64 = data.http.echo_lambda.response_body_base64
filename = "${path.module}/echo-function.zip"
}
####
resource "aws_lambda_function" "echo" {
function_name = "echo"
handler = "echo.handler"
role = aws_iam_role.lambda_exec.arn
runtime = "provided.al2"
architectures = ["arm64"]
filename = "echo-function.zip"
source_code_hash = sha256(data.http.echo_lambda.response_body_base64)
depends_on = [
aws_iam_role_policy_attachment.lambda_logs,
local_sensitive_file.echo_lambda,
]
}
output "echo_lambda_logs" {
description = "Echo lambda logs"
value = format(
"https://%s.console.aws.amazon.com/lambda/home?region=%s#/functions/%s?tab=monitoring",
data.aws_region.current.name,
data.aws_region.current.name,
aws_lambda_function.echo.function_name,
)
}
####
resource "aws_apigatewayv2_api" "lambda_gateway" {
name = "lambda_gateway"
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_integration" "echo_lambda" {
api_id = aws_apigatewayv2_api.lambda_gateway.id
integration_uri = aws_lambda_function.echo.invoke_arn
integration_type = "AWS_PROXY"
integration_method = "POST"
}
resource "aws_lambda_permission" "lambda_permission" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.echo.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda_gateway.execution_arn}/*/*"
}
resource "aws_apigatewayv2_route" "echo" {
api_id = aws_apigatewayv2_api.lambda_gateway.id
route_key = "GET /echo"
target = "integrations/${aws_apigatewayv2_integration.echo_lambda.id}"
authorization_type = "CUSTOM"
authorizer_id = "${aws_apigatewayv2_authorizer.gate.id}"
depends_on = [
aws_apigatewayv2_integration.echo_lambda,
aws_apigatewayv2_authorizer.gate,
]
}
####
resource "aws_apigatewayv2_stage" "lambda_test" {
api_id = aws_apigatewayv2_api.lambda_gateway.id
name = "test"
auto_deploy = true
default_route_settings {
data_trace_enabled = true
logging_level = "INFO"
detailed_metrics_enabled = true
throttling_rate_limit = 100
throttling_burst_limit = 100
}
access_log_settings {
destination_arn = aws_cloudwatch_log_group.lambda_gateway.arn
format = "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId"
}
}
resource "aws_cloudwatch_log_group" "lambda_gateway" {
name = "/aws/apigateway/lambda_gateway"
retention_in_days = 30
}
output "echo_url" {
description = "Base URL for API Gateway stage."
value = format("%s/%s", aws_apigatewayv2_stage.lambda_test.invoke_url, "echo")
}
####
resource "null_resource" "download_gate" {
provisioner "local-exec" {
command = "mkdir -p gate && cd gate && wget -O- https://cdn.slashid.com/releases/latest/gate-free_latest_linux_arm64.tar.gz | tar xz && mv gate-free bootstrap"
}
}
data "archive_file" "gate_zip" {
depends_on = [
null_resource.download_gate
]
source_dir = "./gate/"
output_path = "${path.module}/gate.zip"
type = "zip"
}
resource "aws_lambda_function" "gate" {
function_name = "gate"
role = aws_iam_role.lambda_exec.arn
handler = "gate.handler"
runtime = "provided.al2"
architectures = ["arm64"]
filename = "gate.zip"
source_code_hash = data.archive_file.gate_zip.output_base64sha256
environment {
variables = {
GATE_MODE = "aws_lambda_auth"
GATE_LOG_LEVEL = "debug"
GATE_PLUGINS_0_TYPE = "validate-jwt"
GATE_PLUGINS_0_ENABLED = "true"
GATE_PLUGINS_0_PARAMETERS_JWKS_URL = "https://api.slashid.com/.well-known/jwks.json"
GATE_PLUGINS_0_PARAMETERS_JWT_ALLOWED_ALGORITHMS = "RS256"
GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_ISSUER = "https://api.slashid.com"
GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_AUDIENCE = var.organization_id
}
}
depends_on = [
aws_iam_role_policy_attachment.lambda_logs,
]
}
output "gate_lambda_logs" {
description = "Gate lambda logs"
value = format(
"https://%s.console.aws.amazon.com/lambda/home?region=%s#/functions/%s?tab=monitoring",
data.aws_region.current.name,
data.aws_region.current.name,
aws_lambda_function.gate.function_name,
)
}
####
resource "aws_apigatewayv2_authorizer" "gate" {
api_id = aws_apigatewayv2_api.lambda_gateway.id
authorizer_type = "REQUEST"
authorizer_uri = aws_lambda_function.gate.invoke_arn
identity_sources = ["$request.header.Authorization"]
name = "gate"
authorizer_payload_format_version = "2.0"
enable_simple_responses = true
authorizer_result_ttl_in_seconds = 0
}
resource "aws_lambda_permission" "authorizer_permission" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.gate.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda_gateway.execution_arn}/authorizers/${aws_apigatewayv2_authorizer.gate.id}"
}
set -e
REGION=<YOUR AWS REGION>
ACCOUNT_ID=<YOUR ACCOUNT ID>
ORGANIZATION_ID=<YOUR ORGANIZATION ID>
GATE_LAMBDA_NAME=gate
ECHO_LAMBDA_NAME=echo
ECHO_LAMBDA_ROUTE="/echo"
GATEWAY_NAME=lambda_gateway
STAGE_NAME=test
INTEGRATION_URI=arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:$ECHO_LAMBDA_NAME
aws iam create-role --no-cli-pager \
--role-name serverless_lambda \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Sid": "",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
]
}'
POLICY_ARN=$(aws iam create-policy \
--policy-name lambda_logging \
--description "IAM policy for logging from a lambda" \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}' \
--query 'Policy.Arn' \
--output text)
aws iam attach-role-policy \
--role-name serverless_lambda \
--policy-arn $POLICY_ARN
aws iam attach-role-policy \
--role-name serverless_lambda \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
wget https://storage.googleapis.com/slashid-sandbox-ad6b-cdn/static/echo-lambda.zip
aws lambda create-function --no-cli-pager \
--function-name $ECHO_LAMBDA_NAME \
--handler ${ECHO_LAMBDA_NAME}.handler \
--runtime provided.al2 \
--role arn:aws:iam::469725248735:role/serverless_lambda \
--zip-file fileb://echo-lambda.zip \
--architectures arm64
aws apigatewayv2 create-api --no-cli-pager \
--name $GATEWAY_NAME \
--protocol-type HTTP
API_ID=$(aws apigatewayv2 get-apis --query 'Items[?Name==`'"$GATEWAY_NAME"'`].ApiId | [0]' --output text)
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
--api-id $API_ID \
--integration-method POST \
--integration-type AWS_PROXY \
--payload-format-version 2.0 \
--integration-uri $INTEGRATION_URI \
--query 'IntegrationId' \
--output text \
)
aws lambda add-permission --no-cli-pager \
--statement-id "AllowExecutionFromAPIGateway" \
--action lambda:InvokeFunction \
--function-name $INTEGRATION_URI \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:$REGION:$ACCOUNT_ID:$API_ID/*/*${ECHO_LAMBDA_ROUTE}"
wget -O- https://cdn.slashid.com/releases/latest/gate-free_latest_linux_arm64.tar.gz | tar xz
mv gate-free bootstrap
zip gate.zip bootstrap
aws lambda create-function --no-cli-pager \
--function-name $GATE_LAMBDA_NAME \
--handler gate.handler \
--runtime provided.al2 \
--role arn:aws:iam::${ACCOUNT_ID}:role/serverless_lambda \
--zip-file fileb://gate.zip \
--architectures arm64 \
--environment Variables="{GATE_MODE=aws_lambda_auth,GATE_LOG_LEVEL=debug,GATE_PLUGINS_0_TYPE=validate-jwt,GATE_PLUGINS_0_ENABLED=true,GATE_PLUGINS_0_PARAMETERS_JWKS_URL=https://api.slashid.com/.well-known/jwks.json,GATE_PLUGINS_0_PARAMETERS_JWT_ALLOWED_ALGORITHMS=RS256,GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_ISSUER=https://api.slashid.com,GATE_PLUGINS_0_PARAMETERS_JWT_EXPECTED_AUDIENCE=${ORGANIZATION_ID}}"
AUTHORIZER_ID=$(aws apigatewayv2 create-authorizer \
--api-id $API_ID \
--name $GATE_LAMBDA_NAME \
--authorizer-type REQUEST \
--authorizer-uri "arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:$GATE_LAMBDA_NAME/invocations" \
--identity-source '["$request.header.Authorization"]' \
--authorizer-payload-format-version 2.0 \
--enable-simple-responses \
--query 'AuthorizerId' \
--output text)
aws apigatewayv2 create-route --no-cli-pager \
--api-id $API_ID \
--route-key "GET ${ECHO_LAMBDA_ROUTE}" \
--authorization-type CUSTOM \
--authorizer-id $AUTHORIZER_ID \
--target "integrations/$INTEGRATION_ID"
aws lambda add-permission --no-cli-pager \
--function-name $GATE_LAMBDA_NAME \
--statement-id AllowAPIGatewayInvoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:${REGION}:${ACCOUNT_ID}:${API_ID}/authorizers/${AUTHORIZER_ID}"
aws apigatewayv2 create-stage --no-cli-pager \
--api-id $API_ID \
--stage-name $STAGE_NAME \
--auto-deploy
At the end of execution, script will print the URL to call the Lambda via the stage.
ENDPOINT_URL=$(aws apigatewayv2 get-apis --query 'Items[?Name==`'"$GATEWAY_NAME"'`].ApiEndpoint | [0]' --output text)
FULL_URL="${ENDPOINT_URL}/${STAGE_NAME}${ECHO_LAMBDA_ROUTE}"
echo "The URL to call the Lambda is: $FULL_URL"
Verifying deployment
Let's ensure that requests without a token are rejected:
> curl https://[API ID].execute-api.us-east-1.amazonaws.com/test/echo --header 'Cache-Control:"max-age=0"'
{"message":"Unauthorized"}
Now we can verify that Gate allows requests with a valid token. First, we need to create a valid token.
We will use the SlashID mint token endpoint.
You may have no persons in your organization yet. You can create a new person by using the create person endpoint or in the SlashID Console.
> curl --location --request POST 'https://api.slashid.com/persons/<PERSON ID>/mint-token' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'SlashID-API-Key: <YOUR API KEY>' \
--header 'SlashID-OrgID: <YOUR ORG ID>'
{
"result": "<TOKEN>"
}
Now, let's include the generated token in a request to the echo Lambda:
> curl https://[API ID].execute-api.us-east-1.amazonaws.com/test/echo \
--header 'Authorization:Bearer <TOKEN>' \
--header 'Cache-Control:max-age=0'
{
"event": {
"version": "1.0",
"routeKey": "",
"rawPath": "",
"rawQueryString": "",
"headers": {
"Content-Length": "0",
"Host": "<API ID>.execute-api.us-east-1.amazonaws.com",
"User-Agent": "curl/7.84.0",
"X-Amzn-Trace-Id": "Root=1-652015d6-10b9e01b65c0ef3a443d7fee",
"X-Forwarded-For": "[REQUEST IP]",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https",
"accept": "*/*",
"authorization": "Bearer <TOKEN>",
"cache-control": "max-age=0"
},
"requestContext": {
"routeKey": "",
"accountId": "<ACCOUNT ID>",
"stage": "test",
"requestId": "MYhZjgHNIAMES8g=",
"authorizer": {},
"apiId": "<API ID>",
"domainName": "<API ID>.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "<API ID>",
"time": "",
"timeEpoch": 0,
"http": {
"method": "",
"path": "",
"protocol": "",
"sourceIp": "",
"userAgent": ""
},
"authentication": {
"clientCert": {
"clientCertPem": "",
"issuerDN": "",
"serialNumber": "",
"subjectDN": "",
"validity": {
"notAfter": "",
"notBefore": ""
}
}
}
},
"isBase64Encoded": false
},
"message": "Hello World!"
}
You should check the Gate logs (gate_lambda_logs
Terraform output) if it didn't work.
Remember that it takes AWS a couple of seconds to expose logs in their UI.
If you can't find a reason for failure, you can change the log level by changing the GATE_LOG_LEVEL
env variable to trace
.
Cleaning up
Instructions to remove created resources.
- Terraform
- AWS CLI
Run terraform destroy
.
To remove created resources, please run this script.
# Delete API Gateway Stage
aws apigatewayv2 delete-stage \
--api-id $API_ID \
--stage-name test
# Remove Lambda permission for Gate Lambda
aws lambda remove-permission \
--function-name $GATE_LAMBDA_NAME \
--statement-id AllowAPIGatewayInvoke
# Delete API Gateway Route
aws apigatewayv2 delete-route \
--api-id $API_ID \
--route-id $(aws apigatewayv2 get-routes --api-id $API_ID --query 'Items[?RouteKey==`GET '"$ECHO_LAMBDA_ENDPOINT"'`].RouteId | [0]' --output text)
# Delete API Gateway Authorizer
aws apigatewayv2 delete-authorizer \
--api-id $API_ID \
--authorizer-id $AUTHORIZER_ID
# Delete Gate Lambda function
aws lambda delete-function \
--function-name $GATE_LAMBDA_NAME
# Remove Lambda permission for Echo Lambda
aws lambda remove-permission \
--function-name $ECHO_LAMBDA_NAME \
--statement-id AllowExecutionFromAPIGateway
# Delete API Gateway Integration
aws apigatewayv2 delete-integration \
--api-id $API_ID \
--integration-id $INTEGRATION_ID
# Delete API Gateway
aws apigatewayv2 delete-api \
--api-id $API_ID
# Delete Echo Lambda function
aws lambda delete-function \
--function-name $ECHO_LAMBDA_NAME
# Detach IAM Policies
aws iam detach-role-policy --role-name serverless_lambda --policy-arn $POLICY_ARN
aws iam detach-role-policy --role-name serverless_lambda --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Delete IAM policy
aws iam delete-policy \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/lambda_logging
# Delete IAM role
aws iam delete-role \
--role-name serverless_lambda
# Remove local files
rm echo-lambda.zip gate.zip bootstrap
Troubleshooting
If you are stuck, please check the Troubleshooting page.
Further reading
In this tutorial, we covered just the basic configuration. For a more detailed walkthrough on deploying Gate as a Lambda Authorizer, you can read the full Lambda Authorizer Guide. For more advanced configuration options, you can check the Advanced Configuration page.