Amazon Cognitoを使った認証付きファイルアップロード機能の実装をやってみた
はじめに
かつまたです。
今回は、ユーザープール、IDプールの構築学習を兼ねて、Amazon Cognitoを使って認証機能付きのファイルアップロードシステムを構築してみました。ユーザー登録・ログイン機能から、S3への安全なファイルアップロードまで、一通りの機能を実装しています。
やってみた
前提条件
- アップロード先S3バケット作成済み
S3バケットに直接ファイルアップロードを可能にするため、CORS設定を以下のように設定しました。
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"ETag"
]
}
]
ユーザープール作成
-
ユーザー認証を管理するユーザープールを作成します。Cognitoコンソールから「ユーザープールを作成」を選択します。
-
ユーザープールを以下のように設定し、作成します。
アプリクライアントタイプ:シングルページアプリケーション(SPA)
サインイン識別子のオプション:メールアドレス、ユーザー名
必須属性:email, name
コールバックURL:http://localhost:8000
IDプール作成
-
Cognitoコンソールから「新しいIDプールの作成」を選択する。
-
「認証プロバイダーの設定」の「ユーザーアクセス」では「認証されたアクセス」と「ゲストアクセス」どちらとも選択しました。「認証されたIDソース」は「Amazon Cognitoユーザープール」を選択しました。
- 「認証されたアクセス」と「ゲストアクセス」どちらとも新しいIAMロールを作成しました。「認証されたアクセス」のIAMロールは後ほど編集します。
認証されたロール:Cognito_MyFileUploadAuth_Role
認証されていないロール:Cognito_MyFileUploadUnauth_Role
- IDプロバイダーを接続でユーザープールとの接続設定を設定します。先ほど作成したユーザープールと、ユーザープールに紐づけられたアプリクライアントIDを選択します。
- 「認証されたアクセス」の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ファイル作成と動作確認
- 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 += ``;
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 = ``;
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 = ``;
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 = ``;
}
});
// AWS初期化
initializeAWS(result);
},
onFailure: function(err) {
statusDiv.innerHTML = ``;
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 = ``;
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 = ``;
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 = ``;
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 = ``;
} else {
statusDiv.innerHTML = ``;
}
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 = ``;
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>
- 作成したHTMLファイルをブラウザで開き、動作確認をします。
- ログイン後、任意のファイルをアップロードし、アップロード完了画面やS3バケット内にオブジェクトが存在していることを確認できました。
おわりに
ご覧いただきありがとうございました。
今回は、Amazon Cognitoを活用した認証機能付きファイルアップロードシステムの構築について説明しました。
コンソール側でのユーザープール、IDプールの構築やアプリケーション側でのユーザー登録・確認コード検証・ログインの一連のフロー実装によってCognitoの使われ方について学ぶことができたと感じます。
アノテーション株式会社について
アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。