こんにちは。ハグテク(いとう)です。普段はオランダに巣食いつつフリーランスでデベロッパーをしています。AWSやAlexaの界隈によく顔を出しています。どうも。
この記事は、AWS Lambda と Serverless Advent Calendar 8日目の記事です。テーマは。「API Gateway Lambda Custom Authorizer のメッセージをカスタマイズしてみよう。」です。それでは行ってみましょう!
API Gateway Lambda authorizers とは?
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
APIGateway と APIの実処理 の間に挟み込む認証専用の特別な Lambda ファンクションです。API呼び出し時に渡されるAuthorizationヘッダーのトークン検証や、セキュリティ系ヘッダの検証、JWTトークンのベリフィケーションなどがおもなお仕事です。
Custom Authorizer の最大のメリットとは?
ずばり、これにつきます。
「検証済みを保証した状態で後続の処理に引き渡すことができる」
ここでいう後続の処理において、リクエストに対するバリデーションが不要になるというわけではありませんが、その前段に検証のレイヤーがあることによって、APIの本体側ではビジネスロジックに集中することができます。
APIの処理量の観点からもメリットがあります。CloudFrontが全段でキャッシュした内容を返してくれるように、不要なリクエストはAuthorizerで弾いてくれたら、無駄に後続処理が呼ばれなくて、効率のよい構成ですね。
Authorizerで検証がたくさんできるようになると発生する課題
Authorizer で複数の検証ができるとなると、アーキテクチャとしてはいろいろ効率がよさそうだ、という話をさきほどしましたが、ひとつ課題が出ます。
「API Gateway Lambda authorizers で 403を返すとデフォルトでは同じメッセージしか出せない」
という課題です。
API Gateway Lambda authorizers の制約上、Lambdaファンクションのレスポンスは、IAM の PolicyDocument オブジェクト です。
レスポンスに含める PolicyDocument の Actionに “Deny” を格納して返却すると、特定のメッセージとともに、403 をAPIのレスポンスとして呼び出し元に返してくれる、というしくみです。
呼び出し元には以下のようなメッセージが出ます。
{“message”: “API rejected the call by explicit Deny”}
APIとしてはこれでも十分なのでしょうが、Authorizerがどんな検証でエラーとなったかがメッセージで出せると、よりフレンドリーなAPIにできそうです。
GatewayResponse
APIGateway にデプロイした各APIには、GatewayResponse という項目があります。これはAPIGatewayが特定のレスポンスを検知したら、任意の形にカスタマイズして最終的に返却する、というものです。BodyMappingテンプレートと使いかたは似ています。
Custom Authorizerのレスポンス
メッセージをカスタマイズしたいときのレスポンスの例です。
レスポンスの context 部分は、次の処理に引き渡すデータを格納します。エラーメッセージ用の属性 “errorMessage” を追加して、独自のエラーメッセージを次の処理へ渡します。
const authResponse = {
principalId: 'user',
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: ’Allow' or 'Deny',
Resource: event.methodArn
}
]
},
// 次の処理に渡される
context: {
// 独自のエラーメッセージをコンテキストに含める
errorMessage: 'custom error message
}
}
ResponseTemplateでエラーメッセージをマップする
CustomAuthorizerが返却したレスポンスのcontext部分は、$context.authorizer にマップされます。前節で、context.errorMessage にカスタムしたエラーメッセージを格納していますので、GatewayResponseのResponseTemplatesで、この属性を 最終的なAPIのレスポンスにマップします。
ResponseTemplates:
'application/json': '{"message": "$context.authorizer.errorMessage"}'
Serverless Framework での例を示します。 *1
resources:
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:custom.stage}-api-gateway-rest-api-id
resources:
GatewayResponse:
Type: AWS::ApiGateway:GatewayResponse
Properties:
ResponseType: ACCESS_DENIED
ResponseTemplates:
'application/json': '{"message": "$context.authorizer.errorMessage"}'
RestApiId:
'Fn::ImportValue': ${self:custom.stage}-api-gateway-api-id
*1 GatewayResponseを登録するには対象となるRestApiIdが必要です。一つのserverless.ymlで実現するために、一旦resouces.Outputs を使って、RestApiIdをCloudFormationにエクスポートしてから、そのIDを参照することでRestApiIdを取得しています。
まとめ
これまで、単一のメッセージしか返せないとなると、本体側でカスタマイズして、403を返す、というパターンはしばしばあったと思います。Authorizerでエラーメッセージをある程度柔軟に返すことができれば、Authorizerで弾けるパターンが増えます。つまり、本体部分に余計なバリデーションをさせなくてすむようになり、かつ、本体部分の呼び出しも減らすことができます。簡単にできて、メリットが大きそうな変更ですので、検討してみてはどうでしょうか?