AWS Lambda Authorizer のメッセージをカスタマイズするTips。

こんにちは。ハグテク(いとう)です。普段はオランダに巣食いつつフリーランスでデベロッパーをしています。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テンプレートと使いかたは似ています。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-gatewayresponse.html

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で弾けるパターンが増えます。つまり、本体部分に余計なバリデーションをさせなくてすむようになり、かつ、本体部分の呼び出しも減らすことができます。簡単にできて、メリットが大きそうな変更ですので、検討してみてはどうでしょうか?