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を利用してテンプレートから環境を作ります。
$ 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にします。
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用に書き換えていますが、中身は下記のサンプルコードそのままです。
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
です。
こんな感じ。
レコード追加すると、WebhookリクエストがAPIに渡り、APIからオーソライザーが実行されて、認証が成功するとLambdaが呼び出されます。
kintone Webhookからのリクエストの内容は、オーソライザーとLambdaのCloudWatchログで確認できます。
参考:
AWS SAM リファレンス
API Gateway APIへのアクセスの制御
Use API Gateway Lambda authorizers