kintone Webhook用のAPI をLambdaオーソライザーで保護する方法について

API GatewayとLambdaを利用する時のAPIの保護の方法についての記事です。
kintone Webhookの利用に限ると、リクエストベースのLambdaオーソライザー（リクエストオーソライザー）が良かったので紹介します。

# リクエストオーソライザーを選択する理由

2022-10現在、kintone Webhookではヘッダーを付けることができない。
使えるとすればリクエストパラメーターだけ。
なのでリクエストオーソライザーが合っている。（と個人的に思います）

# 環境

* macOS
* AWS SAM CLI, version 1.58.0
* Node v16.13.1
* kintone

# API Gateway と Lambdaを作成する

SAMを利用してテンプレートから環境を作ります。

```bash
$ sam init
Which template source would you like to use?
	1 - AWS Quick Start Templates
What package type would you like to use?
	1 - Zip (artifact is a zip uploaded to S3)
Which runtime would you like to use?
	1 - nodejs14.x
AWS quick start application templates:
	2 - Hello World Example TypeScript
```

プロジェクトフォルダ配下にtemplate.yamlができるので編集します。

後で kintone Webhook からリクエストするので、メソッドはpostにします。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  lambda-auth-test

  Sample SAM Template for lambda-auth-test
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Auth:
        DefaultAuthorizer: MyLambdaRequestAuthorizer
        Authorizers:
          MyLambdaRequestAuthorizer:
            FunctionPayloadType: REQUEST
            FunctionArn: !GetAtt MyAuthFunction.Arn
            Identity:
              QueryStrings:
                - auth  

  MyAuthFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: authorizer.handler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        # Sourcemap: true # Enabling source maps will create the required NODE_OPTIONS environment variables on your lambda function during sam build
        EntryPoints: 
        - authorizer.ts

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref HelloWorldApi
            Path: /hello
            Method: post
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        # Sourcemap: true # Enabling source maps will create the required NODE_OPTIONS environment variables on your lambda function during sam build
        EntryPoints: 
        - app.ts

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${HelloWorldApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
  MyAuthFunction:
    Description: "Hello World Lambda Auth Function ARN"
    Value: !GetAtt MyAuthFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World Auth function"
    Value: !GetAtt MyAuthFunctionRole.Arn
```

# Lambdaオーソライザーの作成
名前はなんでも良いですが、オーソライザーを作成します。
今回はauthorizer.tsとしました。
若干TypeScript用に書き換えていますが、中身は下記のサンプルコードそのままです。

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


```authorizer.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  console.log('Received event:', JSON.stringify(event, null, 2));
  const headers = event.headers;
  const queryStringParameters = event.queryStringParameters;
  const pathParameters = event.pathParameters;
  const stageVariables = event.stageVariables;
      
  // Parse the input for the parameter values
  const tmp = event.methodArn.split(':');
  const apiGatewayArnTmp = tmp[5].split('/');
  const awsAccountId = tmp[4];
  const region = tmp[3];
  const restApiId = apiGatewayArnTmp[0];
  const stage = apiGatewayArnTmp[1];
  const method = apiGatewayArnTmp[2];
  let resource = '/'; // root resource
  if (apiGatewayArnTmp[3]) {
      resource += apiGatewayArnTmp[3];
  }
      
  const condition = { IpAddress: {}};
  let response: any;

  if (queryStringParameters.auth === "queryValue1") {
      response = generateAllow('me', event.methodArn);
      console.log(`Authorization SUCCESS: ${JSON.stringify(response, null, 2)}`);
  }  else {
      response = generateDeny('me', event.methodArn);
      console.log(`Authorization Error: ${JSON.stringify(response, null, 2)}`);
  }
    return response;
};

// Help function to generate an IAM policy
const generatePolicy = function(principalId: string, effect: string, resource: string) {
  // Required output:
  const authResponse: any = {};
  authResponse.principalId = principalId;
  if (effect && resource) {
      const policyDocument: any = {};
      policyDocument.Version = '2012-10-17'; // default version
      policyDocument.Statement = [];
      const statementOne: any = {};
      statementOne.Action = 'execute-api:Invoke'; // default action
      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;
}
   
const generateAllow = function(principalId: string, resource: string) {
  console.log(principalId);
  console.log(resource);
  return generatePolicy(principalId, 'Allow', resource);
}
   
const generateDeny = function(principalId: string, resource: string) {
  console.log(principalId);
  console.log(resource);
  return generatePolicy(principalId, 'Deny', resource);
}
```

# ビルド＆デプロイ

`sam build` して `sam deploy --guided` します。

# kintone Webhook リクエスト

ビルドが終わると実行したコンソールにAPIのエンドポイントが表示されるので控えておきます。

適当なkintoneアプリを作ります。
Webhookを設定します。

Webhook URL に APIエンドポイントURLを設定して、認証用のパラメーターをキー=バリュー形式で設定します。

今回は `?auth=queryValue1` です。
こんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/c10d050c-7f4e-81c0-8524-b61822cb202b.png)

レコード追加すると、WebhookリクエストがAPIに渡り、APIからオーソライザーが実行されて、認証が成功するとLambdaが呼び出されます。

kintone Webhookからのリクエストの内容は、オーソライザーとLambdaのCloudWatchログで確認できます。

# 参考：

AWS SAM リファレンス

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-reference.html

API Gateway APIへのアクセスの制御

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-controlling-access-to-apis.html

Use API Gateway Lambda authorizers

https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html