I have the following Cloudformation template that deploys a HTTP API gateway with a custom Lambda authorizer that delivers to an SQS queue. I process the messages in the queue with another lambda that is not shown here. When I deploy this template without Authorization enabled (setting AuthorizationType
from CUSTOM
to NONE
in the route, and commenting out AuthorizerId: !Ref APIAuthorizer
) the whole thing works and I see my message flow from API Gateway through SQS and to the subscribed Lambda for processing. However, when I enable Authorization, I see my message enter the Auth Lambda and then receive Internal Server Error in Postman.
There is clearly a problem with the custom auth, however, I cannot find the cause of the problem after a few days of trying different things.
My understanding is that I only need to authorise the routeArn in the returned policy, however, perhaps I need to do something for SQS permissions too?
Here is my template (excluding the final lambda).
Queue:
Type: AWS::SQS::Queue
Properties:
QueueName: my-queue
QueuePolicy:
DependsOn: ["Queue"]
Type: AWS::SQS::QueuePolicy
Properties:
PolicyDocument:
Statement:
- Action: SQS:*
Effect: Allow
Principal: '*'
Resource: !GetAtt Queue.Arn
Version: '2012-10-17'
Queues:
- !Ref Queue
ApiGatewayToSQSRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole
RoleName: ApiGatewayToSQSRole
Policies:
- PolicyName: ApiGatewayLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action: sqs:SendMessage
Effect: Allow
Resource: !GetAtt 'Queue.Arn'
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:DescribeLogGroups
- logs:DescribeLogStreams
- logs:PutLogEvents
- logs:GetLogEvents
- logs:FilterLogEvents
Effect: Allow
Resource: "*"
ApiGateway:
Type: 'AWS::ApiGatewayV2::Api'
DeletionPolicy: Delete
Properties:
Name: "API Gateway to SQS"
ProtocolType: 'HTTP'
ApiGatewayStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref ApiGateway
StageName: dev
AutoDeploy: true
Integration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref ApiGateway
CredentialsArn: !GetAtt ApiGatewayToSQSRole.Arn
PayloadFormatVersion: "1.0"
IntegrationType: AWS_PROXY
IntegrationSubtype: SQS-SendMessage
RequestParameters:
QueueUrl: !Ref Queue
MessageBody: $request.body
Route:
Type: AWS::ApiGatewayV2::Route
DependsOn:
- ApiGateway
- Integration
Properties:
ApiId: !Ref ApiGateway
RouteKey: 'POST /send'
AuthorizationType: CUSTOM
AuthorizerId: !Ref APIAuthorizer
Target: !Sub integrations/${Integration}
APIAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
Name: APIAuthorizer
ApiId: !Ref ApiGateway
AuthorizerType: REQUEST
AuthorizerUri: !Join
- ""
- - "arn:"
- !Ref "AWS::Partition"
- ":apigateway:"
- !Ref "AWS::Region"
- ":lambda:path/2015-03-31/functions/"
- !GetAtt AuthorizerFunction.Arn
- /invocations
AuthorizerResultTtlInSeconds: 300
AuthorizerPayloadFormatVersion: 2.0
EnableSimpleResponses: true
IdentitySource:
- $request.header.authorization
AuthorizePermission:
Type: AWS::Lambda::Permission
DependsOn:
- ApiGateway
- AuthorizerFunction
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref AuthorizerFunction
Principal: apigateway.amazonaws.com
AuthHandlerServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: 'sts:AssumeRole'
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: 2012-10-17
ManagedPolicyArns:
- !Join
- ''
- - 'arn:'
- !Ref 'AWS::Partition'
- ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
AuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: authorizer-lambda
Role: !GetAtt [AuthHandlerServiceRole, Arn]
CodeUri: functions/core_authorizer_lambda
Handler: index.handler
Runtime: python3.11
Tags:
Name: authorizer-lambda
project: my-project
here is my authorizer lambda function
def generate_policy(principal_id: Union[int, str, None], effect: str, resource: str) -> dict:
""" return a valid AWS policy response """
auth_response = {'principalId': principal_id}
if effect and resource:
policy_document = {
'Version': '2012-10-17',
'Statement': [
{
'Sid': 'InvokeAPIStatement',
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}
]
}
auth_response['policyDocument'] = policy_document
return auth_response
def handler(event, context) -> dict:
try:
print("event:", event)
print("context:", context)
route_arn = event.get('routeArn')
return generate_policy('me', 'Allow', route_arn)
except Exception as e:
logging.exception(e)
return {
'statusCode': 500
}
I invoke this with
{
"body": "Hey aws! How are you today?",
"headers": {
"Accept": "application/json",
"authorization": "Bearer <token>"
}
}
** EDIT **
Following advice, I modified the Lambda handler to
def handler(event, context) -> dict:
try:
print("event:", event)
print("context:", context)
route_arn = event.get('routeArn')
method_arn = event.get('methodArn')
print(f"ROUTE_ARN: {route_arn}")
print(f"METHOD_ARN: {method_arn}")
result = generate_policy('me', 'Allow', route_arn)
print(f"ALLOW: {result}")
return result
except Exception as e:
result = generate_policy(None, 'Deny', route_arn)
print(f"DENY: {result}")
return result
and observed the following print statements in CloudWatch logs.
event: {
"version": "2.0",
"type": "REQUEST",
"routeArn": "arn:aws:execute-api:us-east-1:redacted:redacted/dev/POST/send",
"identitySource": [
"Bearer redacted"
],
"routeKey": "POST /send",
"rawPath": "/dev/send",
"rawQueryString": "",
"headers": {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"authorization": "Bearer redacted",
"content-length": "266",
"content-type": "application/json",
"host": "redacted.execute-api.us-east-1.amazonaws.com",
"postman-token": "redacted",
"user-agent": "PostmanRuntime/7.36.0",
"x-amzn-trace-id": "redacted",
"x-forwarded-for": "redacted",
"x-forwarded-port": "443",
"x-forwarded-proto": "https"
},
"requestContext": {
"accountId": "redacted",
"apiId": "redacted",
"domainName": "redacted.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "redacted",
"http": {
"method": "POST",
"path": "/dev/send",
"protocol": "HTTP/1.1",
"sourceIp": "redacted",
"userAgent": "PostmanRuntime/7.36.0"
},
"requestId": "redacted",
"routeKey": "POST /send",
"stage": "dev",
"time": "22/Dec/2023:15:17:58 +0000",
"timeEpoch": 1703258278409
}
}
"ROUTE_ARN: arn:aws:execute-api:us-east-1:redacted:redacted/dev/POST/send"
"METHOD_ARN: None"
ALLOW: {"principalId": "me", "policyDocument": {"Version": "2012-10-17", "Statement": [{"Sid": "InvokeAPIStatement", "Action": "execute-api:Invoke", "Effect": "Allow", "Resource": "arn:aws:execute-api:us-east-1:redacted:redacted/dev/POST/send"}]}}
I have tried different template settings and also simplifying everything as much as possible.
2
Answers
OK so thanks everyone for your help and suggestions. It transpires that it was not related to the above issues but was due to the payload format version in API Gateway. I had elected two optional options (without properly knowing what they were doing or their consequences). These were
PayloadFormatVersion
andEnableSimpleResponses: true
.PayloadFormatVersion
can be either 1.0 or 2.0. The chief difference is the switch frommethodARN
in 1.0 torouteArn
in 2.0.Following this
EnableSimpleResponses: true
means that you shouldn't return a policy document from the handler, but instead must return thisLesson --> don't update payload versions without research!
Make your generate_policy function always return a valid policy that is set to either Allow or Deny. In yours, there is an IF condition, which when it evaluates to false, it will not return a valid policy.
Do something like this instead:
Then make the handler return it directly:
Then examine the logs and make sure what you see as the generated policy is the correct syntax and has the correct principalId, effect, and resource.