見出し画像

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

で、プリンシパルというのは下記の通りなので

ステートメントのアクションやリソースへのアクセスが許可されているアカウントまたはユーザーを指します

Amazon API Gateway のアクセスポリシー言語の概要より

認証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"}


参考:


いいなと思ったら応援しよう!