AWS SAMテンプレートのサンプルをベースにLambda Authorizerを組み込む

LambdaやDynamoDBを使ったサーバレスアーキテクチャを構築するうえでとても便利なIaaC（Infrastructure as a Code）ツールであるSAM（Serverless Application Model）を使って、Lambda Authorizerを使った認証付きAPIを作成したいと思います。
今回はトークンベースの認証とします。
SAMには、いくつかのテンプレートが用意されています。その中でも最もシンプルなhello-worldテンプレートからスタートしてみます。

```
sam init
```

上記のコマンドでプロジェクトの作成を開始します。
今回は、AWS Quick Start Templatesを選び、Zipでデプロイする方式で、ランタイムはnodejs14.x、使用するテンプレートはHello World Exampleとしました。
ちなみに、ここでとってくるSAMのtemplateは、以下のリポジトリからクローンされます。
https://github.com/aws/aws-sam-cli-app-templates

結果として、次のように表示されました。

```
    -----------------------
    Generating application:
    -----------------------
    Name: authorizerDemo
    Runtime: nodejs14.x
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./authorizerDemo/README.md
```

これでアプリケーションを開発していくためのベースのテンプレートからスタートできます。
ちょっとディレクトリ構成をいじります。

```
src <- 新規追加
 hello-world <- もともとあったソースをsrcディレクトリ以下に移動
 auth <- 新規追加
```

authディレクトリに移動して、```npm init```します。
auth.jsを作成します。これがユーザの資格情報をチェックするコードになりますが、ここでは、簡単な例として、tokenに、```mySecret```という文字列が入っているとき、Allowとし、それ以外はDenyとするコードにします。

```javascript
exports.lambdaHandler = async(event, context) => {
    const token = event.authorizationToken;
    // このAPIについて拒否したり許可したりしたいので、このAPIを指定するためのリソースARNを取得
    const resource = event.methodArn;

    // tokenがmySecretであるならば、許可、そうでないなら拒否
    if (token == 'mySecret') {
        console.log('allow');
        return {
            principalId: token,
            policyDocument: {
                Version: "2012-10-17",
                Statement: [{
                    Action: "execute-api:Invoke",
                    Effect: "Allow",
                    Resource: resource,
                }, ],
            },
        };
    }
    else {
        console.log('deny')
        return {
            principalId: token,
            policyDocument: {
                Version: "2012-10-17",
                Statement: [{
                    Action: "execute-api:Invoke",
                    Effect: "Deny",
                    Resource: resource,
                }, ],
            },
        };

    }

};
```

template.yamlを、次のように変更します。
myApiは、API Gatewayを定義します。ここで、Authorizerとして、authFunctionを指定しています。
authFunctionとして、前述のauth.jsが呼ばれます。
また、必須ではないのですが、StageNameをパラメタとして定義して、ほかで使いまわせるようにします。

```yaml
Parameters:
  StageName:
    Type: "String"
    Default: "dev"

Resources:

  myApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref StageName # 上で決めたStageNameを参照します
      Auth:
        DefaultAuthorizer: authFunction
        Authorizers:
          authFunction:
            FunctionArn: !GetAtt authFunction.Arn
            
  authFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/auth/
      Handler: auth.lambdaHandler
      Runtime: nodejs14.x
```

既存のHello-world関数に認証をつけたいと思いますので、以下のようにHello-world関数の定義で、LambdaAuthorizerを呼び出すように変更します。

```yaml
HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            RestApiId: !Ref myApi # <- ここで、Authorizerを呼ぶように設定しています
            Path: /hello
            Method: get
```

Outputも設定しておきます。
HelloWorldApiのエンドポイントで、上記のmyApiのStageNameを参照するように書き換えています。

```yaml
Outputs:
  ApiWithLambdaAuth:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${StageName}/hello/"
```

ここまでできたら、あとは、デプロイするだけです。
template.yamlが見えるパスで、以下のコマンドで、ビルドしてデプロイします。

```
sam build
sam deploy --guided
```

guidedの対話シェルでは、以下のように入力をしました。ここでsamconfig.tomlファイルを作成しているのですが、このファイルとtemplate.yamlファイルが見えているパスで、--guidedをつけずに、`sam deploy`すると、自動的にtomlファイルを読み込んでデプロイしてくれるようになります。便利ですので、せっかくなので作成するようにしましょう。

```
 　　　　Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: lambda-auth-demo
        AWS Region [ap-northeast-1]:   
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]:
```

ところが、デプロイは途中でこけてしまいます。以下のエラーが出力されるのですが、何が起きているのでしょうか。

```
Error: Failed to create changeset for the stack: lambda-auth-demo, 
ex: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state:
 For expression "Status" we matched expected path: "FAILED" Status: FAILED. 
Reason: Unresolved resource dependencies [ServerlessRestApi] in the Outputs block of the template
```

Unresolved resource dependencies [ServerlessRestApi] in the Outputs block of the templateは、ServerlessRestApiというリソースが解決されません、ということなのですが、この、ServerlessRestApiというのは、API Gatewayをテンプレートのなかで明示的に指定しなかったときに、SAMによって暗黙的に生成されるAPI Gatewayのリソース名です。
もともとのテンプレートでは、API Gatewayのリソースを明示的に定義していないので、これが使えるのですが、いま、Authorizerを設定するために、API Gatewayのリソースを明示的に定義したので、使えなくなっています。
そこで、自分で定義したmyApiに変えてあげる必要があります。初めてデプロイしたときにはまったので、共有しておきます。
ただ、Outputsは、SAMが作成したエンドポイントを単に出力するだけなので、最悪なくても、動作に問題はないと思います。その場合は、自分でコンソール上でAPIゲートウェイを見に行くか、`aws cloudformation --describe-stacks`などを用いてエンドポイントを確認します。

```yaml
Outputs:
  ApiWithLambdaAuth:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${myApi}.execute-api.${AWS::Region}.amazonaws.com/${StageName}/hello/"
```

さて、これでデプロイできるはずです。やってみましょう。

```
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                                                      
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 ApiWithLambdaAuth                                                                                                                                                                        
Description         API Gateway endpoint URL                                                                                                                                                                 
Value               https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/                                                                                                                   
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - lambda-auth-demo in ap-northeast-1
```

成功したみたいです！
動作確認をしてみましょう。

```
1.headerなし
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/ 
{"message":"Unauthorized"}
2.不正なトークン
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/ -H "Authorization: mySecrets"
{"Message":"User is not authorized to access this resource with an explicit deny"}
3.正当なトークン
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello/ -H "Authorization: mySecret"
{"message":"hello world"}
```
1では401(Unauthorized)、2では403(Forbidden)が返り、3で200(OK)が返ります。
これで期待通りに動作していることが確認できました。