📝

S3 の静的ウェブサイトと Cognito を連携してみた

に公開

S3 の静的ウェブサイトに Cognito で認証・認可の機能を実装してみました。

手順概要

  1. S3バケットの作成
  2. Cognito ユーザープールの作成
  3. Cognito ID プールの作成
  4. IAM ロールの設定
  5. HTML ファイルの作成
  6. 動作確認

1. S3バケットの作成

1-1. デフォルト設定で S3 バケットを作成します。

1-2. 静的ウェブサイトホスティングを有効化します。

1-3. パブリックアクセスのブロックを無効化します。

1-4. バケットポリシーを設定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-bucket-name/*"
        }
    ]
}

1-5. CORS を設定します。

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "DELETE",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ],
        "MaxAgeSeconds": 3000
    }
]

2. Cognito ユーザープールの作成

2-1. 以下の設定でユーザープールを作成します。

  • アプリケーションタイプ: シングルページアプリケーション
  • サインイン識別子のオプション: メールアドレス

3. Cognito ID プールの作成

3-1. 以下の設定で ID プールを作成します。

  • 認証されたアクセスとゲストアクセスを有効化します。
  • 認証された ID ソース: Amazon Cognito ユーザープール
  • 認証されたロールとゲストロール: 新しい IAM ロールを作成
    • ロール名は任意
  • ユーザープール ID: 手順 2 で作成したユーザープール
  • アプリクライアント ID: 手順 2 で作成されたアプリクライアント




4. IAM ロールの設定

4-1. 認証されたロールに AmazonS3FullAccess ポリシーをアタッチします。

5. HTML ファイルの作成

5-1. 以下の内容で index.html ファイルを作成します。
コード内の以下の値は作成済みのリソースの値に置換してください。

const USER_POOL_ID = 'User Pool ID'; // 手順 2 のユーザープール ID
const CLIENT_ID = 'App Client ID'; // 手順 2 のアプリクライアント ID
const IDENTITY_POOL_ID = 'Identity Pool ID'; // 手順 3 の ID プール ID
const REGION = 'ap-northeast-1'; // 使用するリージョン
const BUCKET_NAME = 'S3 bucket name'; // 手順 1 の S3 バケット名
index.html
index.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.1.12.min.js"></script>
    <script src="https://unpkg.com/amazon-cognito-identity-js/dist/amazon-cognito-identity.min.js"></script>
</head>
<body>
    <div id="app">
        <h1>Cognito認証テスト</h1>
        
        <!-- ログイン前の画面 -->
        <div id="loginSection">
            <h2>ログイン</h2>
            <div>
                <input type="text" id="username" placeholder="ユーザー名">
            </div>
            <div>
                <input type="password" id="password" placeholder="パスワード">
            </div>
            <button onclick="login()">ログイン</button>
            <button onclick="showSignup()">新規登録</button>
            
            <!-- 未認証テスト用ボタンを追加 -->
            <hr>
            <h3>未認証状態のテスト</h3>
            <p>※ログインせずにS3にアクセスしてアクセス拒否を確認</p>
            <button onclick="testUnauthenticatedAccess()" style="background-color: #ff6b6b; color: white;">
                未認証でS3アクセステスト
            </button>
        </div>

        <!-- 新規登録画面 -->
        <div id="signupSection" style="display:none;">
            <h2>新規登録</h2>
            <div>
                <input type="text" id="newUsername" placeholder="ユーザー名">
            </div>
            <div>
                <input type="password" id="newPassword" placeholder="パスワード">
            </div>
            <div>
                <input type="email" id="email" placeholder="メールアドレス">
            </div>
            <button onclick="signup()">登録</button>
            <button onclick="showLogin()">ログインに戻る</button>
        </div>

        <!-- ログイン後の画面 -->
        <div id="mainSection" style="display:none;">
            <h2>認証成功!S3バケットにアクセスできます</h2>
            <button onclick="logout()">ログアウト</button>
            <button onclick="loadBucketContents()">バケット内容を表示</button>
            <button onclick="uploadTestFile()">テストファイルをアップロード</button>
            
            <!-- 認証後も未認証テストができるボタンを追加 -->
            <hr>
            <button onclick="testUnauthenticatedAccess()" style="background-color: #ff6b6b; color: white;">
                未認証でS3アクセステスト(比較用)
            </button>
            
            <div id="bucketContents"></div>
        </div>

        <div id="message"></div>
        
        <!-- テスト結果表示エリアを追加 -->
        <div id="testResults" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc; background-color: #f9f9f9;">
            <h3>テスト結果</h3>
            <div id="testOutput"></div>
        </div>
    </div>

    <script>
        // ★ここに実際の値を設定してください★
        const USER_POOL_ID = 'ap-northeast-1_xxxxxxxxx'; // User Pool ID
        const CLIENT_ID = 'xxxxxxxxxxxxxxxxxxxx';        // App Client ID  
        const IDENTITY_POOL_ID = 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // Identity Pool ID
        const REGION = 'ap-northeast-1';
        const BUCKET_NAME = 'my-website-bucket-20250606'; // ★あなたのバケット名に変更★

        // Cognito User Pool設定
        const poolData = {
            UserPoolId: USER_POOL_ID,
            ClientId: CLIENT_ID
        };
        const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);

        // AWS SDK初期設定
        AWS.config.region = REGION;

        function showMessage(msg) {
            document.getElementById('message').innerHTML = `<p style="color: blue;">${msg}</p>`;
            console.log(msg);
        }

        function showError(msg) {
            document.getElementById('message').innerHTML = `<p style="color: red;">エラー: ${msg}</p>`;
            console.error(msg);
        }

        function addTestResult(msg, isError = false) {
            const testOutput = document.getElementById('testOutput');
            const timestamp = new Date().toLocaleTimeString('ja-JP');
            const color = isError ? 'red' : 'green';
            testOutput.innerHTML += `<p style="color: ${color};">[${timestamp}] ${msg}</p>`;
        }

        function showLogin() {
            document.getElementById('loginSection').style.display = 'block';
            document.getElementById('signupSection').style.display = 'none';
            document.getElementById('mainSection').style.display = 'none';
        }

        function showSignup() {
            document.getElementById('loginSection').style.display = 'none';
            document.getElementById('signupSection').style.display = 'block';
            document.getElementById('mainSection').style.display = 'none';
        }

        function showMain() {
            document.getElementById('loginSection').style.display = 'none';
            document.getElementById('signupSection').style.display = 'none';
            document.getElementById('mainSection').style.display = 'block';
        }

        // 未認証状態でのS3アクセステスト
        function testUnauthenticatedAccess() {
            addTestResult('=== 未認証状態でのS3アクセステスト開始 ===');
            
            // 一時的に認証情報をクリア
            const originalCredentials = AWS.config.credentials;
            
            // 未認証のIdentity Poolクレデンシャルを設定
            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                IdentityPoolId: IDENTITY_POOL_ID
                // Loginsを指定しない = 未認証状態
            });

            const s3 = new AWS.S3({
                region: REGION,
                apiVersion: '2006-03-01'
            });

            // S3バケットの内容を取得しようとする
            const params = {
                Bucket: BUCKET_NAME,
                MaxKeys: 5
            };
            
            addTestResult('未認証でS3バケットにアクセス中...');
            
            s3.listObjects(params, (err, data) => {
                if (err) {
                    addTestResult(`✅ 期待通りアクセス拒否されました: ${err.code} - ${err.message}`, true);
                    addTestResult('セキュリティが正常に動作しています!');
                } else {
                    addTestResult('⚠️ 未認証でもアクセスできてしまいました(セキュリティ設定を確認してください)');
                    addTestResult(`取得したオブジェクト数: ${data.Contents ? data.Contents.length : 0}`);
                }
                
                // 元の認証情報を復元
                AWS.config.credentials = originalCredentials;
                addTestResult('=== 未認証テスト完了 ===');
            });
        }

        // 新規登録
        function signup() {
            const username = document.getElementById('newUsername').value;
            const password = document.getElementById('newPassword').value;
            const email = document.getElementById('email').value;

            if (!username || !password || !email) {
                showError('すべての項目を入力してください');
                return;
            }

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

            userPool.signUp(username, password, attributeList, null, function(err, result) {
                if (err) {
                    showError(`登録エラー: ${err.message}`);
                    return;
                }
                showMessage('登録成功!メールで確認コードが送信されます。確認後、ログインしてください。');
                showLogin();
            });
        }

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

            if (!username || !password) {
                showError('ユーザー名とパスワードを入力してください');
                return;
            }

            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) {
                    showMessage('ログイン成功!');
                    addTestResult('✅ 認証成功: 正常にログインできました');
                    setupAWSCredentials(result);
                    showMain();
                    setTimeout(() => {
                        loadBucketContents();
                    }, 1000);
                },
                onFailure: function(err) {
                    showError(`ログインエラー: ${err.message}`);
                    addTestResult(`❌ 認証失敗: ${err.message}`, true);
                }
            });
        }

        // AWS認証情報設定
        function setupAWSCredentials(result) {
            const token = result.getIdToken().getJwtToken();
            
            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                IdentityPoolId: IDENTITY_POOL_ID,
                Logins: {
                    [`cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}`]: token
                }
            });

            AWS.config.credentials.refresh((error) => {
                if (error) {
                    showError('認証情報の更新に失敗しました: ' + error.message);
                    addTestResult(`❌ 認証情報更新失敗: ${error.message}`, true);
                } else {
                    showMessage('認証情報を更新しました');
                    addTestResult('✅ 認証情報更新成功: AWS APIへのアクセス権限を取得');
                }
            });
        }

        // バケット内容を取得
        function loadBucketContents() {
            showMessage('バケット内容を取得中...');
            
            if (!AWS.config.credentials) {
                showError('認証情報が設定されていません');
                return;
            }

            const s3 = new AWS.S3({
                region: REGION,
                apiVersion: '2006-03-01'
            });

            const params = {
                Bucket: BUCKET_NAME,
                MaxKeys: 10
            };
            
            s3.listObjects(params, (err, data) => {
                if (err) {
                    showError(`バケット内容取得エラー: ${err.message}`);
                    addTestResult(`❌ 認証済みユーザーのS3アクセス失敗: ${err.message}`, true);
                } else {
                    showMessage('バケット内容を取得しました');
                    addTestResult(`✅ 認証済みユーザーのS3アクセス成功: ${data.Contents ? data.Contents.length : 0}個のオブジェクト取得`);
                    
                    let html = `<h3>バケット「${BUCKET_NAME}」の内容:</h3>`;
                    
                    if (data.Contents && data.Contents.length > 0) {
                        html += '<ul>';
                        data.Contents.forEach(object => {
                            html += `<li>
                                <strong>${object.Key}</strong><br>
                                サイズ: ${object.Size} bytes<br>
                                最終更新: ${object.LastModified}
                            </li><br>`;
                        });
                        html += '</ul>';
                    } else {
                        html += '<p>バケットは空です</p>';
                    }
                    
                    document.getElementById('bucketContents').innerHTML = html;
                }
            });
        }

        // テストファイルをアップロード
        function uploadTestFile() {
            showMessage('テストファイルをアップロード中...');
            
            if (!AWS.config.credentials) {
                showError('認証情報が設定されていません');
                return;
            }

            const s3 = new AWS.S3({
                region: REGION,
                apiVersion: '2006-03-01'
            });

            const testContent = `認証テスト成功!
作成日時: ${new Date().toLocaleString('ja-JP')}
ユーザー: ${userPool.getCurrentUser().getUsername()}`;

            const params = {
                Bucket: BUCKET_NAME,
                Key: `test-file-${Date.now()}.txt`,
                Body: testContent,
                ContentType: 'text/plain; charset=utf-8'
            };

            s3.upload(params, (err, data) => {
                if (err) {
                    showError(`ファイルアップロードエラー: ${err.message}`);
                    addTestResult(`❌ ファイルアップロード失敗: ${err.message}`, true);
                } else {
                    showMessage('テストファイルのアップロードが成功しました!');
                    addTestResult('✅ ファイルアップロード成功: 認証済みユーザーは書き込み可能');
                    setTimeout(() => {
                        loadBucketContents();
                    }, 1000);
                }
            });
        }

        // ログアウト
        function logout() {
            const cognitoUser = userPool.getCurrentUser();
            if (cognitoUser) {
                cognitoUser.signOut();
            }
            AWS.config.credentials = null;
            document.getElementById('bucketContents').innerHTML = '';
            addTestResult('ログアウト: セッションとAWS認証情報をクリア');
            showLogin();
            showMessage('ログアウトしました');
        }

        // ページ読み込み時の処理
        window.onload = function() {
            showMessage('アプリケーションを初期化中...');
            addTestResult('アプリケーション初期化開始');
            
            const cognitoUser = userPool.getCurrentUser();
            if (cognitoUser) {
                cognitoUser.getSession((err, session) => {
                    if (err || !session.isValid()) {
                        showMessage('セッションが無効です。ログインしてください。');
                        addTestResult('既存セッション無効: 新規ログインが必要');
                        showLogin();
                    } else {
                        showMessage('既存のセッションを検出しました');
                        addTestResult('既存セッション有効: 自動的にログイン状態を復元');
                        setupAWSCredentials(session);
                        showMain();
                        setTimeout(() => {
                            loadBucketContents();
                        }, 1000);
                    }
                });
            } else {
                showMessage('ログインしてください');
                addTestResult('新規セッション: ログインが必要');
                showLogin();
            }
        };
    </script>
</body>
</html>

5-2. index.html を手順 1 で作成した S3 バケットにアップロードします。

6. 動作確認

6-1. 手順 1 で作成した S3 バケットのバケットウェブサイトエンドポイントにアクセスします。

6-2. 新規登録をクリックします。

6-3. ユーザー名とメールアドレスに同じメールアドレスを入力、パスワードも入力して登録をクリックします。

登録が成功するとメールアドレス宛てに認証コードが送信されますが今回は使用しません。

6-4. Cognito ユーザープールでユーザーを選択し、アカウントの確認をクリックします。

6-5. ウェブサイトをにメールアドレスとパスワードを入力してログインします。

ログイン後に認証情報の取得に失敗している旨のエラーが発生した場合、ログアウト、ページリロード後に再度ログインしてみてください。

6-6. ログイン後に S3 バケットに保存されている index.html のファイル名が表示されれば OK です。

6-7. テストファイルをアップロードをクリックし、追加されたファイルも表示されることを確認します。

6-8. ログアウト後、未認証で S3 アクセステストをクリックし、「期待通りアクセス拒否されました」のメッセージが表示されることを確認します。
この際、ブラウザの開発者ツールでゲストロールが使用されていることも確認できます。

ログインすることで認証されたロールを使用でき、ログインしていない場合にはゲストロールが使用されることを確認できました。

まとめ

今回は S3 の静的ウェブサイトと Cognito を連携してみました。
どなたかの参考になれば幸いです。

Discussion