kintone-Zoom連携自習7日目
ちょっと思い立って自習7日目。
そういえばLambdaオーソライザーを設定してなかったので設定する。
詳細は下記にあるが、ざっくり説明するとAPI Gatewayの保護のための仕組みのこと。
SAM で設定できるのでLambdaオーソライザーを利用する。
kintoneのWebhookで使うにはURLにパラメーターをつけてリクエストすれば良い気がするので、リクエストベースの Lambda オーソライザー関数を使ってやってみる。
検証用のため sam init してテンプレートから設定する。
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: get
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
全体説明
API Gatewayの設定に、オーソライザーを設定。認証用のLambdaと結びつける。そして認証用のLambdaを設定。
今回オーソライザーの認証方法はリクエストパラメーター(URLの後ろに?キー=バリューのクエリ文字列をつけて送信)を利用。
API Gatewayの設定
Auth:→オーソライザーの利用
DefaultAuthorizer: MyLambdaRequestAuthorizer→オーソライザーの名前
Authorizers:
MyLambdaRequestAuthorizer:→これ以降にオーソライザーの設定
FunctionPayloadType: REQUEST→リクエストパラメーターで認証する
FunctionArn: !GetAtt MyAuthFunction.Arn→認証用のLambdaと結びつける
Identity:
QueryStrings:→クエリ文字列を利用
- auth →クエリ文字列のキー
認証用のLambdaの設定
MyAuthFunction:→APIのFunctionArnと同じにします
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
認証用のLambda
今回はauthorizer.tsというファイルを作成しました。
認証用のLambdaでやることは、API Gatewayにリクエストが来るとLambdaオーソライザーに処理が渡るので、リクエストパラメーターから認証用の文字列をパースして認証して、OKなら『IAM ポリシー』と『プリンシパル識別子』を含む出力オブジェクトをリターンしてアクセスを許可します。
authorizer.ts ほぼサンプルコードです。
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
/**
*
* Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
* @param {Object} event - API Gateway Lambda Proxy Input Format
*
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
* @returns {Object} object - API Gateway Lambda Proxy Output Format
*
*/
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];
}
// Perform authorization to return the Allow policy for correct parameters and
// the 'Unauthorized' error, otherwise.
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);
}
下記の部分で、クエリパラメーターに?auth=queryValue1が渡ってきたらOKにしています。雑ですが動作確認なんで。
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)}`);
}
『IAM ポリシー』と『プリンシパル識別子』について
今回のLambdaオーソライザーがやっていることは、要するにAPI Gatewayの呼び出し権限を与えることです。
そのために、Lambdaオーソライザー(今回作成したauthorizser.ts)の中で認証OKとした時に、API Gatewayの呼び出し権限を持つ『IAM ポリシー』を設定してそれをリターンすることで、API Gatewayが動作できるようになるということ(だと思います)。
というのがここに書かれています。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-control-access-policy-language-overview.html
で、プリンシパルというのは下記の通りなので
認証OKの場合はオーソライザー内で下記のようなオブジェクトを作成してリターンしています。サンプルのままなので記載していますが、context部分は認証するだけなら不要です。
{
"principalId": "me",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": "<arn>:/Prod/GET/hello/"
}
]
},
"context": {
"stringKey": "stringval",
"numberKey": 123,
"booleanKey": true
}
}
Lambdaが作成できたらデプロイして curlでアクセスします。
// パラメーターの値が違う場合は認証に失敗します。
$ curl -i -v https://<url>.amazonaws.com/Prod/hello/?auth=queryVal
・・・
HTTP/2 403
・・・
{"Message":"User is not authorized to access this resource with an explicit deny"}
// パラメーターの値が正しい場合は認証に成功して、処理用のLambdaが実行されます。
$ curl -i -v https://<url>.amazonaws.com/Prod/hello/?auth=queryVal1
・・・
HTTP/2 200
・・・
{"message":"hello world"}