🛡️

Amplify の REST API に Lambda オーソライザーを設定する

2023/02/23に公開

はじめに

AWS Amplify で管理している REST API に Lambda オーソライザーを設定しようとしたところ、例が見当たらなかったため検証した手順を書いておきます。

記事の対象者

既に Amplify を利用しており、Amplify で管理している REST API に Lambda オーソライザーを設定したい方を対象としています。Amplify をこれから始める方はまず公式ドキュメントを読む事をおすすめします。
https://aws.amazon.com/jp/amplify/

また、REST API ではなく GraphQL で Lambda 認証を設定したい方は Amplify 公式ドキュメントをご覧ください。
https://docs.amplify.aws/cli/graphql/authorization-rules/#custom-authorization-rule

前提

Amplify CLI と Node.js のバージョンは次の通りです。

$ amplify --version
10.7.3
$ node --version
v18.13.0

プロジェクト初期化

次のコマンドで Amplify プロジェクトを初期化します。

amplify init

初期化後のフォルダ構成は以下の通りです。

tree -d
|-- amplify
|   |-- #current-cloud-backend
|   |   `-- awscloudformation
|   |       `-- build
|   |-- backend
|   |   |-- awscloudformation
|   |   |   `-- build
|   |   `-- types
|   `-- hooks
`-- src

設定手順

CLI でリソースを設定していきます。

API Gateway の作成

まずは amplify add api で認証機能のない API Gateway を作成します。Lambda 関数の作成を希望するか確認されるので、そのまま API Gateway のバックエンドとなる Lambda 関数も作成してしまいます。

$ amplify add api
? Select from one of the below mentioned services: REST
✔ Provide a friendly name for your resource to be used as a label for this category in the project: · RestApi
✔ Provide a path (e.g., /book/{isbn}): · /items
Only one option for [Choose a Lambda source]. Selecting [Create a new Lambda function].
? Provide an AWS Lambda function name: ApiBackend
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? Yes
Edit the file in your editor: /workspaces/amplify-example/amplify/backend/function/ApiBackend/src/index.js
? Press enter to continue 
Successfully added resource ApiBackend locally.
✅ Succesfully added the Lambda function locally
✔ Restrict API access? (Y/n) · no
✔ Do you want to add another path? (y/N) · no
✅ Successfully added resource RestApi locally

API Gateway のバックエンドとなる Lambda 関数の実装はデフォルトのままでも検証には影響がないので編集せずに進めます。デフォルトで作られるコードなので記事には載せていません。

Lambda オーソライザー関数の作成

次に Lambda オーソライザー用の Lambda 関数を作成します。ここで Lambda オーソライザーとして設定できるわけではなく、あくまで Amplify CLI で Lambda 関数を作成するのみとなります。

$ amplify add function
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: CustomAuthorizer
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? Yes
Edit the file in your editor: /workspaces/amplify-example/amplify/backend/function/CustomAuthorizer/src/index.js
? Press enter to continue 
Successfully added resource CustomAuthorizer locally.

出来上がった Lambda 関数のコードを次のように変更します。

amplify/backend/function/CustomAuthorizer/src/index.js
/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */

// A simple token-based authorizer example to demonstrate how to use an authorization token
// to allow or deny a request. In this example, the caller named 'user' is allowed to invoke
// a request if the client-supplied token value is 'allow'. The caller is not allowed to invoke
// the request if the token value is 'deny'. If the token value is 'unauthorized' or an empty
// string, the authorizer function returns an HTTP 401 status code. For any other token value,
// the authorizer returns an HTTP 500 status code.
// Note that token values are case-sensitive.

exports.handler = async (event) => {
  console.log(event);
  switch (event.authorizationToken) {
    case "allow":
      return generatePolicy("user", "Allow", event.methodArn);
    case "deny":
      return generatePolicy("user", "Deny", event.methodArn);
    case "unauthorized":
      return "Unauthorized"; // Return a 401 Unauthorized response
    default:
      return "Error: Invalid token"; // Return a 500 Invalid token response
  }
};

// Help function to generate an IAM policy
const generatePolicy = (principalId, effect, resource) => ({
  principalId,
  policyDocument:
    effect && resource
      ? {
          Version: "2012-10-17",
          Statement: [
            {
              Action: "execute-api:Invoke",
              Effect: effect,
              Resource: resource,
            },
          ],
        }
      : undefined,
  context: {
    stringKey: "stringval",
    numberKey: 123,
    booleanKey: true,
  },
});

上記 Lambda 関数のコードは、AWS 公式ドキュメントに載っている『トークンベースの Lambda オーソライザー関数』のサンプルコードを参考にしており、Authorization ヘッダーに設定された token 文字列の内容が allow だったら承認するような仕組みになっています。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-token-lambda-function-create

API Gateway をオーバーライドする

ここが味噌です。Amplify CLI で提供されているコマンドだけでは API Gateway に Lambda オーソライザーを設定することができないため、Amplify の機能として提供されているオーバーライドの仕組みを利用する必要があります。API Gateway の設定をオーバーライドするには次のコマンドを実行します。
https://docs.amplify.aws/cli/restapi/override/

$ amplify override api
✅ Successfully generated "override.ts" folder at /workspaces/amplify-example/amplify/backend/api/RestApi

少しするとオーバーライド用の TypeScript ファイル (override.ts) が生成されます。生成された TypeScript ファイルを次のように編集します。

amplify/backend/api/RestApi/override.ts
// This file is used to override the REST API resources configuration
import { AmplifyApiRestResourceStackTemplate } from "@aws-amplify/cli-extensibility-helper";

// ⚠️ Lambda オーソライザー関数を記事と異なる名前で作っている場合はここを編集してください 
// amplify/backend/function/<your-custom-authorizer-function-name>
const customAuthorizerFunctionName = "CustomAuthorizer";

// see: https://docs.amplify.aws/cli/restapi/override/
export function override(resources: AmplifyApiRestResourceStackTemplate) {
  const customAuthorizerFunctionArn = {
    "Fn::Join": [
      "",
      [
        "arn:aws:lambda:",
        {
          Ref: "AWS::Region",
        },
        ":",
        {
          Ref: "AWS::AccountId",
        },
        `:function:${customAuthorizerFunctionName}-`,
        {
          Ref: "env",
        },
      ],
    ],
  };
  resources.restApi.addPropertyOverride("Body.securityDefinitions", {
    MyLambdaAuthorizer: {
      type: "apiKey",
      name: "Authorization",
      in: "header",
      "x-amazon-apigateway-authtype": "custom",
      "x-amazon-apigateway-authorizer": {
        type: "token",
        authorizerUri: {
          "Fn::Join": [
            "",
            [
              "arn:aws:apigateway:",
              {
                Ref: "AWS::Region",
              },
              ":lambda:path/2015-03-31/functions/",
              customAuthorizerFunctionArn,
              "/invocations",
            ],
          ],
        },
        authorizerResultTtlInSeconds: 300,
      },
    },
  });
  resources.addCfnResource(
    {
      type: "AWS::Lambda::Permission",
      properties: {
        FunctionName: customAuthorizerFunctionArn,
        Action: "lambda:InvokeFunction",
        Principal: "apigateway.amazonaws.com",
        SourceAccount: {
          Ref: "AWS::AccountId",
        },
        SourceArn: {
          "Fn::Join": [
            "",
            [
              "arn:aws:execute-api:",
              {
                Ref: "AWS::Region",
              },
              ":",
              {
                Ref: "AWS::AccountId",
              },
              `:${resources.restApi.ref}/authorizers/*`,
            ],
          ],
        },
      },
    },
    "ApiGatewayPermission"
  );
  for (const path in resources.restApi.body.paths) {
    // Add the Authorization header as a parameter to requests
    resources.restApi.addPropertyOverride(
      `Body.paths.${path}.x-amazon-apigateway-any-method.parameters`,
      [
        ...resources.restApi.body.paths[path]["x-amazon-apigateway-any-method"]
          .parameters,
        {
          name: "Authorization",
          in: "header",
          required: false,
          type: "string",
        },
      ]
    );
    // Use your new custom authorizer for security
    resources.restApi.addPropertyOverride(
      `Body.paths.${path}.x-amazon-apigateway-any-method.security`,
      [{ MyLambdaAuthorizer: [] }]
    );
  }
}

override.ts の中では次の設定を行っています。

  • Amplify CLI によって生成される OpenAPI 仕様の Body.securityDefinitions を書き換えて Lambda オーソライザーを定義
  • API Gateway が Lambda オーソライザー関数を呼び出せるように AWS::Lambda::Permission リソースを作成
  • 全ての API パスで Lambda オーソライザーを使うように書き換え

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-permission.html

デプロイ

設定は以上となります。次のコマンドでデプロイします。

amplify push

動作確認

実際に動作を確認してみます。
まずは Authorization ヘッダーに allow を設定して、正常なレスポンスを期待するリクエストを投げます。

$ curl -H "Authorization: allow" https://xxxxxxxxxx.execute-api.ap-northeast-3.amazonaws.com/dev/items
"Hello from Lambda!"

Amplify CLI によって自動生成された Lambda 関数のレスポンスが確認できます。

次に Authorization ヘッダーに deny を設定して、明示的に拒否することを期待するリクエストを投げます。

$ curl -H "Authorization: deny" https://xxxxxxxxxx.execute-api.ap-northeast-3.amazonaws.com/dev/items
{"message":"User is not authorized to access this resource with an explicit deny"}

期待する拒否メッセージが表示されました。

最後に Authorization ヘッダーを設定せずにリクエストを投げます。

$ curl https://xxxxxxxxxx.execute-api.ap-northeast-3.amazonaws.com/dev/items
{"message":"Unauthorized"}

こちらも Unauthorized のメッセージが返ってきていて期待する内容になっていることが確認できます。

最後に

Amplify で REST API に Lambda オーソライザーを設定する手順を記載しました。Amplify CLI リソース作成時に設定できない内容でもオーバーライドを利用することで柔軟に対応することができます。他にもカスタムリソースといった仕組みを使って Amplify CLI で作成できない任意のリソースを構築することができます。オーバーライドでも addCfnResource を使うことでリソースの作成は可能ですが、カスタムリソースを利用するとAWS CDKCloudFormationでリソースを定義できるため、オーバーライドよりも簡単に任意のリソースを作成が可能です。
なお、これらは便利な機能ですが、カスタムリソースやオーバーライドを多用していくと Amplify でお手軽にリソースを管理するメリットが薄れていく可能性があります。もしメリットが薄れていると感じた場合はバックエンドだけ AWS CDK で管理するなど、別の手段を検討しても良いかもしれません。
とはいえ、スピーディな実装ができる Amplify で痒い所に手が届くというのは大きなポイントでした。もし、同様のケースでお困りの方の助けになれば幸いです。

参考

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-token-lambda-function-create
https://docs.amplify.aws/cli/restapi/override/

Discussion