Amazon Cognitoを使った認証付きファイルアップロード機能の実装をやってみた

Amazon Cognitoを使った認証付きファイルアップロード機能の実装をやってみた

2025.08.01

はじめに

かつまたです。

今回は、ユーザープール、IDプールの構築学習を兼ねて、Amazon Cognitoを使って認証機能付きのファイルアップロードシステムを構築してみました。ユーザー登録・ログイン機能から、S3への安全なファイルアップロードまで、一通りの機能を実装しています。

やってみた

前提条件

  • アップロード先S3バケット作成済み
    S3バケットに直接ファイルアップロードを可能にするため、CORS設定を以下のように設定しました。
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "DELETE",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]

ユーザープール作成

  1. ユーザー認証を管理するユーザープールを作成します。Cognitoコンソールから「ユーザープールを作成」を選択します。

  2. ユーザープールを以下のように設定し、作成します。
    アプリクライアントタイプ:シングルページアプリケーション(SPA)
    サインイン識別子のオプション:メールアドレス、ユーザー名
    必須属性:email, name
    コールバックURL:http://localhost:8000

スクリーンショット 2025-07-21 10.53.06.png

IDプール作成

  1. Cognitoコンソールから「新しいIDプールの作成」を選択する。

  2. 「認証プロバイダーの設定」の「ユーザーアクセス」では「認証されたアクセス」と「ゲストアクセス」どちらとも選択しました。「認証されたIDソース」は「Amazon Cognitoユーザープール」を選択しました。

スクリーンショット 2025-07-21 10.55.49.png

  1. 「認証されたアクセス」と「ゲストアクセス」どちらとも新しいIAMロールを作成しました。「認証されたアクセス」のIAMロールは後ほど編集します。
    認証されたロール:Cognito_MyFileUploadAuth_Role
    認証されていないロール:Cognito_MyFileUploadUnauth_Role

スクリーンショット 2025-07-21 10.56.06.png

  1. IDプロバイダーを接続でユーザープールとの接続設定を設定します。先ほど作成したユーザープールと、ユーザープールに紐づけられたアプリクライアントIDを選択します。

スクリーンショット 2025-07-18 18.24.36.png

  1. 「認証されたアクセス」のIAMロールにS3への操作権限を付与するために、以下のポリシーをアタッチします。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::your-bucket-name"
        }
    ]
}

HTMLファイル作成と動作確認

  1. S3にファイルをアップロードできる認証付きアプリを作成します。以下が今回使用したHTMLファイルになります。AIにかなり助けてもらいながら作成しました。userPoolId、clientId、identityPoolId、bucketNameを自身の環境の値に置き換えて利用します。
HTML例
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cognito ファイルアップロードアプリ</title>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1400.0.min.js"></script>
    <script src="https://unpkg.com/amazon-cognito-identity-js@6.3.7/dist/amazon-cognito-identity.min.js"></script>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .container { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
        .hidden { display: none; }
        button { padding: 10px 20px; margin: 5px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 3px; }
        button:hover { background-color: #0056b3; }
        input { padding: 8px; margin: 5px; width: 200px; border: 1px solid #ddd; border-radius: 3px; }
        .file-list { margin-top: 20px; }
        .file-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
        .debug { background-color: #f8f9fa; padding: 10px; margin: 10px 0; border-radius: 5px; font-family: monospace; max-height: 150px; overflow-y: auto; border: 1px solid #dee2e6; }
        .error { color: #dc3545; }
        .success { color: #28a745; }
        .warning { color: #ffc107; }
        .status { margin: 10px 0; padding: 10px; border-radius: 3px; }
        .status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
    </style>
</head>
<body>
    <h1>🚀 Cognito ファイルアップロードアプリ</h1>

    <!-- デバッグ情報(折りたたみ可能) -->
    <div class="container">
        <h3 onclick="toggleDebug()" style="cursor: pointer;">🔧 デバッグ情報 (クリックで表示/非表示)</h3>
        <div id="debugInfo" class="debug hidden">
            <div id="debugLog"></div>
        </div>
    </div>

    <!-- ログイン前の画面 -->
    <div id="loginSection" class="container">
        <h2>ログイン</h2>
        <div>
            <input type="text" id="username" placeholder="ユーザー名"><br>
            <input type="password" id="password" placeholder="パスワード"><br>
            <button onclick="login()">ログイン</button>
            <button onclick="showSignup()">新規登録</button>
        </div>
        <div id="loginStatus"></div>
    </div>

    <!-- 新規登録画面 -->
    <div id="signupSection" class="container hidden">
        <h2>新規登録</h2>
        <div>
            <input type="text" id="signupUsername" placeholder="ユーザー名"><br>
            <input type="email" id="signupEmail" placeholder="メールアドレス"><br>
            <input type="password" id="signupPassword" placeholder="パスワード"><br>
            <input type="text" id="signupName" placeholder="名前"><br>
            <button onclick="signup()">登録</button>
            <button onclick="showLogin()">ログインに戻る</button>
        </div>
        <div id="signupStatus"></div>
    </div>

    <!-- 確認コード入力画面 -->
    <div id="confirmSection" class="container hidden">
        <h2>確認コード入力</h2>
        <p>メールに送信された確認コードを入力してください</p>
        <div>
            <input type="text" id="confirmCode" placeholder="確認コード"><br>
            <button onclick="confirmSignup()">確認</button>
        </div>
        <div id="confirmStatus"></div>
    </div>

    <!-- ログイン後の画面 -->
    <div id="mainSection" class="container hidden">
        <h2>ファイルアップロード</h2>
        <div id="userInfo"></div>

        <!-- AWS接続状態表示 -->
        <div id="awsStatus" class="status"></div>

        <!-- ファイルアップロード -->
        <div style="margin: 20px 0; padding: 15px; border: 2px dashed #007bff; border-radius: 5px;">
            <h3>📁 ファイルをアップロード</h3>
            <input type="file" id="fileInput" multiple accept="*/*">
            <br><br>
            <button onclick="uploadFiles()">📤 アップロード</button>
            <button onclick="listFiles()">🔄 ファイル一覧更新</button>
            <button onclick="logout()">🚪 ログアウト</button>
        </div>

        <div id="uploadStatus"></div>

        <!-- ファイル一覧 -->
        <div class="file-list">
            <h3>📋 アップロード済みファイル</h3>
            <div id="fileList"></div>
        </div>
    </div>

    <script>
        // 設定値
        const CONFIG = {
            userPoolId: 'ap-northeast-1_XXXXXXXXX',
            clientId: 'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            identityPoolId: 'ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
            bucketName: 'your-bucket-name-here',
            region: 'ap-northeast-1'
        };

        // デバッグ用ログ関数
        function debugLog(message, type = 'info') {
            const debugDiv = document.getElementById('debugLog');
            const timestamp = new Date().toLocaleTimeString();
            const className = type === 'error' ? 'error' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : '';
            debugDiv.innerHTML += `<div class="${className}">[${timestamp}] ${message}</div>`;
            debugDiv.scrollTop = debugDiv.scrollHeight;
            console.log(`[${timestamp}] ${message}`);
        }

        function toggleDebug() {
            const debugInfo = document.getElementById('debugInfo');
            debugInfo.classList.toggle('hidden');
        }

        // 初期化
        debugLog('アプリケーション開始');

        const poolData = {
            UserPoolId: CONFIG.userPoolId,
            ClientId: CONFIG.clientId
        };
        const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

        let currentUser = null;
        let s3 = null;

        // 画面表示制御
        function showLogin() {
            document.getElementById('loginSection').classList.remove('hidden');
            document.getElementById('signupSection').classList.add('hidden');
            document.getElementById('confirmSection').classList.add('hidden');
            document.getElementById('mainSection').classList.add('hidden');
        }

        function showSignup() {
            document.getElementById('loginSection').classList.add('hidden');
            document.getElementById('signupSection').classList.remove('hidden');
        }

        function showMain() {
            document.getElementById('loginSection').classList.add('hidden');
            document.getElementById('signupSection').classList.add('hidden');
            document.getElementById('confirmSection').classList.add('hidden');
            document.getElementById('mainSection').classList.remove('hidden');
        }

        // 新規登録
        function signup() {
            const username = document.getElementById('signupUsername').value;
            const email = document.getElementById('signupEmail').value;
            const password = document.getElementById('signupPassword').value;
            const name = document.getElementById('signupName').value;
            const statusDiv = document.getElementById('signupStatus');

            if (!username || !email || !password || !name) {
                statusDiv.innerHTML = '<div class="status error">すべての項目を入力してください</div>';
                return;
            }

            const attributeList = [
                new AmazonCognitoIdentity.CognitoUserAttribute({
                    Name: 'email',
                    Value: email
                }),
                new AmazonCognitoIdentity.CognitoUserAttribute({
                    Name: 'name',
                    Value: name
                })
            ];

            statusDiv.innerHTML = '<div class="status">登録中...</div>';

            userPool.signUp(username, password, attributeList, null, function(err, result) {
                if (err) {
                    statusDiv.innerHTML = `<div class="status error">登録エラー: ${err.message}</div>`;
                    debugLog('登録エラー: ' + err.message, 'error');
                    return;
                }
                currentUser = result.user;
                statusDiv.innerHTML = '<div class="status success">確認コードをメールで送信しました</div>';
                debugLog('新規登録成功', 'success');
                document.getElementById('signupSection').classList.add('hidden');
                document.getElementById('confirmSection').classList.remove('hidden');
            });
        }

        // 確認コード入力
        function confirmSignup() {
            const confirmCode = document.getElementById('confirmCode').value;
            const statusDiv = document.getElementById('confirmStatus');

            if (!confirmCode) {
                statusDiv.innerHTML = '<div class="status error">確認コードを入力してください</div>';
                return;
            }

            statusDiv.innerHTML = '<div class="status">確認中...</div>';

            currentUser.confirmRegistration(confirmCode, true, function(err, result) {
                if (err) {
                    statusDiv.innerHTML = `<div class="status error">確認エラー: ${err.message}</div>`;
                    debugLog('確認エラー: ' + err.message, 'error');
                    return;
                }
                statusDiv.innerHTML = '<div class="status success">登録完了!ログインしてください</div>';
                debugLog('アカウント確認成功', 'success');
                setTimeout(showLogin, 2000);
            });
        }

        // ログイン
        function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const statusDiv = document.getElementById('loginStatus');

            if (!username || !password) {
                statusDiv.innerHTML = '<div class="status error">ユーザー名とパスワードを入力してください</div>';
                return;
            }

            statusDiv.innerHTML = '<div class="status">ログイン中...</div>';
            debugLog(`ログイン試行: ${username}`);

            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) {
                    statusDiv.innerHTML = '<div class="status success">ログイン成功!</div>';
                    debugLog('ログイン成功', 'success');
                    currentUser = cognitoUser;

                    showMain();

                    // ユーザー情報表示
                    cognitoUser.getUserAttributes(function(err, attributes) {
                        if (!err) {
                            const name = attributes.find(attr => attr.Name === 'name')?.Value || username;
                            document.getElementById('userInfo').innerHTML = `<h3>👋 ようこそ、${name}さん!</h3>`;
                        }
                    });

                    // AWS初期化
                    initializeAWS(result);
                },
                onFailure: function(err) {
                    statusDiv.innerHTML = `<div class="status error">ログインエラー: ${err.message}</div>`;
                    debugLog('ログインエラー: ' + err.message, 'error');
                }
            });
        }

        // AWS SDK初期化
        function initializeAWS(result) {
            debugLog('AWS初期化開始');
            const awsStatusDiv = document.getElementById('awsStatus');
            awsStatusDiv.innerHTML = '<div class="status">AWS接続中...</div>';

            try {
                AWS.config.region = CONFIG.region;
                AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                    IdentityPoolId: CONFIG.identityPoolId,
                    Logins: {
                        [`cognito-idp.${CONFIG.region}.amazonaws.com/${CONFIG.userPoolId}`]: result.getIdToken().getJwtToken()
                    }
                });

                AWS.config.credentials.refresh(function(err) {
                    if (err) {
                        awsStatusDiv.innerHTML = `<div class="status error">❌ AWS接続エラー: ${err.message}</div>`;
                        debugLog('AWS認証エラー: ' + err.message, 'error');
                    } else {
                        awsStatusDiv.innerHTML = '<div class="status success">✅ AWS接続成功 - S3利用可能</div>';
                        debugLog('AWS認証成功', 'success');
                        s3 = new AWS.S3();
                        listFiles();
                    }
                });
            } catch (error) {
                awsStatusDiv.innerHTML = `<div class="status error">❌ AWS初期化エラー: ${error.message}</div>`;
                debugLog('AWS初期化例外: ' + error.message, 'error');
            }
        }

        // ファイルアップロード
        function uploadFiles() {
            if (!s3) {
                alert('❌ S3が初期化されていません。AWS接続を確認してください。');
                return;
            }

            const fileInput = document.getElementById('fileInput');
            const files = fileInput.files;
            const statusDiv = document.getElementById('uploadStatus');

            if (files.length === 0) {
                alert('📁 ファイルを選択してください');
                return;
            }

            debugLog(`ファイルアップロード開始: ${files.length}個のファイル`);
            statusDiv.innerHTML = `<div class="status">📤 ${files.length}個のファイルをアップロード中...</div>`;

            let uploadCount = 0;
            let successCount = 0;
            const totalFiles = files.length;

            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                                const key = `uploads/${currentUser.username}/${Date.now()}_${file.name}`;

                const params = {
                    Bucket: CONFIG.bucketName,
                    Key: key,
                    Body: file,
                    ContentType: file.type
                };

                debugLog(`アップロード開始: ${file.name}`);

                s3.upload(params, function(err, data) {
                    uploadCount++;

                    if (err) {
                        debugLog(`アップロードエラー: ${file.name} - ${err.message}`, 'error');
                    } else {
                        successCount++;
                        debugLog(`アップロード成功: ${file.name}`, 'success');
                    }

                    // 全ファイルの処理完了チェック
                    if (uploadCount === totalFiles) {
                        if (successCount === totalFiles) {
                            statusDiv.innerHTML = `<div class="status success">✅ ${totalFiles}個のファイルのアップロードが完了しました!</div>`;
                        } else {
                            statusDiv.innerHTML = `<div class="status error">⚠️ ${successCount}/${totalFiles}個のファイルがアップロードされました</div>`;
                        }
                        fileInput.value = '';
                        setTimeout(() => {
                            listFiles();
                        }, 1000);
                    }
                });
            }
        }

        // ファイル一覧取得
        function listFiles() {
            if (!s3 || !currentUser) {
                document.getElementById('fileList').innerHTML = '<div class="status error">S3が初期化されていません</div>';
                return;
            }

            debugLog('ファイル一覧取得開始');

            const params = {
                Bucket: CONFIG.bucketName,
                Prefix: `uploads/${currentUser.username}/`
            };

            s3.listObjects(params, function(err, data) {
                const fileListDiv = document.getElementById('fileList');

                if (err) {
                    fileListDiv.innerHTML = `<div class="status error">ファイル一覧の取得に失敗: ${err.message}</div>`;
                    debugLog('ファイル一覧取得エラー: ' + err.message, 'error');
                    return;
                }

                debugLog(`ファイル一覧取得成功: ${data.Contents.length}`, 'success');

                if (data.Contents.length === 0) {
                    fileListDiv.innerHTML = '<div class="status">📂 アップロードされたファイルはありません</div>';
                    return;
                }

                let html = '';
                data.Contents.forEach(function(file) {
                    const fileName = file.Key.split('/').pop();
                    const fileSize = (file.Size / 1024).toFixed(2);
                    const uploadDate = file.LastModified.toLocaleDateString();

                    html += `
                        <div class="file-item">
                            <div>
                                <strong>📄 ${fileName}</strong><br>
                                <small>サイズ: ${fileSize} KB | アップロード日: ${uploadDate}</small>
                            </div>
                            <div>
                                <button onclick="downloadFile('${file.Key}', '${fileName}')" style="background-color: #28a745;">⬇️ ダウンロード</button>
                                <button onclick="deleteFile('${file.Key}', '${fileName}')" style="background-color: #dc3545;">🗑️ 削除</button>
                            </div>
                        </div>
                    `;
                });
                fileListDiv.innerHTML = html;
            });
        }

        // ファイルダウンロード
        function downloadFile(key, fileName) {
            if (!s3) return;

            debugLog(`ダウンロード開始: ${fileName}`);

            const params = {
                Bucket: CONFIG.bucketName,
                Key: key,
                Expires: 300 // 5分間有効なURL
            };

            try {
                const url = s3.getSignedUrl('getObject', params);
                const link = document.createElement('a');
                link.href = url;
                link.download = fileName;
                link.target = '_blank';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                debugLog(`ダウンロード成功: ${fileName}`, 'success');
            } catch (error) {
                debugLog(`ダウンロードエラー: ${fileName} - ${error.message}`, 'error');
                alert(`ダウンロードエラー: ${error.message}`);
            }
        }

        // ファイル削除
        function deleteFile(key, fileName) {
            if (!s3) return;

            if (!confirm(`🗑️ "${fileName}" を削除しますか?\n\nこの操作は取り消せません。`)) return;

            debugLog(`削除開始: ${fileName}`);

            const params = {
                Bucket: CONFIG.bucketName,
                Key: key
            };

            s3.deleteObject(params, function(err, data) {
                if (err) {
                    debugLog(`削除エラー: ${fileName} - ${err.message}`, 'error');
                    alert(`削除エラー: ${err.message}`);
                } else {
                    debugLog(`削除成功: ${fileName}`, 'success');
                    alert(`✅ "${fileName}" を削除しました`);
                    listFiles(); // ファイル一覧を更新
                }
            });
        }

        // ログアウト
        function logout() {
            if (currentUser) {
                currentUser.signOut();
                currentUser = null;
                s3 = null;
                debugLog('ログアウト完了');
            }

            // 画面をクリア
            document.getElementById('userInfo').innerHTML = '';
            document.getElementById('uploadStatus').innerHTML = '';
            document.getElementById('fileList').innerHTML = '';
            document.getElementById('awsStatus').innerHTML = '';

            showLogin();
        }

        // 初期化完了
        debugLog('アプリケーション初期化完了', 'success');
    </script>
</body>
</html>
  1. 作成したHTMLファイルをブラウザで開き、動作確認をします。
    スクリーンショット 2025-07-21 12.02.51.png

スクリーンショット 2025-07-21 12.02.56.png

  1. ログイン後、任意のファイルをアップロードし、アップロード完了画面やS3バケット内にオブジェクトが存在していることを確認できました。
    スクリーンショット 2025-07-18 18.48.46.png

おわりに

ご覧いただきありがとうございました。
今回は、Amazon Cognitoを活用した認証機能付きファイルアップロードシステムの構築について説明しました。
コンソール側でのユーザープール、IDプールの構築やアプリケーション側でのユーザー登録・確認コード検証・ログインの一連のフロー実装によってCognitoの使われ方について学ぶことができたと感じます。

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.