モダンなフロントエンド開発スタイルを知らずにAmazon Cognitoを試してみた

## AWSマネージドサービスを試したい
やっぱり認証が使えると捗る気がする、ということで、[Amazon Cognito](https://aws.amazon.com/jp/cognito/)を試してみました。
[Amplify](https://aws.amazon.com/jp/amplify/)というライブラリ群がリリースされて実装が簡易になったという話は聞いた…だけ。

## コンソールから設定
まずどこからとっつけばいいのかというところですが、AWSコンソールからCognitoを触ってみましょう。
色んなサイトを参考にさせていただきました。

- [Amazon Cognitoを使ったサインイン画面をつくってみる～サインアップ編～](https://www.tdi.co.jp/miso/amazon-cognito-sign-up)
- [Amazon Cognito User Pool / Identity Poolで事始め](http://blog.serverworks.co.jp/tech/2018/08/16/amazoncognitosample/)
- [3連休にCognito vue/aws-amplifyを使って手軽に認証機能をハンズオン](https://blog.asial.co.jp/2018/09/21/3%E9%80%A3%E4%BC%91%E3%81%ABCognito_vue/aws-amplify%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E6%89%8B%E8%BB%BD%E3%81%AB%E8%AA%8D%E8%A8%BC%E6%A9%9F%E8%83%BD%E3%82%92%E3%83%8F%E3%83%B3%E3%82%BA%E3%82%AA%E3%83%B3https://blog.asial.co.jp/2018/09/21/3%E9%80%A3%E4%BC%91%E3%81%ABCognito_vue/aws-amplify%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E6%89%8B%E8%BB%BD%E3%81%AB%E8%AA%8D%E8%A8%BC%E6%A9%9F%E8%83%BD%E3%82%92%E3%83%8F%E3%83%B3%E3%82%BA%E3%82%AA%E3%83%B3)
- [Cognito+AWS Amplifyの最小構成](https://qiita.com/k_hoso/items/3be67f27edd8bab460dd)
- [はじめてのCognito! CognitoのログインUIを使って認証システムを実装したまとめ](https://kotororo.net/2018/05/%E3%81%AF%E3%81%98%E3%82%81%E3%81%A6%E3%81%AEcognito-cognito%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3ui%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%AA%8D%E8%A8%BC%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F%E3%81%BE%E3%81%A8%E3%82%81/)

ポイントとしては、

1. ユーザープールの作成
    - 「クライアントシークレットを生成」のチェックを**外す**
1. IDプールの作成
    - 認証プロバイダを「Cognito」とし、上でつくったユーザープールID・アプリクライアントIDをセット

となります。
今回はログインIDをメールアドレスとする設定にし、ユーザー自身が登録（サインアップ）できるようにしました。
また、MFA認証(二段階認証)はOFFにしています。

コンソールのユーザープールからユーザー追加ができたりします。
試しに一件仮パスワード発行状態でユーザー作成しました。
すると仮パスワードの通知メールが届きます。

## サンプルアプリ作成
#### 準備
さて、問題はここから。
以前は複数のSDKを入れなければならないとかあったようですが、現状ではAmplifyがベストプラクティスなのではないか？と。
[Amplify Framework](https://aws-amplify.github.io/docs/)
とはいえ、ReactやVueでさくっと試せるスキルがないので、JavaScriptでのサンプルを試してみようと思ったところ…。
どうもAmplifyは上でコンソールでやったようなユーザープールの作成からCLIベースで対話式でするものらしい。

あれ…？ コンソールからつくっちゃたよ…。どうする？

色々と調べてみると、やはり設定する方法はある模様。
[Amplify JavaScript - Authentication#Manual Setup](https://aws-amplify.github.io/docs/js/authentication#manual-setup)
よし、とりあえずAmplifyをインストールじゃ。

たまたまローカルのWindows10環境に[nodist](https://github.com/nullivex/nodist)をよくわからないまま入れていたので、`npm`は使えそう。
【参考】
[nodistでNode.jsをバージョン管理](https://qiita.com/satoyan419/items/56e0b5f35912b9374305)

適当なフォルダを掘って、[Getting Started](https://aws-amplify.github.io/docs/js/start?ref=amplify-js-btn&platform=purejs)にしたがって`package.json`をコピペして`npm install`！

おお、node_modulesの下になんかたくさん落ちてきたぞ～。

そして手順通りに`webpack.config.js`もコピペして`npm start`！

おお、いろいろコンパイルされてローカルサーバーが立ち上がった～。
`localhost:8080`でサンプルページにアクセスできます。

え…XAMPPとか、VirtualBoxとVagrantとか要らんの…？
npm（Node.js？）すごくね…？　←そこかよ

#### サンプル実装
さて、無事環境が整ったので、ここから本題、Cognitoで遊んでみます。

上記[Getting Started](https://aws-amplify.github.io/docs/js/start?ref=amplify-js-btn&platform=purejs)は以降、`amplify`コマンドを使った環境構築に進みそうなので、一旦離脱。
[Amplify JavaScript - Authentication](https://aws-amplify.github.io/docs/js/authentication)を参考にそれぞれの処理を実装していきます。

※このあと、webpackでビルドしたfunctionの扱いにハマったりしたのですが、脇道なのでさらっと…
【参考】
[webpackでビルドしたbundle.jsの関数をonclickで動かしたかった話。](https://qiita.com/JKL_Presto/items/4ee716212e4121681526)

##### ①config
[Manual Setup](https://aws-amplify.github.io/docs/js/authentication#manual-setup)にしたがって記述します。
必要最小限？のIDのみ。
`import`のところだけサンプルの書き方だとうまく行かなかったので書き換えました。

```js:app.js
import Amplify from '@aws-amplify/core';
import Auth from '@aws-amplify/auth';

Amplify.configure({
    Auth: {
        // REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID
        identityPoolId: 'ap-northeast-1:xxxxxxxx...',

        // REQUIRED - Amazon Cognito Region
        region: 'ap-northeast-1', //東京リージョン

        // OPTIONAL - Amazon Cognito User Pool ID
        userPoolId: 'ap-northeast-1_xxxx...',

        // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
        userPoolWebClientId: 'xxxxxxxx...',
    }
});

const currentConfig = Auth.configure();
```

##### ②ログイン（サインイン）
先にコンソールでテストユーザーを登録していたので、ログインできるか試してみましょう。
[Sign In](https://aws-amplify.github.io/docs/js/authentication#sign-in)にしたがって、画面(`index.html`)やプロンプトからユーザー入力値を受け取れるようにしてみます。

```js:app.js
let SignIn = async function () {
    try {
        const username = document.getElementById("userid").value;
        const password = document.getElementById("passwd").value;
        const user = await Auth.signIn(username, password);
        console.log(user);

        if (user.challengeName === 'SMS_MFA' ||
            user.challengeName === 'SOFTWARE_TOKEN_MFA') {

        } else if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
            const {requiredAttributes} = user.challengeParam; // the array of required attributes, e.g ['email', 'phone_number']
            // You need to get the new password and required attributes from the UI inputs
            // and then trigger the following function with a button click
            // For example, the email and phone_number are required attributes
            const newPassword = prompt("新しいパスワードを入力してください。");
            if (!newPassword) {
                alert("新しいパスワードは必須です。");
                return;
            }
            const loggedUser = await Auth.completeNewPassword(
                user,              // the Cognito User Object
                newPassword,       // the new password
                // OPTIONAL, the required attributes
                {
                    // email: '',
                    // phone_number: '',
                }
            );
        } else if (user.challengeName === 'MFA_SETUP') {

        }

        document.getElementById("AuthResult").innerHTML = user.attributes.name + ' としてログイン中';
        document.getElementById("SignIn").style.display = "none";
        document.getElementById("SignOut").style.display = "inline-block";
    } catch (err) {
        console.log(err);
        document.getElementById("AuthResult").innerHTML = 'ログイン失敗：' + err.code;
    }
}
window.SignIn = SignIn;
```

ログインボタンから`SignIn`をコールすると…
おお、ログインできましたね。そして仮パスワードだったため、新しいパスワードが無事求められ、新しいパスワードが登録できました。
存在しないユーザーやパスワード間違いでそれぞれExceptionが返ってきていますし[^1]、新しいパスワードで無事ログインできました。

[^1]: セキュリティ的には、ユーザーが存在するかどうかを返すのは微妙ですけどね…。[Cognitoを使うとログインID（メールアドレス等）が存在するかどうか判別可能になる](http://knqyf263.hatenablog.com/entry/2017/12/14/220150)

##### ③ログアウト（サインアウト）
次にログアウトしてみます。
[Sign Out](https://aws-amplify.github.io/docs/js/authentication#sign-out)のサンプルほぼそのままです。

```js:app.js
let SignOut = function () {
    Auth.signOut()
        .then(function(data) {
            console.log(data);

            document.getElementById("AuthResult").innerHTML = 'ログアウトしました';
            document.getElementById("SignOut").style.display = "none";
            document.getElementById("SignIn").style.display = "inline-block";
        })
        .catch(err => console.log(err));
}
window.SignOut = SignOut;
```

ログインした状態でログアウトボタンから`SignOut`をコールすると、無事ログアウトされました。

##### ④ユーザー登録（サインアップ）
では、画面からユーザー登録してみましょう。
[Sign Up](https://aws-amplify.github.io/docs/js/authentication#sign-up)にしたがい、登録→認証コード入力という流れを簡易的に実装します。

```js:app.js
let SignUp = function () {
    const username = document.getElementById("userid").value;
    const password = document.getElementById("passwd").value;
    const name = document.getElementById("username").value;

    Auth.signUp({
        username,
        password,
        attributes: {
            name: name,
            // email,          // optional
            // phone_number,   // optional - E.164 number convention
            // other custom attributes
        },
        // validationData: []  //optional
        })
        .then(function(data) {
            console.log(data);
            document.getElementById("AuthResult").innerHTML = 'アカウント仮登録：' + data.user.username;

            confirmSignUp(data.user.username);
        })
        .catch(function(err) {
            console.log(err);
            document.getElementById("AuthResult").innerHTML = '登録失敗：' + err.code;
        });
}
window.SignUp = SignUp;

function confirmSignUp(username) {
    // After retrieving the confirmation code from the user
    const code = prompt("認証コードを入力してください。");
    Auth.confirmSignUp(username, code, {
        // Optional. Force user confirmation irrespective of existing alias. By default set to True.
        // forceAliasCreation: true
        })
        .then(function(data) {
            console.log(data);
            document.getElementById("AuthResult").innerHTML = 'アカウント登録完了';
        })
        .catch(err => console.log(err));
}
```

ユーザー登録ボタンから`SignUp`をコールすると、入力したアドレスに認証コードが届き、それをプロンプトに入力するとユーザー登録が完了しました。
AWSコンソールのユーザープールを見てみると、ユーザーが増えたことが確認できます。
そして新しいユーザーでもログインが問題なくできます。

##### ⑤ログイン中かどうか
あとは今ログインしているかどうかです。
[Retrieve Current Authenticated User](https://aws-amplify.github.io/docs/js/authentication#retrieve-current-authenticated-user)のとおりにやってみます。

```js:app.js
let isLogIn = function () {
    Auth.currentAuthenticatedUser({
        // bypassCache: false  // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
    }).then(function(user) {
        console.log(user);

        document.getElementById("AuthResult").innerHTML = 'あなたは' + user.attributes.name;
        document.getElementById("SignIn").style.display = "none";
        document.getElementById("SignOut").style.display = "inline-block";
    })
    .catch(err => console.log(err));
}
window.isLogIn = isLogIn;
```

画面読み込み時に`isLogIn`をコールするようにすると、ログイン中であれば無事ログイン中である旨が表示されました。
あるユーザーでログインしたまま、別のユーザーでログインしましたが、返ってくるのは後者のユーザーのようです。

## 実験後
なかなかおもしろかったです。
アプリつくる度に認証実装するのもバカバカしいですし、そこだけ切り出してマネージドサービスでやってくれるのは嬉しいですね。

あとは実際やるとしたら、config情報をクライアント側で見せないようにするにはどうするの？あたりが気になるところですね。