Building Secure APIs with Lambda, API Gateway, and Authorizer using AWS CloudFormation
Introduction:
APIs are a crucial part of modern application development, enabling communication between different systems and facilitating seamless data exchange. However, securing APIs is equally important to protect sensitive data and ensure that only authorized users can access the resources. In this blog post, we’ll explore how to build secure APIs with Lambda, API Gateway, and an authorizer using AWS CloudFormation.
Prerequisites:
Before diving into the implementation, make sure you have an AWS account and basic knowledge of AWS services like Lambda, API Gateway, and CloudFormation.
Step 1: Defining the CloudFormation Stack
To automate the creation of the required resources, we’ll use AWS CloudFormation. Below is an example CloudFormation template that defines the stack:
AWSTemplateFormatVersion: 2010-09-09
Description: API Gateway, Authorizer and Lambda function with IAM role & policy
Parameters:
ApiGatewayHTTPMethod:
Type: String
Default: POST
Env:
Type: String
Default: dev
Prefix:
Type: String
Default: sri
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["_", [Ref: "Prefix", "LambdaRole", Ref: "Env"]]
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Join ["_", [Ref: "Prefix", "LambdaPolicy", Ref: "Env"]]
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Effect: Allow
Resource: "*"
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Join ["_", [Ref: "Prefix", Ref: "Env"]]
Description: !Join ["_", [Ref: "Prefix","LambdaFunction", Ref: "Env"]]
Runtime: python3.8
MemorySize: 128
Role: !GetAtt LambdaRole.Arn
Handler: index.handler
Code:
ZipFile: |
import json
def handler(event, context):
request_body = json.loads(event['body'])
name = request_body.get('name')
print(f"Hello, {name}!")
return {
'statusCode': 200,
'body': json.dumps({'message': f"Hello, {name}!"})
}
LambdaPermission:
DependsOn: LambdaFunction
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt LambdaFunction.Arn
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Join ["", ["arn:aws:execute-api:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":", Ref: "ApiGateway", "/*"]]
LambdaLogGroup:
DependsOn: LambdaFunction
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Join ['', ['/aws/lambda/', !Ref LambdaFunction]]
RetentionInDays: 30
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: !Join ["_", [Ref: "Prefix", Ref: "Env"]]
Description: !Join ["_", [Ref: "Prefix", "APIGateway", Ref: "Env"]]
EndpointConfiguration:
Types:
- REGIONAL
AuthorizerLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["_", [Ref: "Prefix", "AuthorizerLambdaRole", Ref: "Env"]]
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- apigateway.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: !Join ["_", [Ref: "Prefix", "AuthorizerLambdaPolicy", Ref: "Env"]]
PolicyDocument:
Version: 2012-10-17
Statement:
- Action: "lambda:InvokeFunction"
Effect: Allow
Resource: "*"
AuthorizerLambdaFunction:
DependsOn:
- LambdaRole
Type: AWS::Lambda::Function
Properties:
FunctionName: !Join ["_", [Ref: "Prefix", "AuthorizerLambda", Ref: "Env"]]
Runtime: python3.8
MemorySize: 128
Role: !GetAtt LambdaRole.Arn
Handler: index.handler
Code:
ZipFile: |
def handler(event, context):
print(event)
# validate the token
if event['authorizationToken'] == 'srikanth@1223334444':
auth = 'Allow'
else:
auth = 'Deny'
# return the response
authResponse = { "principalId": "abc123", "policyDocument": { "Version": "2012-10-17", "Statement": [{"Action": "execute-api:Invoke", "Resource": "*", "Effect": auth}] }}
return authResponse
ApiAuthorizer:
DependsOn:
- ApiGateway
- AuthorizerLambdaRole
- AuthorizerLambdaFunction
Type: AWS::ApiGateway::Authorizer
Properties:
Name: !Join ["_", [Ref: "Prefix", "BasicAuthorizer", Ref: "Env"]]
Type: TOKEN
AuthorizerCredentials: !GetAtt AuthorizerLambdaRole.Arn
IdentitySource: method.request.header.authorizationToken
AuthorizerUri: !Join ["", ["arn:aws:apigateway:" , Ref: AWS::Region , ":lambda:path/2015-03-31/functions/", !GetAtt AuthorizerLambdaFunction.Arn, "/invocations"] ]
AuthorizerResultTtlInSeconds: 300
RestApiId: !Ref ApiGateway
RestMethod:
DependsOn:
- LambdaPermission
- LambdaFunction
- ApiGateway
- AuthorizerLambdaFunction
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: !Ref ApiGatewayHTTPMethod
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri: !Sub
- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations
- lambdaArn: !GetAtt LambdaFunction.Arn
ResourceId: !GetAtt ApiGateway.RootResourceId
RestApiId: !Ref ApiGateway
AuthorizationType: CUSTOM
AuthorizerId: !Ref ApiAuthorizer
ApiGatewayDeployment:
DependsOn: RestMethod
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref ApiGateway
StageName: !Ref Env
Outputs:
ApiInvokeURL:
Description: Invoke this URL in Postman or cURL to access the API Gateway
Value: !Join ['', ['https://', !Ref ApiGateway, '.execute-api.', !Ref 'AWS::Region', '.amazonaws.com/', !Ref Env]]
Copy the provided CloudFormation template above and paste it into a new file named ‘serverless-api.yaml’.
Step 2: Understanding the Stack
Let’s break down the CloudFormation template and understand the purpose of each resource:
- LambdaRole: This IAM role allows the Lambda function to assume the role and includes permissions to create and write logs.
- LambdaFunction: Here, we define the Lambda function, its configuration, and the code that will be executed when the function is invoked.
- LambdaPermission: This resource grants the API Gateway permission to invoke the Lambda function.
- LambdaLogGroup: It creates a CloudWatch Logs log group to store logs generated by the Lambda function.
- ApiGateway: This resource sets up the API Gateway REST API, providing a name and description.
- AuthorizerLambdaRole: The IAM role for the authorizer Lambda function allows both the Lambda and API Gateway services to assume the role and invoke any Lambda function.
- AuthorizerLambdaFunction: This resource defines the authorizer Lambda function, which validates the incoming token and returns an authorization response.
- ApiAuthorizer: The API Gateway authorizer resource specifies the authorizer type, credentials, identity source, and result cache TTL.
- RestMethod: It creates an API Gateway method that integrates with the Lambda function and enables custom authorization using the authorizer.
- ApiGatewayDeployment: This resource deploys the API Gateway REST API.
Step 3: Deploying the Stack
To deploy the CloudFormation stack, follow these steps:
- Access the AWS Management Console and open the CloudFormation service.
2. Click on ‘Create stack’ and choose ‘With new resources’
3. Upload the CloudFormation template which we have saved in serverless-api.yaml
4. Provide stack name and configure any desired parameters, such as the API Gateway HTTP method, environment, and prefix.
5. Before deploying the CloudFormation template, ensure you acknowledge and review the required capabilities and permissions for IAM resources to ensure a secure deployment.
Step 4: Testing the API
Once the CloudFormation stack is deployed successfully, you can test the API by invoking the URL provided in the stack outputs.
- Open Postman and set the HTTP method to POST.
- Set the API endpoint to the URL provided in the CloudFormation stack outputs (ApiInvokeURL). This URL represents the endpoint of your API Gateway.
- In the request body, provide a JSON object with a key-value pair where the key is “name” and the value is your name (e.g., “srikanth”)
- Send the request and observe the response. Initially, you will receive a 401 Unauthorized response
Understanding the 401 Unauthorized Response:
- The 401 Unauthorized response indicates that an authorizer is attached to the API Gateway, and the request needs to be authorized before accessing the resource.
Adding Authorization in Postman:
- In Postman, go to the “Authorization” tab.
- Select “API Key” as the authorization type.
- Set the “Key” field to “authorizationToken” and the “Value” field to “srikanth@1223334444” .
- Trigger the API request again, and you should receive a 200 response with the message “Hello” followed by the name you provided in the request body.
Handling Unauthorized Requests:
To understand the effect of incorrect authorization, try the following scenarios
- Use a valid “Key” field but modify it slightly (authorizationToken1) and a valid value (srikanth@1223334444). This will result in a 401 Unauthorized response.
- Use the correct “Key” field (authorizationToken) and a valid value but modify it slightly. This will result in a 403 Forbidden response with the message “User is not authorized to access this resource with an explicit deny.”
By following these steps, you can test the deployed API Gateway using Postman and observe the effects of authorization on the responses. This will help you understand how the authorizer validates and controls access to your API.
Conclusion:
In this blog post, we explored how to build secure APIs with Lambda, API Gateway, and an authorizer using AWS CloudFormation. We learned about the components involved, such as the Lambda function, API Gateway, and authorizer, and how they work together to provide secure access to API resources. By leveraging CloudFormation, we can automate the creation and management of these resources, making it easier to deploy and maintain secure APIs in AWS.
Serverless architectures offer numerous benefits, such as scalability, reduced operational overhead, and cost optimization. With AWS Lambda and API Gateway, developers can build highly performant and flexible APIs without worrying about server management.
References: