API GatewayのLambda オーソライザーから後続のLambdaにデータを引き渡す

API GatewayのLambdaオーソライザー(カスタムオーソライザー)から後続処理にデータを引渡す方法について調査してみました。
2018.05.25

サーバーレス開発部@大阪の岩田です。

API GatewayのLambda オーソライザー (以前のカスタムオーソライザー)について調べる機会があったので、調査したことをまとめます。

Lambdaオーソライザーとは?

以前はカスタムオーソライザーと呼ばれていた機能で、Lambdaを使用してAPIメソッドへのアクセスを制御する機能です。

以前こちらの記事でも紹介されています。

Amazon API Gateway で Custom Authorization を使ってクライアントの認可を行う

現在開発しているアプリの中で、単なる認可処理だけではなく

  • 処理の中で取得した情報を後続のLambdaに引き渡したい
  • Lambdaオーソライザーで認証が失敗した場合のエラーメッセージをカスタマイズしたい

という要件があり、実現方法について調査しました。

手順

まずはSAMを使って簡単なAPIと裏で動くLambdaを作成します。 なお、ランタイムにはpython3.6を使用しています。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: LambdaAuthorizer Test

Resources:
  Hello:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: hello.lambda_handler
      Runtime: python3.6
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /
            Method: get

ソースコードはこれだけです。

hello.py

def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "body": 'hello'
    }

デプロイしてみます。

aws cloudformation package --template-file template.yaml --output-template-file output.yaml --s3-bucket xxxxx
aws cloudformation deploy --template-file output.yaml --stack-name lambda-authorizer-test --capabilities CAPABILITY_IAM

デプロイできたので、動作を確認してみます。

curl https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Stage/
hello

まずは動作確認OKです。

Lambdaオーソライザーの設定

先程作成したAPIにLambdaオーソライザーを設定していきます。 まずは認可処理に使うLambdaを実装します。 Authorizationヘッダに1が入ってたら認証OKとするだけのプログラムです。

authorizer.py

def lambda_handler(event, context):

    token = event["headers"]["Authorization"]
    if token == "1":    
        return {
            "principalId" : 1,
            "policyDocument" : {
                "Version" : "2012-10-17",
                "Statement" : [
                    {
                        "Action": "*",
                        "Effect": "Allow",
                        "Resource": "arn:aws:execute-api:*:*:*/*/*/"   
                    }
                ]
            }
        }

    return {
        "principalId" : 1,
        "policyDocument" : {
            "Version" : "2012-10-17",
            "Statement" : [
                {
                    "Action": "*",
                    "Effect": "Deny",
                    "Resource": "arn:aws:execute-api:*:*:*/*/*/"   
                }
            ]
        }
    }

SAMテンプレートは下記の様に修正しました。 template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: LambdaAuthorizer Test

Resources:
  Hello:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: hello.lambda_handler
      Runtime: python3.6
  Authorizer:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: authorizer.lambda_handler
      Runtime: python3.6
  LambdaPermissionHello:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref Hello
      Principal: apigateway.amazonaws.com
  LambdaPermissionAuthorizer:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref Authorizer
      Principal: apigateway.amazonaws.com
  HelloAPI:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Dev
      DefinitionBody:
        swagger: "2.0"
        info:
          version: "1.0.0"
          title: "lambda authorizer test"
        basePath: "/"
        schemes:
        - "http"
        paths:      
          /:
            get:
              summary: "lambda authorizer test"
              description: "lambda authorizer test"
              produces:
              - "application/json"
              responses:
                "200":
                  description: "successful operation"
              x-amazon-apigateway-integration:
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Hello.Arn}/invocations
                passthroughBehavior: "when_no_match"
                httpMethod: "POST"
                contentHandling: "CONVERT_TO_TEXT"
                type: "aws_proxy"
                responses:
                  default:
                    statusCode: "200"
              security:
                - test-authorizer: []
        securityDefinitions:
          test-authorizer:
            type: apiKey
            name: Authorization
            in: header
            x-amazon-apigateway-authtype: custom
            x-amazon-apigateway-authorizer:
              type: request
              identitySource: method.request.header.Authorization
              authorizerUri:
                Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Authorizer.Arn}/invocations

再度デプロイして動作を確認します。 ※最初に作成したAPI Gatewayは削除されます。

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/
{"message":"Unauthorized"}

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/ -H Authorization:1
hello

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/ -H Authorization:2
{"Message":"User is not authorized to access this resource with an explicit deny"}

Authorizationヘッダに1を設定した場合だけ正常応答が返ってきています。

後続処理へのデータの引き渡し

ここからが本題です。 Lambdaオーソライザーの中で取得した情報を後続処理に引き渡します。 authorizer.pyを下記の様に修正します。

import base64
import json
def lambda_handler(event, context):

    token = event['headers']['Authorization']
    if token == '1':  
        context = {
          "parent1": "val1",
          "parent2":{
            "child1": "val1",
            "child2": "val2",  
          }
        } 
        json_context = json.dumps(context)
        base64_context = base64.b64encode(json_context.encode('utf8'))
        return {
            'principalId' : 1,
            'policyDocument' : {
                'Version' : '2012-10-17',
                'Statement' : [
                    {
                        "Action": "*",
                        "Effect": "Allow",
                        "Resource": "arn:aws:execute-api:*:*:*/*/*/"   
                    }
                ]
            },
            'context': {
                'additional_info': base64_context.decode('utf-8')
            }
        }

    return {
        'principalId' : 1,
        'policyDocument' : {
            'Version' : '2012-10-17',
            'Statement' : [
                {
                    "Action": "*",
                    "Effect": "Deny",
                    "Resource": "arn:aws:execute-api:*:*:*/*/*/"   
                }
            ]
        }
    }

処理の中で取得した情報をレスポンスとして返却する辞書の['context']['additional_info']に設定しています。 注意点として、AWSのドキュメント に記載されている様にcontextにはJSONや配列を設定できないため、JSONをbase64でエンコードした文字列を返す様にしています。

動作検証のため、hello.pyを下記の様に修正します。

import base64
import json
def lambda_handler(event, context):

    additional_info = event['requestContext']['authorizer']['additional_info']
    additional_info = base64.b64decode(additional_info)
    additional_info = json.loads(additional_info)
    return {
        "statusCode": 200,
        "body": json.dumps(additional_info)
    }

Lambdaオーソライザーで設定したcontextの中身が、event['requestContext']['authorizer']の中に入っているのでデコードしてそのままレスポンスに返します。

デプロイ後に動作検証してみます。

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/ -H Authorization:1
{"parent1": "val1", "parent2": {"child1": "val1", "child2": "val2"}}

OKです! Lambdaオーソライザー内で取得した情報が後続のLambdaから参照できています! 実際の業務では、この情報を元に、権限レベルの低いユーザーに対しては後続のLambdaのレスポンスから特定の項目を削るといった利用方法が考えられます。

エラーメッセージのカスタマイズ

次に、認可処理でエラーになった場合のエラーメッセージをカスタマイズしてみます。 authorizer.pyを修正します。

#...略
    return {
        "principalId" : 1,
        "policyDocument" : {
            "Version" : "2012-10-17",
            "Statement" : [
                {
                    "Action": "*",
                    "Effect": "Deny",
                    "Resource": "arn:aws:execute-api:*:*:*/*/*/"   
                }
            ]
        },
        "context" : {
            "message": "custom error message from lambda"
        }
    }

Authorizationヘッダが"1"でなかった場合に、返却するポリシーの["context"]["message"]にエラーメッセージを設定しています。

最後に、API GatewayがLambdaオーソライザーで設定されたエラーメッセージを返却する様に、API Gatewayレスポンスの本文マッピングテンプレートを設定します。 SAMテンプレートに以下の記述を追加します。

  #...略
  GatewayResponse:
    Type: "AWS::ApiGateway::GatewayResponse"
    Properties:
      ResponseTemplates: 
        application/json: >
          {"message":"$context.authorizer.message"}
      ResponseType: DEFAULT_4XX
      RestApiId: !Ref HelloAPI

デプロイ後に動作検証してみます。

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/ -H Authorization:2
{"Message":"User is not authorized to access this resource with an explicit deny"}

ここで何故か期待したレスポンスが返却されませんでした。 マネジメントコンソールから確認したところ、本文マッピングテンプレートは正しく更新できていたので、手動でAPIの再デプロイを試したところ、期待通りのレスポンスが返却される様になりました。

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/ -H Authorization:2
{"message":"custom error message from lambda"}

まとめ

最後に手動の再デプロイが必要な理由がどうしても分からなかったのですが、無事にLambdaオーソライザーと後続のLambdaを連携させることができました! 個々のLambdaの中で認証・認可処理を実装することもできますが、Lambdaオーソライザーを有効活用することで、システムをよりマイクロサービス化させることができ、メリットが出せるのではないでしょうか? 誰かの参考になれば幸いです。