認証のあるサイトをAWSで構築する際にCognitoユーザープールに必要な最低限の操作

# はじめに

「あのとき、こんなドキュメントを見つけられてれば良かったのに・・・」を形にしてみた。

たとえば、

- Apacheとかnginxとか/何かてきとーな言語/何かてきとーなデータベースの組み合わせで、認証の必要なサイトを構築したことがある。
- アカウント情報はデータベースにあって、何かてきとーな言語であれこれ処理するような形だ。
- ユーザー自身でアカウントを作成したりしない。管理者が操作する運用だ。
- そーゆーのをAWSで構築したいと考えたとする。
- Lambdaがあるから、何かてきとーな言語の代わりができると考えちゃったわけだ。
- そして、AWSにCognitoがあってユーザープールがあることを見つけちゃったとする。
- 別のやり方があるのはわかるが、従来通りに近い方法を踏襲したい。

という状況のときに、AWSのドキュメントを見て、いきなり理解できるだろうか?
できないような気がする。
ある程度AWSを理解してるならいいんだろうけど、何というか独特の表現を把握できてないとイマイチ頭に入ってこない構造をしてると思う。
たぶん、「最低限何をしたらいいの?」を判断できない。

ここでは、想定した状況のときに最低限何をしたらよいか、AWS CLIの操作で説明する。
ここで実行してるAWS CLIのコマンドはAPIに1対1で対応してる。
それをLamndaで実装したら、最低限のレベルでユーザー認証の必要なサイトのユーザー管理部分を実装できるようになる。

何度も最低限と書いてるように、ここで説明した方法は実装するには不足がある。
また、唯一の方法ではない。他の方法が適切な場合もあるだろう。

# 前提条件

それぞれの作成方法は後述するが、既に次の2つが作成済みであることが必要となる。

- Cognitoユーザープール
- Cognitoユーザープールのアプリクライアント

ここではAWS CLIをプロファイルを指定して実行してるが、プロファイルは必須ではない。
普段はプロファイルの指定なしで操作してる場合は、そのように読み替えればよい。

# Cognitoユーザープールの操作

実際にはLambdaで実装するのだが、ここではAWS CLIで操作してみる。
この操作を、お好きな言語を使ってLambdaで実装すれば良い。
ユーザーの作成と削除は、実際の運用でもAWS CLIで操作したほうが簡単かもしれない。

### ユーザーの作成

`admin-create-user`, `admin-initiate-auth`, `admin-respond-to-auth-challenge`, `global-sign-out` を使う。

```bash
# 仮パスワードを指定してユーザーを作成する。
# この時点ではユーザーに連絡しない。
aws --profile 'プロファイル名' cognito-idp admin-create-user \
    --user-pool-id 'ユーザープールID' \
    --username 'ユーザー名に使うメールアドレス' \
    --message-action SUPPRESS \
    --temporary-password '仮パスワード'

# 初回ログインの反応を見る。
aws --profile 'プロファイル名' cognito-idp admin-initiate-auth \
    --user-pool-id 'ユーザープールID' \
    --client-id 'アプリクライアントID' \
    --auth-flow ADMIN_NO_SRP_AUTH \
    --auth-parameters 'USERNAME=ユーザー名に使うメールアドレス,PASSWORD=仮パスワード'
# 次のような結果が表示される
#   {
#       "ChallengeName": "NEW_PASSWORD_REQUIRED",
#       "Session": "この部分をセッション情報としてメモする",
#       (略)
#   }

# ユーザーに連絡する初期パスワードを設定する。
aws --profile 'プロファイル名' cognito-idp admin-respond-to-auth-challenge \
    --user-pool-id 'ユーザープールID' \
    --client-id 'アプリクライアントID' \
    --session 'セッション情報' \
    --challenge-name NEW_PASSWORD_REQUIRED \
    --challenge-responses 'USERNAME=ユーザー名に使うメールアドレス,NEW_PASSWORD=初期パスワード'
# 次のような結果が表示される
#   {
#       "ChallengeParameters": {},
#       "AuthenticationResult": {
#           "AccessToken": "この部分をアクセストークンとしてメモする",
#           (略)
#       }
#   }

# ログアウトしておく。
aws --profile 'プロファイル名' cognito-idp global-sign-out \
    --access-token 'アクセストークン'

# 以上の流れにするのは「"ChallengeName": "NEW_PASSWORD_REQUIRED"」に対する実装を省略するため。
```

### ユーザーの削除

`admin-delete-user` を使う。

```bash
aws --profile 'プロファイル名' cognito-idp admin-delete-user \
    --user-pool-id 'ユーザープールID' \
    --username 'ユーザー名に使うメールアドレス'
```

### ログイン

`admin-initiate-auth` を使う。

```bash
aws --profile 'プロファイル名' cognito-idp admin-initiate-auth \
    --user-pool-id 'ユーザープールID' \
    --client-id 'アプリクライアントID' \
    --auth-flow ADMIN_NO_SRP_AUTH \
    --auth-parameters 'USERNAME=ユーザー名に使うメールアドレス,PASSWORD=パスワード'
# 次のような結果が表示される
#   {
#       "ChallengeParameters": {},
#       "AuthenticationResult": {
#           "AccessToken": "この部分をアクセストークンとしてメモしておく",
#           (略)
#       }
#   }
# ここでは「クライアント側にアクセストークンを通知して以降はアクセストークンを送ってもらう」という形を想定する。
# とりあえず的なレベルではこの程度だが、実際にはもっと複雑になる。
```

### ログイン後の認証

`get-user` を使う。

```bash
aws --profile 'プロファイル名' cognito-idp get-user \
    --access-token 'アクセストークン'
# ユーザーを取得できれば「アクセストークンは正しい/ログインしてる」と判断する。
# そうでなければ「アクセストークンは正しくない/ログインしてない」と判断する。
# とりあえず的なレベルではこの程度だが、実際にはもっと複雑になる。
```

### ログアウト

`global-sign-out` を使う。

```bash
aws --profile 'プロファイル名' cognito-idp global-sign-out \
    --access-token 'アクセストークン'
```

### パスワードの変更

`change-password` を使う。

```bash
aws --profile 'プロファイル名' cognito-idp change-password \
    --previous-password '以前のパスワード' \
    --proposed-password '新しいパスワード' \
    --access-token 'アクセストークン'
```

# このドキュメントの不足

たぶん、実際に実装してみると、いろいろと追加したくなる。
次のような点を検討することになると思う。

- ふつー考えるに、パスワード忘れ対策は必要だ。
- アクセストークンの期限は1時間なので、期限が切れたらリフレッシュトークンを使って更新する必要がある。
- リフレッシュトークンにも期限がある。期限が切れたら再ログインを促すことになる。
- 一定期間が経過したらパスワード変更を要求する機能も欲しくなる。それの是非はともかく、欲しがる人はいるので。

# 前提条件を整える

### テスト用のIAMユーザーを準備する

- てきとーなユーザー名を考えて、AWSマネジメントコンソールのIAMで作成する。
- アクセスの種類は「プログラムによるアクセス」で、グループに追加する必要はない。タグもいらない。
- 「このユーザーにはアクセス権限がありません」とか言われるが、気にしない。あとで権限を設定する。
- 作成したら、アクセスキーIDとシークレットアクセスキーをメモっておく。
- 作成したユーザーを選ぶと「アクセス権限」を確認できる。
- 「アクセス権限の開始方法」の囲みの中の「インラインポリシーの追加」をクリックする。
- 次の内容のポリシーを、てきとーな名前をつけて設定する。

    ```json
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "AlowCognitoUserPool",
                "Effect": "Allow",
                "Action": "cognito-idp:*",
                "Resource": "*"
            }
        ]
    }
    ```

### AWS CLIを準備する

- [AWSのドキュメント](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/installing.html)を参考に、AWS CLIを使えるように準備する。
- プロファイルを設定しておく。例えばこんな感じ。

    ```bash
    aws --profile 'プロファイル名' configure
    # AWS Access Key ID [None]: アクセスキーID
    # AWS Secret Access Key [None]: シークレットアクセスキー
    # Default region name [None]: ap-northeast-1
    # Default output format [None]: json
    ```

### ユーザープールの作成

```bash
aws --profile 'プロファイル名' cognito-idp create-user-pool \
    --admin-create-user-config 'AllowAdminCreateUserOnly=true,UnusedAccountValidityDays=7' \
    --auto-verified-attributes email \
    --mfa-configuration OFF \
    --policies 'PasswordPolicy={MinimumLength=8,RequireUppercase=true,RequireLowercase=true,RequireNumbers=true,RequireSymbols=true}' \
    --pool-name 'ユーザープール名' \
    --username-attributes email
# 次のような結果が表示される
#   {
#       "UserPool": {
#           "Id": "この部分をユーザープールIDとしてメモしておく",
#           (略)
#       }
#   }
```

## ユーザープールと関連するアプリクライアントの作成

```bash
aws --profile 'プロファイル名' cognito-idp create-user-pool-client \
        --user-pool-id 'ユーザープールID' \
        --client-name 'アプリクライアント名' \
        --explicit-auth-flows ADMIN_NO_SRP_AUTH \
        --no-generate-secret \
        --read-attributes email \
        --refresh-token-validity 30 \
        --write-attributes email
# 次のような結果が表示される
#   {
#       "UserPoolClient": {
#           (略)
#           "ClientId": "この部分をアプリクライアントIDとしてメモしておく",
#           (略)
#       }
#   }
```