Cognitoを使ってAPI Gatewayのアクセス認証をしてみた

## はじめに
これまでS3のバケットポリシーとAPI Gatewayのリソースポリシーでアクセス元のIP制限をかけていたため、許可されているIPアドレスからでないと、アクセスし、APIをたたけないようになっていました。しかし、今回訳あってIP制限を外すことになってしまい、APIを誰でも叩き放題になってしまったので、解決策を考えました。

## 方法検討
### API Gatewayで使えるアクセスを認証
APIのアクセスを認証する方法を調べたところ以下の3つが出てきました。

+ Cognito
+ Lambdaオーソライザー
+ IAM認証

#### Cognito
Cognitoユーザープールで認証時にユーザープールトークンが発行され、そのトークンを使用して認証する方法です。

##### わたしが思うユースケース

+ ユーザー認証にCognitoを使用しているとき

#### Lambdaオーソライザー
API Gatewayを叩いた時に、認証用のLambda関数を呼び、認証が通れば、実行したいAPI(今回だとLambda関数)が実行されるようになるという方法です。

##### わたしが思うユースケース

+ Auth0などのCognito外の認証プラットフォームを使っているとき

**参考**

+ https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
+ https://dev.classmethod.jp/articles/lambda-authorizer/

#### IAM認証
APIの実行権限を付与したIAMユーザーを作成し、IAMユーザーのアクセスキー、シークレットキーを使ってAWS Signature V4 署名を作成し認証する方法です。
ユーザーにIAMロールが付与されていれば、それも使用することができます。

##### わたしが思うユースケース

+ サーバーからAPIをたたくとき（EC2などIAMロールが使えるときはIAMロールを利用）
+ CognitoのグループでIAMロールを付与しているとき

**参考**

+ https://dev.classmethod.jp/articles/api-gateway-iam-authentication-sigv4/

### 現在の構成図

アプリケーション部分の構成図は下記の通りです。

![Cognito.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/5acdeb44-793c-8d1b-a8b9-8c2056b16b79.png)



ユーザー認証部分はCognito + Amplifyフレームワークで構築しています。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の[構築準備編](https://qiita.com/maika_kamada/items/1289b8926208d35751b1)と[構築完成編](https://qiita.com/maika_kamada/items/bd55a9fa3dd551d63b94)をご覧ください。
そして、アプリケーション部分はLambda + RDS Proxy + RDSで実装しています。この構築方法については「[祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた](https://qiita.com/maika_kamada/items/6eb6a40c17b4b8995acb)」をご覧ください。

### 結論
現状、Cognitoユーザープールを使ってユーザー認証をしているので、API Gatewayのアクセス認証にもCognitoを使うことにしました！


## 手順
既存の構成にAPIのアクセス認証をつけていくので、Cognitoユーザープールを使ってのユーザー認証、API Gatewayを使ってLambdaを実行する部分については既に構築できていることを前提として、下記の流れで進めていきます。ただ、今回はDB操作は行わず、メッセージを送り、メッセージをそのまま返すLambda関数を実行するようにしています。

1. API Gatewayの設定
2. フロントの実装

## やってみる
### 1. API Gatewayの設定
まず、API Gatewayのオーソライザーを作成していきます。
API Gatewayのコンソールから、[オーソライザー]を開きます。

![スクリーンショット 2021-02-03 21.05.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/21dbdffb-f6fb-3084-d269-b3c2efd4ae3b.png)

新規でオーソライザーを作成します。
名前、タイプ、Cognitoユーザープール、トークンのソースを入力し、作成ボタンをクリックします。トークンのソース`Authorization`はリクエストのヘッダーとしてトークンを送るときに使います。

![スクリーンショット 2021-02-09 23.10.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/30247fef-a78f-1a9c-c33a-5f0a60ec8163.png)


次に、作成したオーソライザーはメソッド単位で設定していきます。つまり、複数メソッドがある場合はそれぞれに設定しないとトークンなしでAPI Gatewayを叩けてしまうので注意です。

次のように、[リソース]→[オーソライザーを設定したいメソッド]→[メソッドリクエスト]を開きます。

![スクリーンショット 2021-02-13 14.59.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/2c28dad8-0ce2-8a1e-42fb-06aed4bc01e3.png)


許可の部分に先ほど作った`cognito-authorizer`を設定します。選択肢に出てこない場合はリロードなどすると選択肢に出てきます！

![スクリーンショット 2021-02-09 23.26.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/c1b2db19-1a8d-5c07-712d-fc0fb757e8fc.png)

そして最後にデプロイします！
これでオーソライザーの設定は完了です。

### 2. フロントの実装
取得したユーザープールトークンをヘッダーにつけてAPI Gatewayをたたく処理を実装します。

#### axiosのインストール
API Gatewayを叩くのにaxiosを使うために、プロジェクトにaxiosを追加します。

```
$ yarn add axios
```

#### ソースコード

認証時に必要なトークンは下記の方法で取得可能です。

```javascript
const user = Auth.currentAuthenticatedUser()
const idToken = user.signInUserSession.idToken.jwtToken
```

この`idToken`を`Authorization`キーのバリューとしてヘッダーに持たせることで、リクエストが可能になります。


```App.js
import React from "react";
import Amplify, {Auth} from 'aws-amplify';
import awsconfig from './aws-exports';
import {withAuthenticator} from "@aws-amplify/ui-react";
import axios from "axios";
import "./App.css"

Amplify.configure(awsconfig);

function App() {
    const API_URL = "＜API Gatewayで取得したURL＞"
    const [message, setMessage] = React.useState("");
    const [response, setResponse] = React.useState("");

    const handleChange = event => {
        setMessage(event.target.value);
    };

    const handleSubmit = async(event) => {
        const user = await Auth.currentAuthenticatedUser()
        const idToken = user.signInUserSession.idToken.jwtToken
        const headers = {headers: {"Authorization": idToken}};
        axios.post(API_URL, {message: message}, headers)
            .then((response) => {
                if(response.data.message === message){
                    setResponse(response.data.message);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
                alert("登録に失敗しました。もう一度送信してください。");
                console.log(response);
        });
        event.preventDefault();
    }

    return (
        <fieldset>
            <form onSubmit={handleSubmit}>
                <label >
                    <input type="text" value={message} onChange={handleChange} />
                </label>
                <input type="submit" value="送信" />
            </form>
            <div>{response}</div>
        </fieldset>
    );
}

export default withAuthenticator(App);
```

## 実行結果
### ヘッダーあり
入力欄の下に、Lambdaから返ってきたメッセージが表示されるようになっています。入力した値がLambdaを介して返ってきています！

![画面収録-2021-02-09-23.37.09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/3e118387-4652-e1ca-108c-44c40fa83127.gif)


### ヘッダーなし
ちなみに、ヘッダーにidトークンを付けずに実行してみました。

※ API Gatewayをたたくところのみ抜粋

```App.js
        axios.post(API_ADD_URL, {message: message})
            .then((response) => {
                if(response.data.message === message){
                    setResponse(response.data.message);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
                alert("送信に失敗しました。もう一度送信してください。");
                console.log(response);
        });

```

ソースを上記のように変更し、実行すると・・

![スクリーンショット 2021-02-03 20.14.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/c8f86319-a2f3-d284-903d-b5e82171ce52.png)

エラーが出て、Lambdaが実行できないことがわかりました！

## おわりに
無事に、API Gatewayにアクセス認証をつけることができました！今回はもともとCognitoユーザープールを使ってユーザー認証をやっていたので、Cognitoのオーソライザーを使って簡単に設定することができました。既存のシステムの構成によってこれでIP制限を外しても、セキュリティを担保することができたのではないかと思います！めでたし！


## 参考
+ [Cognitoで認証されたユーザーだけがAPI Gatewayを呼び出せるオーソライザーを使ってみた](https://dev.classmethod.jp/articles/api-gateway-cognito-authorizer/)
+ [最近の砂場活動その9: Cognitoで認証済みのユーザーでAPI Gatewayを呼び出す](https://www.yasuhisay.info/entry/2019/01/07/081500)