Search code examples
amazon-web-servicesaws-lambdaaws-cloudformationaws-api-gatewayamazon-sqs

API Gateway Custom Authorizer Internal Server Error


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.


Solution

  • 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!