Simple serverless architecture using API Gateway, Lambda Authorizer and Secrets Manager

Andrii Shykhov
4 min readNov 3, 2023

--

Introduction:

In most cases, it is necessary to protect API Gateway. AWS offers a couple of ways to do it. In this post, you can find a simple way to control access to API Gateway through Lambda authorizer with a Simple response.

About the project:

In this project, we have API Gateway, 2 lambda functions — the “Authorizer” lambda function and “Main” lambda function, Secret Manager for storing authorization token. The main lambda function returns a simple response in case of a successful authorization with the Authorizer lambda. The Authorizer lambda function plays the role of access blocker. The lambda function checks a token passed in the client’s HTTP request and takes the token from Secrets Manager, after that it determines whether the tokens are identical. If yes — returns “true” in response, if no — returns “false”. All infrastructure in AWS is created with CloudFormation.

Token value is passed as a parameter during the process of creation of CloudFormation stack, but it is possible to create token dynamically, more information in AWS documentation.

The infrastructure schema:

The CloudFormation code template:

Parameters:
AuthorizationTokenName:
Type: String
Description: Authorization token resource name
Default: 'AuthorizationToken'
AuthorizationTokenValue:
Type: String
Description: Authorization token value
Default: ''

Resources:
#####################################
# Secret Manager
#####################################
AuthorizationSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Ref AuthorizationTokenName
Description: Access token for Authorization Lambda
SecretString:
Fn::Sub:
- '{"token": "${AuthorizationTokenValue}"}'
- AuthorizationTokenValue: !Ref AuthorizationTokenValue

#####################################
# Lambda Functions
#####################################
AuthorizationLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: AuthorizationLambdaFunction
Runtime: python3.10
Handler: index.lambda_handler
Role: !GetAtt AuthorizationLambdaExecutionRole.Arn
Environment:
Variables:
AuthorizationTokenName: !Ref AuthorizationTokenName
Code:
ZipFile: |
import boto3
from botocore.exceptions import ClientError
import json
import os

def lambda_handler(event, context):

secret_name = os.environ['AuthorizationTokenName']
region_name = context.invoked_function_arn.split(":")[3]

# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
# Take the token value from the responce
secret = get_secret_value_response['SecretString']
secret_token = json.loads(secret)['token']
authorization_token = event.get('headers', {}).get('authorization')

# Check if the authorization token match with secret manager token
is_authorized = authorization_token in secret_token

# Construct the response
response = {
"isAuthorized": is_authorized
}
return response
except ClientError as e:
raise e

AuthorizationLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: AuthorizationLambdaExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SecretsManagerAccessPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- secretsmanager:DescribeSecret
Resource:
- !GetAtt AuthorizationSecret.Id
- PolicyName: ApiGatewayInvokePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyApi}/*/*/invoke

MainLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: MainLambdaFunction
Runtime: python3.10
Handler: index.lambda_handler
Role: !GetAtt MainLambdaExecutionRole.Arn
Code:
ZipFile: |
import json

def lambda_handler(event, context):
response = {
"statusCode": 200,
"body": json.dumps("Simple Main lambda function responce")
}
return response

MainLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: MainLambdaExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

#####################################
# API Gateway
#####################################
MyApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: HttpApi4328
ProtocolType: HTTP

HttpApiGatewayAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
Name: HttpApiGatewayAuthorizer
ApiId: !Ref MyApi
AuthorizerType: REQUEST
EnableSimpleResponses: YES
AuthorizerUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizationLambdaFunction.Arn}/invocations
AuthorizerResultTtlInSeconds: 0
AuthorizerPayloadFormatVersion: '2.0'
AuthorizerCredentialsArn: !GetAtt ApiGatewayInvokeLambdaRole.Arn
IdentitySource:
- "$request.header.Authorization"

ApiGatewayInvokeLambdaRole:
Type: AWS::IAM::Role
DependsOn:
- AuthorizationLambdaFunction
- MainLambdaFunction
- AuthorizationSecret
Properties:
RoleName: ApiGatewayInvokeLambdaRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: AuthorizerPermissionsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
- sts:AssumeRole
Resource:
- !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AuthorizationLambdaFunction}"
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${AuthorizationSecret}"
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyApi}/*/*/invoke

HttpApiGateway:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref MyApi
StageName: dev
AutoDeploy: true

HttpApiGatewayRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref MyApi
RouteKey: ANY /invoke
AuthorizationType: CUSTOM
AuthorizerId: !Ref HttpApiGatewayAuthorizer
Target: !Join
- '/'
- - integrations
- !Ref MyHttpApiIntegration

MyHttpApiIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref MyApi
IntegrationType: AWS_PROXY
PayloadFormatVersion: '2.0'
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MainLambdaFunction.Arn}/invocations

MainLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt MainLambdaFunction.Arn
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyApi}/*/*/invoke

Prerequisites:

Before you start, make sure the following requirements are met:
- An AWS account with permissions to create resources.
- AWS CLI installed on your local machine.

Deployment:

1. Clone the repository:

https://gitlab.com/Andr1500/apigw_authentication_lambda.git

2. Fill in all necessary parameters in the root.yaml, retrieve_invoke_url.sh files, and create a CloudFormation stack with a CloudFormation template:

aws cloudformation create-stack \
--stack-name apigw-lambda-sm \
--template-body file://root.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=AuthorizationTokenValue,ParameterValue="token_value" \
--region eu-central-1 \
--disable-rollback

3. Retrieve the Invoke URL of the Stage with retrieve_invoke_url.sh script and make a simple test of the API with the CURL command:

./retrieve_invoke_url.sh 
Invoke URL: https://api_id.execute-api.eu-central-1.amazonaws.com/dev

export APIGW_TOKEN='token_value'
curl -X GET -H "Authorization: $APIGW_TOKEN" https://api_id.execute-api.eu-central-1.amazonaws.com/dev/invoke
"Simple Main lambda function responce"

curl -X GET -H "Authorization: INCORRECT_TOKEN" https://api_id.execute-api.eu-central-1.amazonaws.com/dev/invoke
{"message":"Forbidden"}

4. Delete the CloudFormation stack:

aws cloudformation delete-stack --stack-name apigw-lambda-sm

Conclusion:

Lambda Authorizer with a simple response can be a fast and simple solution for securing API Gateway. Lambda function as Authorizer gives us a high level of customization — we can use different services for storing credentials and not only AWS services, we can configure different logic of the function code work depending on our requirements.

If you found this post helpful and interesting, please click the clap button below to show your support.
You can also support me with a virtual coffee https://www.buymeacoffee.com/andrworld1500 .

--

--