Cognitoで色んな社会の願いを叶えられるか考えてみた(2021/3版)

昨年の[この記事](https://qiita.com/taniyuu/items/c37be3d0c275f797962f)でもCognito取り上げましたが、今年度も相変わらずCognito使うことがあったので、書きます。

# アブスト
2020年度は、（みんな忘れてるかもしれないけど）某D口座の話してた時が認証周りでは一番盛り上がってた気がします。

そんな最中にCognitoはどうなの、みたいなことを調べることが急増したので、Q&Aみたいな感じ→**社会の願い**で羅列していきます。

# 基礎知識
この記事では、断りのない限り、「Cognito」は「Amazon Cognito」のことを指します。「ユーザプール」は「Amazon Cognitoのユーザプール」(IDプールってのがありますが、今回はそれじゃない)のことを指します。
何も知らない人は、BlackBeltをさっと眺めておくのが良いと思います。というか、それで満足したらここでブラウザバックで。

https://www.slideshare.net/AmazonWebServicesJapan/20200630-aws-black-belt-online-seminar-amazon-cognito

今回は、この資料や公式リファレンスではあんまり言及されてないけど、よく聞かれそうなことを以下で記載していきます。


# ベースの認証要件
何もないと書きづらいので、適当にベースの要件を決めちゃいます。

- モバイルアプリ、またはWebブラウザからアクセスされるサービスの、認証認可にCognito、Cognitoユーザプールを使う
- エンドユーザは、自身のIDとパスワード、メアド、携帯番号を登録する。
    - IDは他の人とダブっていないければOK。※ちなみに、ユーザプールの属性名は`username`
    - メアド、携帯番号は複数IDで使い回してもOK。
- MFA(２要素認証)をONにする。MFAには登録された携帯番号を利用する。


# 社会の願い
本題です。

## パスワードは英文字数字両方が必ず含まれている
パスワードポリシーの要件ですね。
Cognitoは、こんな感じです。
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html

- Minimum length, which must be at least 6 characters but fewer than 99 characters
- Require numbers
- Require a special character from this set:
```
= + - ^ $ * . [ ] { } ( ) ? " ! @ # % & / \ , > < ' : ; | _ ~ `
```
- Require uppercase letters
- Require lowercase letters

この組み合わせが使えるので、**そのままじゃ無理です**w
大文字、小文字それぞれを必須にはできますが、大文字小文字または、というパターンは作れないです。


以下のようなもので対応できました。

1. 【確実】APIサーバでバリデーションを自作する
1. 【サインアップ時のみ】[Pre Sign-up Lambda Trigger](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html)でバリデーションを自作する
    - サインアップ時のみとありますが、裏を返すとパスワード忘れとかパスワード変更とかには対応できないので、あまりいい案ではなさそうです。現実味が出たらもう少し細かく書きます。
1. パスワードポリシーを変える
    - まだ間に合う・・・！なら今がチャンス。

## パスワードを世代管理したい
これもパスワードポリシーの要件ですが、シンプルにCognitoには**機能がない**です。

1. 諦めて[Auth0](https://auth0.com/docs/connections/database/password-options#password-history)を使う（最大24世代）※ちょっと調べたら出てきたから
1. Auth0みたいなのを自作する ~~テストすんのめんどそう・・・~~


## メールアドレス、携帯番号を変更するときは、ユーザが確認してから
ここまでで書いていなかったですが、ユーザプールに登録されたユーザのメールアドレスや携帯番号は、登録時に自動的に確認メールまたはSMSを送信し、確認を取ります。
ところが、変更時は、デフォルト[^1]では**ユーザ確認前に属性変更されてしまいます**。

[^1]: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_UpdateUserAttributes.html

あるユーザが`hogehoge＠piyo.com`から`fugafuga＠piyo.com`に変えたい場合、こんな感じです。

||メールアドレス|確認済みかどうか|
|---|---|---|
|変更前|hogehoge＠piyo.com|TRUE|
|変更後&確認前|fugafuga＠piyo.com|FALSE|
|変更後&確認後|fugafuga＠piyo.com|TRUE|

ここで大事なのは、「変更後&確認前」のタイミングの扱いをどうするかです。
デフォルトの動きに沿わせると、宙ぶらりんな`fugafuga＠piyo.com`になっているので、このユーザで使えるメールアドレスは存在しない、となるのが妥当だと思います。
が、ここでもし`hogehoge＠piyo.com`が有効なものとして扱いたい、という場合だと、このデフォルトの動きが使えないことになります。

ということで、解決させてみました。

1. 確認が必要な属性を変更するときは、変更専用のユーザプールに登録するようなフローにする：[モバイルアプリの実装サンプル](https://github.com/taniyuu/CognitoMfaSample/blob/main/README.md)
    ![](https://github.com/taniyuu/CognitoMfaSample/raw/main/docs/flow_update_attr.png)
1. 説得する
1. 諦める

意外とこれだけでできちゃうならアリかも、と思いました。
もちろんRDBなど利用するのもいいんですけど、個人情報が分散するのがなーと思ったので。


## メール、SMSの送信サービスを任意のものにしたい
CognitoからメールやSMSを送信するケースは、ここまで記載した通り確認時やMFAのような場合に発生します。
AWSのマネージドコンソールでは、いかにもSES（AWSのメールサービス）やSNS（AWSの通知などで使うサービス）経由でしか使えない感じで書いていますが、全然それ以外（メールならCuenoteとか、SMSなら空電プッシュとか）を経由させて使えることがわかりました。

それが**[Custom sender Lambda Triggers](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sender-triggers.html)**です。
Lambdaで各送信イベントをキャッチして、各サービスにREST APIで送信するとか、そういうことができます。

これ、なぜかAWS公式ブログにも何も書いていないけどしれっと追加されてる機能で、経過を見ると2020年に追加されてます。
まあ、AWSで完結させて欲しいんでしょうね。。。気持ちはわかる。

# まとめ
認証周りはまだまだこれから進歩して行くとAuthleteさんの誰かが言っていると思うので、ウォッチしつつ。
言いたかったのはそっちじゃなくて、Cognito（とそれに紐づくAmplify Auth）は色々めったうちにされがちだけど、コスパは悪くないと思うよって話をしたかった。

みんな大好きセキュリティに注ぎ込むお金さえあればこういうのもあるよ、とか、これやらないとあの過去の事件を再現できるリスクあるよ、とかそういう話をするための材料としては、いいものが揃ったなと思っている。