Search code examples
aws-lambdaaws-cloudformationaws-api-gatewayaws-sam

SAM Template - define HttpApi with Lambda Authorizer and Simple Response


Description of the problem

I have created a Lambda function with API Gateway in SAM, then deployed it and it was working as expected. In API Gateway I used HttpApi not REST API.

Then, I wanted to add a Lambda authorizer with Simple Response. So, I followed the SAM and API Gateway docs and I came up with the code below.

When I call the route items-list it now returns 401 Unauthorized, which is expected.

However, when I add the header myappauth with the value "test-token-abc", I get a 500 Internal Server Error.

I checked this page but it seems all of the steps listed there are OK https://aws.amazon.com/premiumsupport/knowledge-center/api-gateway-http-lambda-integrations/

I enabled logging for the API Gateway, following these instructions: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging.html

But all I get is something like this (redacted my IP and request ID):

[MY-IP] - - [07/Jul/2021:08:24:06 +0000] "GET GET /items-list/{userNumber} HTTP/1.1" 500 35 [REQUEST-ID]

(Perhaps I can configure the logger in such a way that it prints a more meaningful error message? EDIT: I've tried adding $context.authorizer.error to the logs, but it doesn't print any specific error message, just prints a dash: -)

I also checked the logs for the Lambda functions, there is nothing there (all logs where from the time before I added the authorizer). So, what am I doing wrong?

What I tried:

This is my Lambda Authorizer function which I have deployed using sam deploy, when I test it in isolation using an event with the myappauth header, it works:

exports.authorizer = async (event) => {
    let response = {
        "isAuthorized": false,
    };

    if (event.headers.myappauth === "test-token-abc") {
        response = {
            "isAuthorized": true,
        };
    }

    return response;

};

and this is the SAM template.yml which I deployed using sam deploy:

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  myapp-v1

Transform:
  - AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: nodejs14.x
    MemorySize: 128
    Timeout: 100
    Environment:
      Variables:
        MYAPP_TOKEN: "test-token-abc"

Resources:
  MyAppAPi:
    Type: AWS::Serverless::HttpApi
    Properties:
      FailOnWarnings: true
      Auth:
        Authorizers:
          MyAppLambdaAuthorizer:
            AuthorizerPayloadFormatVersion: "2.0"
            EnableSimpleResponses: true
            FunctionArn: !GetAtt authorizerFunction.Arn
            FunctionInvokeRole: !GetAtt authorizerFunctionRole.Arn
            Identity:
              Headers:
                - myappauth
        DefaultAuthorizer: MyAppLambdaAuthorizer

  itemsListFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/v1-handlers.itemsList
      Description: A Lambda function that returns a list of items.
      Policies:
        - AWSLambdaBasicExecutionRole
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /items-list/{userNumber}
            Method: get
            ApiId: MyAppAPi

  authorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/v1-handlers.authorizer
      Description: A Lambda function that authorizes requests.
      Policies:
        - AWSLambdaBasicExecutionRole

Edit:

User @petey suggested that I tried returning an IAM policy in my authorizer function, so I changed EnableSimpleResponses to false in the template.yml, then I changed my function as below, but got the same result:

exports.authorizer = async (event) => {
    let response = {
        "principalId": "my-user",
        "policyDocument": {
            "Version": "2012-10-17",
            "Statement": [{
                "Action": "execute-api:Invoke",
                "Effect": "Deny",
                "Resource": event.routeArn
            }]
        }
    };

    if (event.headers.myappauth == "test-token-abc") {
        response = {
            "principalId": "my-user",
            "policyDocument": {
                "Version": "2012-10-17",
                "Statement": [{
                    "Action": "execute-api:Invoke",
                    "Effect": "Allow",
                    "Resource": event.routeArn
                }]
            }
        };
    }

    return response;

};

Solution

  • I am going to answer my own question because I have resolved the issue, and I hope this will help people who are going to use the new "HTTP API" format in API Gateway, since there is not a lot of tutorials out there yet; most examples you will find online are for the older API Gateway standard, which Amazon calls "REST API". (If you want to know the difference between the two, see here).

    The main problem lies in the example that is presented in the official documentation. They have:

      MyLambdaRequestAuthorizer:
        FunctionArn: !GetAtt MyAuthFunction.Arn
        FunctionInvokeRole: !GetAtt MyAuthFunctionRole.Arn
    

    The problem with this, is that this template will create a new Role called MyAuthFunctionRole but that role will not have all the necessary policies attached to it!

    The crucial part that I missed in the official docs is this paragraph:

    You must grant API Gateway permission to invoke the Lambda function by using either the function's resource policy or an IAM role. For this example, we update the resource policy for the function so that it grants API Gateway permission to invoke our Lambda function.

    The following command grants API Gateway permission to invoke your Lambda function. If API Gateway doesn't have permission to invoke your function, clients receive a 500 Internal Server Error.

    The best way to solve this, is to actually include the Role definition in the SAM template.yml, under Resources:

    MyAuthFunctionRole
      Type: AWS::IAM::Role
      Properties: 
        # [... other properties...]
        AssumeRolePolicyDocument:
           Version: "2012-10-17"
           Statement:
             - Effect: Allow
               Principal:
                 Service:
                   - apigateway.amazonaws.com
               Action:
                 - 'sts:AssumeRole'
        Policies: 
          # here you will put the InvokeFunction policy, for example:
          - PolicyName: MyPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action: 'lambda:InvokeFunction'
                  Resource: !GetAtt MyAuthFunction.Arn
    

    You can see here a description about the various Properties for a role: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html

    Another way to solve this, is to separately create a new policy in AWS Console, which has InvokeFunction permission, and then after deployment, attach that policy to the MyAuthFunctionRole that SAM created. Now the Authorizer will be working as expected.

    Another strategy would be to create a new role beforehand, that has a policy with InvokeFunction permission, then copy and paste the arn of that role in the SAM template.yml:

    MyLambdaRequestAuthorizer:
        FunctionArn: !GetAtt MyAuthFunction.Arn
        FunctionInvokeRole: arn:aws:iam::[...]