Amazon Cognitoを使ったユーザー管理：メリットと注意点

# はじめに

ユーザー情報管理のAWSのランニングコストを下げるために、Amazon Cognitoについて調査した内容を記載します。

# Cognitoとは

Cognitoは、ユーザー情報を管理するためのAWSサービスです。RDSなどでユーザー情報を管理するよりも遥かにランニングコストが低いですが、
ユーザー情報の一括取得が60件までしか取得できないことが解りました。61件以上の取得を試みるとエラーが発生します（一括取得には適切なロール権限が必要です）。そのため、ユーザー一覧画面の作成には不向きなサービスと言えます。

ユーザーの最大登録数を60件までに制限するなどのルールを設ければ一覧画面は作成できます。しかし、ユーザー情報を変更する際には、変更対象のユーザーの「パスワード」と「ユーザー名」が必要だったり、パスワードの再発行には、登録されている「Eメール」または「電話番号」を使用して「確認コード」を発行し、その確認コードを用いてパスワードの変更処理を行う必要があります。このため、Cognitoは管理者が自由にユーザー情報を変更することができず、柔軟性に欠ける部分があります。
しかし、Cognitoはモバイルアプリやウェブアプリのユーザー管理には非常に向いています。これにより、ユーザー登録、ログイン、パスワードリセット、多要素認証（MFA）などの機能を簡単に実装することができます。


# Cognito機能のまとめ

 - 60件までのユーザー情報なら一括で取得できます。ただし、適切なロール権限が必要です。

 - パスワードを忘れてしまった場合、再発行が可能です。ただし、「Eメール」または「電話番号」による確認コードの発行が必要です。パスワード再発行時には、確認コード、ユーザー名、新しいパスワードの入力が必要です。

 - ユーザー名は変更できませんが、ユーザー名以外の設定項目は変更可能です。ただし、変更時には「ユーザー名」と「パスワード」が必要になります。

 - ユーザー情報を新規作成する際には、「Eメール」または「電話番号」を使用して「確認コード」を発行し、ステータスを「未確認」から「確認済み」に変更する必要があります。AWSマネジメントコンソールのCognito画面からもステータスの変更が可能です。

 - AWSマネジメントコンソールのCognito画面からユーザー情報を新規追加した場合、管理者が追加したユーザーと判断されるため、初回ログイン時にパスワード変更処理が必要になります（初回のみ変更が必要です）。

# Cognitoの作成手順

AWSマネジメントコンソールのCognito画面にて「ユーザープール」を作成して「ユーザープールID」と「クライアントID」を発行します。
IDプールは、ユーザープールと連携させて、ユーザープールに登録されているユーザーに対してAWSサービスへのアクセス権限を設定するためのものです。（アクセス権限を設定する場合は、IDプールも作成します）

下記のサンプルコードに作成した「ユーザープールID」と「クライアントID」を記載すると、画面からログイン処理を実施できます。

## ログイン処理のサンプル

ログイン画面のサンプルコードです。

```html:index.html
<!DOCTYPE html>
<html llang= "ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン</title>
    <script src="https://cdn.jsdelivr.net/npm/amazon-cognito-identity-js@latest/dist/amazon-cognito-identity.min.js"></script>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.814.0.min.js"></script>
    <script>

        const poolData = {
            UserPoolId: 'ap-****-*_*********', // ここを作成したユーザープールIDに置き換える
            ClientId: '8p*******************', // ここを作成したクライアントIDに置き換える
        };

        const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

        // ログイン処理
        function logIn(event) {
            event.preventDefault();
            const username = document.getElementById("username").value;
            const password = document.getElementById("password").value;
            let text1 = document.getElementById('text1');

            const authenticationData = {
                Username: username,
                Password: password
            };
            const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);

            const userData = {
                Username: username,
                Pool: userPool
            };
            const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

            cognitoUser.authenticateUser(authenticationDetails, {
                onSuccess: function(result) {
                    text1.innerHTML = 'ログイン成功 : ' + cognitoUser.getUsername() + '<br>';
                },
                onFailure: function(err) {
                    if (err.code === 'UserNotConfirmedException') {
                        text1.innerHTML = 'ユーザー認証されていません。<br>';
                        resendConfirmationCode(username, text1);
                    }else{
                        text1.innerHTML = 'ログイン失敗しました : ' + (err.message || JSON.stringify(err)) + '<br>';
                    }
                },
                newPasswordRequired: function(userAttributes, requiredAttributes) {
                    const newPassword = prompt('新しいパスワードを入力してください');
                    cognitoUser.completeNewPasswordChallenge(newPassword, {}, {
                        onSuccess: function(result) {
                            text1.innerHTML = 'パスワード変更しました : ' + cognitoUser.getUsername() + '<br>';
                        },
                        onFailure: function(err) {
                            text1.innerHTML = 'ログインエラー : ' + (err.message || JSON.stringify(err)) + '<br>';
                        }
                    });
                }
            });
        }

        // 認証メール送信処理
        function verify(event) {
            // デフォルトのフォーム送信を無効にする
            event.preventDefault();
            var username = document.getElementById("new_username").value;
            var verificationCode = document.getElementById("verification-code").value;
            let text1 = document.getElementById('text1');

            var userData = {
                Username: username,
                Pool: userPool
            };
            var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
            cognitoUser.confirmRegistration(verificationCode, true, function(err, result) {
                if (err) {
                    text1.innerHTML = '認証に失敗しました : ' + (err.message || JSON.stringify(err)) + '<br>';
                } else {
                    text1.innerHTML = '認証に完了しました : ' + result + '<br>';
                }
            });
        }

        // 再認証メール送信処理
        function resendConfirmationCode(username, text1) {
            var userData = {
                Username: username,
                Pool: userPool
            };
            var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
            document.getElementById('verification').style.display = 'block';
            cognitoUser.resendConfirmationCode(function(err, result) {
                if (err) {
                    text1.innerHTML = '再認証に失敗しました : ' + (err.message || JSON.stringify(err)) + '<br>';
                    return;
                }else{
                    text1.innerHTML = '登録されているメールに確認コードを再送信しました.<br>';
                }
            });
        }
    </script>
    <style>
        .block {
            margin: 20px;
            padding: 20px 20px 40px 20px;
            width: 500px;
            background-color: #ffffff;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        .block1 {
            margin: 20px;
        }
        .block2 {
            padding: 10px;
            margin-top: 5px;
            border: 1px solid #212121;
            border-radius: 5px;
            width: 520px;
            background-color: #fffbf2;
        }
        h2 {
            text-align: center;
            margin-bottom: 20px;
        }

        input[type="text"],
        input[type="password"],
        input[type="email"]{
            width: 96%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        button[type="submit"] {
            width: 100%;
            margin-top: 10px;
            padding: 10px;
            background-color: #4CAF50;
            color: #ffffff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button[type="submit"]:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>

    <div class="block">
        <form id="login-form">
            <p class="fsize">ログイン画面</p>
            <label for="username">ユーザ名</label>
            <input type="text" name="username" id="username" placeholder="ユーザ名">
            <label for="password">パスワード</label>
            <input type="password" name="password" id="password" placeholder="パスワード">
            <button type="submit" onclick="logIn(event);">ログインする</button>
        </form>
    </div>
    
    <div class="block" id="verification" style="display:none;">
        <form id="verification-form">
            <p class="fsize">確認コードを入力してください</p>
            <label for="verification-code">確認コード</label>
            <input type="text" name="verification-code" id="verification-code" placeholder="確認コード" required>
            <button type="submit" onclick="verify(event);">確認する</button>
        </form>
    </div>

    <div class="block1">
        <p style="margin-top: 30px;">【ログ出力】</p>
        <div id="text1" class="block2">
        </div>
    </div>
</body>
</html>
```

## ユーザ新規作成処理のサンプル

ユーザ新規作成画面のサンプルコードです。

```html:index.html

〜〜　ログイン処理のサンプルのコードに追加してください　〜〜

<script>
// ログイン新規作成処理
function signUp(event)  {
    event.preventDefault();
    const username = document.getElementById("new_username").value;
    const password = document.getElementById("new_password").value;
    var email = document.getElementById("email").value;
    let text1 = document.getElementById('text1');

    var attributeList = [];
    var dataEmail = {
        Name: 'email',
        Value: email
    };
    var attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(dataEmail);
    attributeList.push(attributeEmail);

    try {
        userPool.signUp(username, password, attributeList, null, function(err, result) {
            if (err) {
                text1.innerHTML = 'ユーザ作成失敗 : ' + (err.message || JSON.stringify(err)) + '<br>';
            } else {
                var cognitoUser = result.user;
                text1.innerHTML = '新規作成しました : ' + cognitoUser.getUsername() + '<br>';
                text1.innerHTML += '確認コードをメールで送信しました。コードを入力してください。<br>';
                document.getElementById('verification').style.display = 'block';
            }
        });
    } catch (error) {
        text1.innerHTML = 'ユーザ作成失敗 : ' + error.message + '<br>';
    }
}
</script>

<body>
    <div class="block">
        <form id="signup-form">
            <p class="fsize">ユーザ登録画面</p>
            <label for="new_username">ユーザ名</label>
            <input type="text" name="new_username" id="new_username" placeholder="ユーザ名">
            <label for="new_password">パスワード</label>
            <input type="password" name="new_password" id="new_password" placeholder="パスワード">
            <label for="email">認証を確認するメールアドレス</label>
            <input type="email" name="email" id="email" placeholder="メールアドレス">
            <button type="submit" onclick="signUp(event);">新規登録する</button>
        </form>
    </div>
</body>
```

## ユーザ新規作成処理のサンプル

ユーザ新規作成画面のサンプルコードです。

```html:index.html

〜〜　ログイン処理のサンプルのコードに追加してください　〜〜

<script>
async function changePassword(event) {
    event.preventDefault();
    const username = document.getElementById('change-username').value;
    const oldPassword = document.getElementById('old-password').value;
    const newPassword = document.getElementById('new-password').value;
    const text1 = document.getElementById('text1');
    const userData = {
        Username: username,
        Pool: userPool
    };
    const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
    const authenticationData = {
        Username: username,
        Password: oldPassword
    };
    const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
    cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: function(result) {
            cognitoUser.changePassword(oldPassword, newPassword, function(err, result) {
                if (err) {
                    text1.innerHTML = 'パスワード変更エラー : ' + (err.message || JSON.stringify(err)) + '<br>';
                } else {
                    text1.innerHTML = 'パスワードの変更に成功しました。<br>';
                }
            });
        },
        onFailure: function(err) {
             text1.innerHTML = 'パスワード変更エラー : ' + (err.message || JSON.stringify(err)) + '<br>';
        }
    });
}
</script>

<body>
    <div class="block">
        <form id="change-form" onsubmit="changePassword(event);">
            <p class="fsize">パスワード変更画面</p>
            <label for="change-username">ユーザー名:</label>
            <input type="text" id="change-username" name="change-username" required>
            <label for="old-password">現在のパスワード:</label>
            <input type="password" id="old-password" name="old-password" required>
            <label for="new-password">新しいパスワード:</label>
            <input type="password" id="new-password" name="new-password" required>
            <button type="submit" onclick="changePassword(event);">パスワード変更する</button>
        </form>
    </div>
</body>
```

# おわりに

読んで頂き、ありがとうございました。
個人的に調査した内容なので、少しでもお役に立てればと思います。
以上、よろしくお願いいたします。