Cognitoを使ってログイン制御してみる

# 背景
AWS使ってサーバーレスで自分用の家計簿的なwebサービスを勉強も兼ねて開発中。大分実用に耐えるようになってきた所。
[前回の投稿](https://qiita.com/silverbox/items/d15e38fd57ffd3ebc848)

自分しか使わない、かつ自分しかそのアドレス（CloudFrontが自動生成した不規則文字列）は知らないけど、外部公開はされてるので、不正アクセスを防ぐためにも認証機構をつけたい。Cognito使いたい。

# 参考にさせてもらったページ
[Cognitoが提供する認証機能を手を動かしながら理解](https://www.wakuwakubank.com/posts/696-aws-cognito/)
[CloudFormation で Cognito](https://qiita.com/y13i/items/1923b47079bdf7c44eec)
[AWS Cognitoを使ってみた](https://qiita.com/small-teton/items/2ed23e203ac0029a93ae)
[vue-cliで作成したSPAにシンプルにCognitoログインを組み込む](https://qiita.com/daikiojm/items/b02c19cfea6766c308ca)
[Cognitoのユーザプールでの認証するところまでを最速でやる](https://qiita.com/k_hoso/items/afe9aa8183b8bf0651a1)

# まずは理解
AWS Cognitoがログイン認証とそれに関わるAWSリソース制御が可能という事は知ってるけど、実際にどのようなイメージでそれを制御しているのかを知らない。まず、AWSコンソールでCognitoのページに行く。

#### ２つの機能
「ユーザープールの管理」「IDプール」の二つが現れる。追加画面を開いて、何が出来るか確認していく。

###### ユーザープール
AWSコンソールメッセージによると、**アプリユーザーのサインアップとサインインオプションを提供するユーザーディレクトリです** だそうで。左側に以下のタブが存在。

 - 属性：ユーザーに関する情報（名前とかE-mailアドレスとか）の設定。カスタムも設定可能。
 - ポリシー：パスワードのポリシー関係の設定
 - MFAそして確認：多要素認証を使うか、パスワードリセット方法などの設定。これに関係して、SMSを使用してメールを送信する為のIAMロールをCognitoに設定する必要あり。
 - メッセージのカスタマイズ：認証に使用するメールの送信元、Reply-toやメッセージ中身の設定
 - タグ：多くのユーザープールを管理する時に使うのかな？
 - デバイス：デバイスの情報を記憶するか。新デバイスでアクセスした時に警告メールを送ったりする為に必要？後は、一度ログインしたデバイスはしばらく再認証しないとか？
 - アプリクライアント：このユーザープールを使うアプリの追加。それ用のキーが発行される。その認証方法も細かく設定できるらしい。
 - トリガー：ユーザー認証の各タイミングをトリガーにしてLambda関数を実行できるらしい。

###### IDプール
同じく、**ユーザーに別の AWS のサービスへのアクセスを許可する AWS 認証情報を提供します** だそうで。こちらはタブ形式でなく、ウィザード形式での設定のみ。

 - ID プールを作成する：基本情報。名前や、認証プロバイダーの選択。Cognitoを始めとしてFacebookやtwitterも使える。
 - アクセス権限の設定：Cognitoが使用するIAMロールの設定。認可された場合のロールと、認可されてない場合のロールの二つが必要。

#### 理解中間まとめ

 - Cognitoのユーザープールは、ユーザーの管理に関わる機能を提供。認証プロバイダーの一つ。
 - CognitoのIDプールは、認証プロバイダーを使って、AWSの機能認可を行う。
   - 例１：認証してる場合、S3の特定共通リソースアクセス許可
   - 例２：S3のユーザー毎に異なる特定リソース許可
   - 例３：ユーザー毎に、DynamoDBでユーザー毎に異なるキーの値のデータを許可
   - その他、AWS機能に関する制限が可能
 - CognitoのIDプールで使用する認証プロバイダーにCognitoのユーザープールを使用可能。代わりにFacebookとかtwitterの認証プロバイダーも使用できるが、その場合、当然ながら認証機構側でその機構を使用する為のIDなどを事前に取得しておく必要がある。

# 決めるべき事
 - 認証プロバイダーをユーザープールにするか外部認証プロバイダーにするか
 - ユーザープールにする場合、どのような認証をするか
 - ユーザープールを使うアプリクライアントがどの様な認証フローを必要とするか
 - 認証された場合とそうでない場合にAWSリソースに対してどのようなアクセス制限を行うか

#### 今回のケースでの方針
 - 外部認証プロバイダーのIDとか持ってないので、ユーザープールを認証プロバイダーに使う
 - 目的はログイン機構だけなので、認証詳細はデフォルトでOK。以下部分を変える。
  - e-mailをユーザーIDにする
  - 管理者のみユーザー追加可能（他の人に使われるのを防ぐ目的だから）
  - アプリクライアントの詳細設定。JavaScript（ブラウザ）では「クライアントシークレットを生成」にチェックすると使えないらしいので外す。ブラウザアプリでは「ALLOW_USER_SRP_AUTH」をチェックすればよいらしい。
 - 今回はログイン制御だけ出来ればいいので（AWSリソースの制限は無い）、IDプールのロールはデフォルトのまま。

#### 実際の作成ステップ

###### ユーザープール
1. **ユーザープールの作成**
2. 名前を入れる（今回は、MyAppUserPool）
3. デフォルトを確認する
4. **エイリアス属性** タブにて、「E メールアドレスおよび電話番号」をチェック。e-mailをIDとして使うようにする
5. **ポリシー** タブにて、「管理者のみにユーザーの作成を許可する」をチェック。
5. **アプリクライアント** タブにて「アプリクライアントの追加」。
6. アプリ名前を指定（今回は、MyHomeAccount）
7. ブラウザアプリなので、必須チェック以外外す
8. **アプリクライアントの作成** を行う
9. **確認** タブにて、「プールの作成」を行う。
10. 左上リンクから、Cognito-ユーザープールトップに戻る。
11. 作ったユーザープールをクリックして開く。
12. **全般設定** タブにて、**プール ID** をメモ
13. **アプリクライアント** タブにて、**アプリクライアントID**をメモ

###### IDプール
14. **新しいIDプールの作成** をクリック
15. 名前を入れる（今回は、MyAppIDPool）
16. **認証プロバイダー** を展開し、Cognitoタブにて、ユーザープール作成時にメモしたユーザープールID、アプリクライアントIDを入力
17. **プールの作成** を行う。
18. ロール指定の画面が出てくる。一旦そのまま作成。
19. 左上リンクから**フェデレーティッドアイデンティティ**を選択し、IDプール一覧を表示
20. 作成したIDプールを選択
21. 右上の**IDプールの編集**を選択
22. **認証されていないロール**、**認証されたロール** それぞれで、新しいロールの作成を選択＆デフォルトのまま**許可**
23. **変更の保存**

###### ユーザー作成
管理者のみユーザー作成出来る設定なので、ユーザープールの管理画面でユーザー作成。AWSコンソールで、**ユーザープール** => 作成したユーザープール => **ユーザーとグループ** にて仮パスワードでユーザー作成。

###### Vue.jsでの画面を作成
1. [vue-cliで作成したSPAにシンプルにCognitoログインを組み込む](https://qiita.com/daikiojm/items/b02c19cfea6766c308ca) をトレース。
2. ステップ１で使用している **cognito.js** を一部修正。cognitoUser.authenticateUser に **newPasswordRequired** 処理を追加 ※１
3. 実際にログイン。新しいパスワード入れる。※２

※１：ログインすると、以下のエラーがブラウザコンソール出てきたので

```
Uncaught (in promise) TypeError: callback.newPasswordRequired is not a function
```

変更部分詳細

```js:cognito.js
  /**
   * username, passwordでログイン
   */
  login (username, password) {
    const userData = { Username: username, Pool: this.userPool }
    const cognitoUser = new CognitoUser(userData)
    const authenticationData = { Username: username, Password: password }
    const authenticationDetails = new AuthenticationDetails(authenticationData)
    return new Promise((resolve, reject) => {
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: (result) => {
          // 実際にはクレデンシャルなどをここで取得する(今回は省略)
          resolve(result)
        },
        onFailure: (err) => {
          reject(err)
        },
        newPasswordRequired: (userAttributes, requiredAttributes) => {
          var newPass = window.prompt('新しいパスワードを入力してください', '入力値は表示されます。別の人に見られていない事を確認してください')
          cognitoUser.completeNewPasswordChallenge(newPass, {}, this)
        }
      })
    })
  }
```



※２：新しいパスワード入れた後、以下のエラーがブラウザコンソール出てきた。completeNewPasswordChallenge 関数のcallbackもちゃんと実装必要があるはずだが、今回はスルー。Failしたと思われるが、ユーザーのステータスをAWSコンソールで確認すると**CONFIRMED**になっていたし。今後ここらへんもうちょっとちゃんとすることにする。

```
Uncaught (in promise) TypeError: callback.onFailure is not a function
```

(2019年12月14日追記)
[ちゃんとしました](https://qiita.com/silverbox/items/327b940e89c1634d8ff8) 。callbacksを雑にしていた事が原因でした。詳細はリンク先の別投稿で。

# 結論的な手順まとめ
1. [AWS Cognitoを使ってみた](https://qiita.com/small-teton/items/2ed23e203ac0029a93ae) を参考に、CognitoのユーザープールとIDプールを作成。
2. [vue-cliで作成したSPAにシンプルにCognitoログインを組み込む](https://qiita.com/daikiojm/items/b02c19cfea6766c308ca) を参考に、ステップ１で作成したIDを使ってログイン画面実装。

※ただし、自分の目的に合わせて微調整は必要。今回の投稿が微調整の参考になれば幸いです。

# 思った事
 - もし、複数のユーザー毎に異なるS3領域やデータを保存する場合、S3のフォルダ構成やDynamoDBのキー構造も考慮する必要が出てくる。今回のケースは複数ユーザーは考慮しないが、今後そのようなものを設計する場合は、Cognitoでの制御方法を考慮に入れる事が必要そう。
 - Vueに基本機能として認証制御が存在していた。考えれば当然だが素晴らしい。
 - Cognitoとそれ用のjavaScriptライブラリで簡単に認証機能つけられた。素晴らしい。
 - 今回の件だけなら、Cognitoユーザープールだけで十分だったかも。