この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
サーバーレス開発部@大阪の岩田です。
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オーソライザーを有効活用することで、システムをよりマイクロサービス化させることができ、メリットが出せるのではないでしょうか? 誰かの参考になれば幸いです。