API GatewayのLambda オーソライザーから後続のLambdaにデータを引き渡す
サーバーレス開発部@大阪の岩田です。
API GatewayのLambda オーソライザー (以前のカスタムオーソライザー)について調べる機会があったので、調査したことをまとめます。
Lambdaオーソライザーとは?
以前はカスタムオーソライザーと呼ばれていた機能で、Lambdaを使用してAPIメソッドへのアクセスを制御する機能です。
以前こちらの記事でも紹介されています。
現在開発しているアプリの中で、単なる認可処理だけではなく
- 処理の中で取得した情報を後続の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オーソライザーを有効活用することで、システムをよりマイクロサービス化させることができ、メリットが出せるのではないでしょうか? 誰かの参考になれば幸いです。