skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 and EnableSimpleResponses: true.

    PayloadFormatVersion can be either 1.0 or 2.0. The chief difference is the switch from methodARN in 1.0 to routeArn in 2.0.

    Following this EnableSimpleResponses: true means that you shouldn't return a policy document from the handler, but instead must return this

    {
      "isAuthorized": true/false,
      "context": {
        "exampleKey": "exampleValue"
      }
    }
    

    Lesson --> don't update payload versions without research!


  2. 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:

    import json
    
    def generate_policy(effect: str, principal: str, resource: str):
        """ Builds a policy in the format API Gateway expects the authorizer to return """
        data = {
            'principalId': principal,
            'policyDocument': {
                'Version': '2012-10-17',
                'Statement': [
                    {
                        'Action': 'execute-api:Invoke',
                        'Effect': effect,
                        'Resource': resource
                    }
                ]
            }
        }
        print(json.dumps(data))
        return data
    

    Then make the handler return it directly:

    def handler(event, context) -> dict:
        ...
        return generate_policy(effect, principal, resource)
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search