SAM Template - define HttpApi with Lambda Authorizer and Simple Response
Asked Answered
D

3

6

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;

};
Dipteral answered 7/7, 2021 at 8:46 Comment(0)
D
10

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::[...]
Dipteral answered 19/7, 2021 at 5:46 Comment(2)
what policy did you attach to your role?Halley
You need to create a policy with InvokeFunction see here for an example docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/… but since you probably want to access all functions, in the Resource you should put just an asterisk: "Resource": "*"Dipteral
S
2

Just to complete the answer. You have to add an AssumeRolePolicyDocument under Properties.

The role will then state

  MyAuthFunctionRole:
  Type: AWS::IAM::Role
  Properties:
     AssumeRolePolicyDocument:
       Version: "2012-10-17"
       Statement:
         - Effect: Allow
           Principal:
             Service:
               - apigateway.amazonaws.com
           Action:
             - 'sts:AssumeRole'
  Policies:
     # see answer above
Stuart answered 18/7, 2022 at 6:50 Comment(1)
Thank you @Tom-Erik, I have added this to my answer!Dipteral
X
1

Your lambda authorizer is not returning what is expected to be an actual lambda authorizer (an IAM policy). This could explain that internal error 500.

To fix, replace is with something like this that returns an IAM policy (or rejects):

// A simple token-based authorizer example to demonstrate how to use an authorization token 
// to allow or deny a request. In this example, the caller named 'user' is allowed to invoke 
// a request if the client-supplied token value is 'allow'. The caller is not allowed to invoke 
// the request if the token value is 'deny'. If the token value is 'unauthorized' or an empty
// string, the authorizer function returns an HTTP 401 status code. For any other token value, 
// the authorizer returns an HTTP 500 status code. 
// Note that token values are case-sensitive.

exports.handler =  function(event, context, callback) {
    var token = event.authorizationToken;
    // modify switch statement here to your needs
    switch (token) {
        case 'allow':
            callback(null, generatePolicy('user', 'Allow', event.methodArn));
            break;
        case 'deny':
            callback(null, generatePolicy('user', 'Deny', event.methodArn));
            break;
        case 'unauthorized':
            callback("Unauthorized");   // Return a 401 Unauthorized response
            break;
        default:
            callback("Error: Invalid token"); // Return a 500 Invalid token response
    }
};

// Help function to generate an IAM policy
var generatePolicy = function(principalId, effect, resource) {
    var authResponse = {};
    
    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; 
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; 
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }
    
    // Optional output with custom properties of the String, Number or Boolean type.
    authResponse.context = {
        "stringKey": "stringval",
        "numberKey": 123,
        "booleanKey": true
    };
    return authResponse;
}

Lots more information here : https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-lambda-function-create

Xavier answered 7/7, 2021 at 16:44 Comment(1)
Your example is for REST API but I am using HTTP API which is described here. I'm using Simple Response so the IAM policy is not needed. Anyway I tried turning EnableSimpleResponses to false and then returned an IAM policy, in the format expected by HTTP API as described here but I'm still getting the same result unfortunately. I updated the answer to reflect this.Dipteral

© 2022 - 2024 — McMap. All rights reserved.