SAM 模板 - 使用 Lambda Authorizer 和 Simple Response 定义 HttpApi

SAM Template - define HttpApi with Lambda Authorizer and Simple Response

问题描述

我在 SAM 中使用 API 网关创建了一个 Lambda 函数,然后部署它并且它按预期工作。在 API 网关中我使用了 HttpApi 而不是 REST API.

然后,我想添加一个带有简单响应的 Lambda 授权方。所以,我遵循了 SAM 和 API 网关文档,并想出了下面的代码。

当我现在调用路由 items-list 时 returns 401 Unauthorized,这是预期的。

但是,当我添加 header myappauth 和值 "test-token-abc" 时,我得到 500 Internal Server Error.

我检查了这个页面,但似乎那里列出的所有步骤都没有问题 https://aws.amazon.com/premiumsupport/knowledge-center/api-gateway-http-lambda-integrations/

我按照以下说明为 API 网关启用了日志记录:https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging.html

但我得到的只是这样的东西(编辑了我的 IP 和请求 ID):

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

(也许我可以以打印更有意义的错误消息的方式配置记录器?编辑:我尝试将 $context.authorizer.error 添加到日志中,但它不会打印任何特定错误消息,只打印破折号:-)

我还检查了 Lambda 函数的日志,那里什么也没有(所有日志都来自我添加授权方之前的时间)。 那么,我做错了什么?

我尝试了什么:

这是我使用 sam deploy 部署的 Lambda 授权器函数,当我使用带有 myappauth header 的 event 对其进行隔离测试时,它有效:

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

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

    return response;

};

这是我使用 sam deploy:

部署的 SAM template.yml
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

编辑:

用户@petey 建议我尝试在我的授权函数中返回一个 IAM 策略,所以我在 template.yml 中将 EnableSimpleResponses 更改为 false,然后我将我的函数更改如下,但得到了相同的结果:

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;

};

您的 lambda 授权方未返回预期的实际 lambda 授权方(IAM 策略)。这可以解释内部错误 500.

要修复,请替换为 returns IAM 策略(或拒绝)这样的内容:

// 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;
}

这里有更多信息:https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-lambda-function-create

我将回答我自己的问题,因为我已经解决了这个问题,我希望这对那些打算在 API 网关中使用新的“HTTP API”格式的人有所帮助,因为目前还没有很多教程;您在网上找到的大多数示例都是针对较旧的 API 网关标准,Amazon 将其称为“REST API”。 (如果想知道两者的区别,see here)。

主要问题出在example that is presented in the official documentation。他们有:

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

问题在于此模板将创建一个名为 MyAuthFunctionRole 的新角色,但该角色不会附加所有必要的策略!

我在官方文档中遗漏的关键部分是 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.

解决此问题的最佳方法是在 SAM template.yml 中实际包含角色定义,位于 Resources:

MyAuthFunctionRole
  Type: AWS::IAM::Role
  Properties: 
    # [... other properties...]
    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

您可以在此处查看有关角色的各种属性的说明:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html

解决此问题的另一种方法是在 AWS 控制台中单独创建一个具有 InvokeFunction 权限的新策略,然后在部署后将该策略附加到 SAM 创建的 MyAuthFunctionRole。现在授权方将按预期工作。

另一种策略是预先创建一个新角色,该角色具有具有 InvokeFunction 权限的策略,然后将该角色的 arn 复制并粘贴到 SAM template.yml 中:

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