LambdaもAPI Gatewayも不要。AWS Amplify Frameworkを使ってブラウザ側のコードだけでWebアプリを作る。

# 0. 概要

Cognito ID Poolの機能を使うと、何かのサービスで認証に成功したユーザに対してAWS認証情報を発行できます。このAWS認証情報と、[AWS SDK for JavaScript](https://github.com/aws/aws-sdk-js)があれば、ブラウザ側のコードだけでAWSのサービス触り放題じゃね･･･？と思ったので、やり方を調べてみました。

認証サービスは何でもいいのですが、ユーザごとに権限を分ける方法を調べたかったので、Cognito User Poolを使います。
イメージはこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/1eac738e-e6c8-bf98-c6ee-45e5f2c62e1b.png)


はじめは[amazon-cognito-identity-js](https://github.com/amazon-archives/amazon-cognito-identity-js)を使ってあれこれしようと思ったのですが、User PoolとID Poolの連携がどうにもうまく行かないしそもそも資料もあんまりない。あれこれ調べているうちに、いつのまにか新しいライブラリが登場していることを知りました。

それが`Amplify Framework`です。
https://github.com/amazon-archives/amazon-cognito-identity-js

既存のライブラリを組み合わせたフレームワークなので、これならUser PoolとID Poolの連携がうまく行きそうです。さっそく使ってみます。

# 1. 準備編

フレームワークを触る前に、最初に示した概要図まわりの状態を作ります。
必要になるのは、「ユーザ」「グループ」「ロール」「ポリシー」です。
この辺りの詳しい構成は、概要図では省略してしまったのでもう少し詳細な図を載せます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/dc74b542-d7ce-1cc6-91c0-0d3df63dca08.png)

* `User Pool`では「ユーザ」と「グループ」を管理します。ユーザはグループに所属します。  
* `IAM`では「ポリシー」と「ロール」を管理します。ポリシーは「何が出来るか」の定義で、ロールはポリシーをまとめたものです。
* この２つをつなぐのが`ID Pool`です。`User Pool`によって認証されたユーザのグループを確認し、それに対応したロールを持つAWS認証情報を返します。

以上を踏まえて、さっそく環境を作っていきましょう。
※ この図の構成が分かっている方は準備編は読み飛ばして構いません。

## 1.1 User Pool

ユーザプールと、それを利用するアプリクライアントを作成します。
右側の「ステップに沿って実行する」から作成します。
![ユーザプールの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/cb50d606-f0bd-1cff-c4fb-e5560e0a4b2c.png)

以下の２点の修正箇所以外は全てデフォルトのまま「次のステップへ」ボタンで進んで構いません。

### 修正箇所1. 必須属性の解除
ユーザIDとパスワードのみで検証するシンプルなアプリを作りたいので、メールアドレスの必須チェックは外します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/d8746486-50ad-e89f-b872-096b8cc5a4d9.png)

### 修正箇所2. アプリクライアントの作成
この認証機能を使うアプリクライアントを作成します。
![アプリクライアントの追加をクリック](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/7cd31e2f-1172-4275-1c11-9bb876536caf.png)
![アプリクライアントの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/2f842bf3-742c-5a1c-d82c-3f1b1e40bab9.png)

### プールIDとプールARNの確認
最後の確認画面で「プールの作成」ボタンをクリックしたら、ユーザプールの作成は完了です。「プールID」と「アプリクライアントID」は後ほど使うのでメモしておきます。
![プールIDの確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/26d5fd0b-6b47-f44d-f9e7-4c9a4a185efd.png)
![アプリクライアントIDの確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/1d387240-ff6d-78c4-fd24-41354d8e8229.png)


## 1.2 IDプールの作成
ユーザプールと連携するIDプールを作成します。
![IDプールの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/148f0537-fd55-c177-fb56-080068de6e80.png)
![IDプール名の入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/201ab571-4813-9986-6701-394921ff5829.png)
![プールID、アプリクライアントIDの入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/1d2c232e-521a-46a8-e0fd-70099cd54b9f.png)
![IDプールデフォルトロールの作成可否](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/4c52dddc-ae76-69a8-46da-009e79a0676f.png)
↑「認証に成功しているユーザ」「していないユーザ」それぞれに与えるロールを作成しても良いか聞いています。今回は、Cognito User Poolのグループに対応したロールを与える予定なので、使用しません。
![IDプールのID](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/375edbcf-442c-7213-4299-1eabdf735d35.png)
↑最後に表示されるIDプールのIDをメモしておきます。

グループに対応したロールのAWS認証情報を提供してもらえるように、設定を変更します。
![IDプールの編集](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/84eaa6ed-e403-2303-80ae-79d80150d524.png)
![トークンからロールを選択する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/bb80c129-ee29-15ea-d596-565be77d3c5a.png)


## 1.3 ポリシーの作成
一度Cognitoを離れてIAMに移動し、ポリシーを作成します。
ポリシーでは、ログインユーザが操作できる権限の範囲を作成します。

検証環境であればなんでもいいのですが、公開環境で適当に大きな権限を与えてしまうと当然、多大な被害が出てしまうので慎重に。
今回は、Cognitoに登録されているユーザのリストを表示できる権限を作ってみます。

![ポリシーの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/119319aa-4f20-92be-e2d2-dda95da001e1.png)
![ポリシーの定義](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/fcef7709-0932-8438-9ccc-0ea58e625433.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/0b2318d7-564a-51fd-1036-f11f69f24a40.png)

## 1.4 ロールの作成
先ほど作成したポリシーを持つロールを作成します。
![ロールの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/912687ca-309d-18be-7e5a-5fa4bc6294bc.png)
![ロールの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/5700df5d-7521-a650-1c73-365fb2805e12.png)
![ポリシーの選択](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/f49d73ec-60db-51c3-5a23-d32309fc1e1f.png)
![ロールのタグ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/51e1a118-039e-4ca3-a2dd-d3c34ff2421f.png)
![ロールの名前](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/a674d63d-72e1-ef5f-e536-251c45a31dfa.png)

これで管理者用のロールが作成できました。
説明は省きますが、もう一つ、一般ユーザ用の何もポリシーを持たない「amplify-test-user-role」を作成しておきます。

## 1.5 ログイン用ユーザの作成
動作検証用にユーザを作成します。
![ユーザの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/c41c588a-42f8-7865-20be-00652ab24ef6.png)
![ユーザの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/ac37e493-2c99-7a9f-2029-25514937a4d4.png)
![権限あり、なしユーザ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/5a5ce936-647c-7dbe-cc4f-9d6deef3947b.png)


## 1.6 グループの作成

再びCognitoに戻り、今度はグループを作成します。
![グループの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/0025bc3c-8ecd-d87d-f317-c5c8951e5eb3.png)
![グループの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/7a9870d1-0928-5ccb-64aa-9fd01006b271.png)
![グループの選択](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/85a610ef-ff73-c535-fe14-06c54b776543.png)
![ユーザの追加](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/876163f6-84ec-29c9-d4d3-ee7b1c7fcc5a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/6281a2e4-9081-64fb-539d-73aa07acc49d.png)

さらにもうひとつ、管理者ではないユーザ用のグループ「user」を作ります。IAMロールには、何もポリシーを持たない「amplify-test-user-role」を設定しておきましょう。
このグループには、no_roleユーザを所属させておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/a7f78fed-326c-695e-7b0d-b1681073254e.png)

これで環境設定は終わりです。


# 2. クライアント側コードの作成

ここからがやっと本題です。Amplify Frameworkを使って簡単な認証機能と、AWS認証情報を使った利用者情報の取得を試してみましょう。

## 2.1 プロジェクト作成

`create-react-app`コマンドでさくっと作ります。
`Amplify Framework`に`React`は必須ではないので何でもいいです。

```bash
# プロジェクトの作成
create-react-app amplify-test
cd amplify-test
```
```bash
# aws-amplifyとaws-sdkをインストール
npm install aws-amplify aws-sdk --save
```

## 2.2 Amplify初期化コードの埋め込み

最初に読み込まれる場所に、Amplifyの初期化コードを書きます。
準備編で作ったIDプール、ユーザプール、アプリクライアントのIDを、`Auth`プロパティの中に指定します。

```js
import Amplify from "aws-amplify";

// Amplifyの設定
Amplify.configure({
  Auth: {
    // IDプールのID
    identityPoolId: "ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    // リージョン
    region: "ap-northeast-1",
    // ユーザプールのID
    userPoolId: "ap-northeast-1_XXXXXXXXX",
    // アプリクライアントID
    userPoolWebClientId: "XXXXXXXXXXXXXXXXXXXXXXXXXX",
  },
});
```

## 2.3 ログイン処理
Cognitoにログインする仕組みを作ります。
AmplifyのAuthクラスに、Cognitoにログインするための関数`signIn`が用意されています。
その他の関数の仕様はこちらを参照してください。
https://aws-amplify.github.io/amplify-js/api/classes/authclass.html
使い方のサンプルもこちらにたくさん載っています。
https://aws-amplify.github.io/docs/js/authentication

```js
import { Auth } from "aws-amplify";

/**
 * ログイン処理
 * @param {string} userid
 * @param {string} password
 */
export async function login(userid, password) {
  const user = await Amplify.Auth.signIn(userid, password);
  if (user.challengeName === "NEW_PASSWORD_REQUIRED") {
    // 初回ログイン時は、必ずパスワードを再設定するように求めてくる
    // 新しいパスワードは同じでも構わないので、completeNewPasswordに同じパスワードを渡してしまう
    await Auth.completeNewPassword(user, password);
  }
}
```

## 2.4 利用者情報取得処理

こちらは管理者権限の利用者だけが使える処理です。
`currentCredentials`から、現在ログイン中の利用者が所属するグループに対応したAWS認証情報が取得できます。
これをCognitoUserPoolsの管理クラスに渡すことで、管理機能が使えるようになります。


```js
/**
 * Cognito User Pool内のユーザを取得する
 * @return ユーザ一覧
 */
export async function getCognitoUsers() {
  // ログイン中のユーザのグループに対応したAWS認証情報を取得する
  const credentials = await Auth.currentCredentials();
  // Cognito User Poolの操作クラスにAWS認証情報を渡して初期化する
  const cognitoServiceProvider = new AWS.CognitoIdentityServiceProvider({
    credentials: Amplify.Auth.essentialCredentials(credentials),
    region: "ap-northeast-1",
  });

  // ユーザプール内のユーザを取得する (AWS認証情報がないと使えない部分)
  const result = await cognitoServiceProvider.listUsers({ UserPoolId: "ap-northeast-1_XXXXXXXX" }).promise();
  const users = result.Users;
  return users;
}
```

## 2.5 ログインフォーム、利用者一覧表示画面の作成

Amplify Frameworkとは関係ない部分ですが、一応載せておきます。

```jsx
import React, { useState } from "react";
import { getCognitoUsers, login } from "./cognito";

function App() {
  // ユーザID
  const [userId, setUserId] = useState("");
  // パスワード
  const [password, setPassword] = useState("");
  // ログイン後に、Cognitoから取得する予定のユーザ一覧
  const [users, setUsers] = useState([]);
  // エラーメッセージ
  const [error, setError] = useState("");
  // ユーザID変更時
  const onChangeUserId = (e) => {
    setUserId(e.currentTarget.value);
  };
  // パスワード変更時
  const onChangePassword = (e) => {
    setPassword(e.currentTarget.value);
  };
  // ログイン時
  const onLogin = async (e) => {
    e.preventDefault();
    setError(""); // エラーメッセージクリア
    try {
      // ログインを実行する
      await login(userId, password);
    } catch (e) {
      setError(e.message); // エラーメッセージ表示
    }
  };
  // 利用者一覧取得時
  const onClickGetUsers = async () => {
    setError(""); // エラーメッセージクリア
    try {
      // ログインに成功したら、ユーザ一覧を取得する
      const users = await getCognitoUsers();
      setUsers(users);
    } catch (e) {
      setError(e.message); // エラーメッセージ表示
    }
  };
  return (
    <div className="App">
      <form onSubmit={onLogin}>
        <div>
          USER:<input type="text" onChange={onChangeUserId} value={userId} />
        </div>
        <div>
          PASS:<input type="password" onChange={onChangePassword} value={password} />{" "}
        </div>
        <button type="submit">ログイン</button>
      </form>
      <button type="button" onClick={onClickGetUsers}>
        利用者一覧取得
      </button>
      {users.length > 0 ? (
        <table>
          <thead>
            <tr><th>利用者名</th><th>状態</th><th>作成日</th></tr>
          </thead>
          <tbody>
            {/* ユーザの数だけループ */}
            {users.map((user) => {
              return (
                <tr key={user.Username}>
                  <td>{user.Username}</td><td>{user.UserStatus}</td> <td>{user.UserCreateDate.toString()}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      ) : null}
      <p className="error-message">{error}</p>
    </div>
  );
}

export default App;

```

# 3. 動作確認

## 3.1 ログインなしの場合
まずは、ログインをせずにCognitoの利用者一覧を取得してみましょう。
![認証なしで一覧取得.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/8516748d-d315-5fe6-dac2-8a4cfdfb127f.gif)

「AWS認証情報がない」というエラーが出ています。

## 3.2 ログイン失敗の場合
存在しないユーザでログインした場合も一応見ておきます。
![ログイン失敗.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/87d5a971-5080-400a-cedf-8382bd60c9f3.gif)

ログインできていないので、当然「AWS認証情報がない」というエラーが出ます。

## 3.3 管理者グループではない利用者の場合
管理者権限を持たない利用者でログインしてみます。

![ログイン成功1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/24a7a23b-fa46-ec34-e50d-fb06623f8f58.gif)

エラーとしてこのようなメッセージが返って来ました。

> User: arn:aws:sts::000000000000:assumed-role/amplify-test-user-role/CognitoIdentityCredentials is not authorized to perform: cognito-idp:ListUsers on resource: arn:aws:cognito-idp:ap-northeast-1:000000000000:userpool/ap-northeast-1_XXXXXXXXXX

`amplify-test-user-role`というロールがIDプールから発行されましたが、このロールは`cognito-idp:ListUsers`を実行する権限(ポリシー)を持っていないので、エラーが発生しているのがわかります。

## 3.4 管理者グループの利用者の場合
最後に、管理者権限を持つ利用者でログインしてみます。

![ログイン成功2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215631/2df13e58-b698-c200-7f2c-cc46fa44787e.gif)

ユーザー「iewori」はロール`amplify-test-admin-role`を持っており、このロールは`cognito-idp:ListUsers`を実行する権限(ポリシー)があるので取得に成功しています。

# まとめ
`Amplify Framework`で調べると一番よく使われるケースとしては`S3`の操作や`GraphQL`との連携あり、それらは[Strage](https://aws-amplify.github.io/docs/js/storage)や[API](https://aws-amplify.github.io/docs/js/api)として便利なクラスがすでに提供されています。なので今回のCognito管理機能の利用は実は特殊ケースです(今のところ)。
そんな特殊ケースでもAWS認証情報さえ取得できればサーバー側のコードを書くことなく実装できてしまうことが、お分かりいただけたかと思います。

AWSのサービスを直接触る形になるので`APIGateway`と`Lambda`を通じたREST API形式よりもセキュリティ上の設計をより厳密にする必要はありますが、コード量削減と効率アップのためにチャレンジしてみてはいかがでしょうか。