メインコンテンツへスキップ
見出し画像

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


    参考:


    あなたへのおすすめ