Top Engineer/Ambassadorによるレビュー

アイレット25卒社員によるAWSブログリレーのAmazon Cognito編です。
この記事は、AWS Top Engineerであるアジャイル事業部小巻 玖美さんにレビューいただきました!

▼レビューいただいた方の投稿記事:
iret.media

Amazon Cognitoとは

Amazon Cognitoは、AWSが提供するマネージドな認証・認可サービスです。
*以降、本文中では「Cognito」と表記します。

これを利用することで、開発者はユーザー登録やログイン、パスワード管理、多要素認証(MFA)などの機能を安全かつ迅速にアプリへ導入することができます!
Cognitoは単なるログイン機能にとどまらず、ソーシャルログイン(Google・Facebook・Apple)やスケーリング、セキュリティ管理までAWSが自動で処理します。

仕組みとしては、次の2つのコンポーネントで構成されています。

  • ユーザープール(User Pool):
    ユーザーのサインアップ/サインインを管理し、認証後に JWTトークン を発行します。
  • IDプール(Identity Pool):
    認証済みユーザーに対し、S3やDynamoDBなどのAWSリソースへの一時的なアクセス権限を付与します。

このようにCognitoを使えば、アプリの認証・認可機能をセキュアかつスケーラブルに実装できます!

Cognitoの主な機能

Cognitoは、大きく分けて「ユーザープール」と「IDプール(IDフェデレーション)」という2つの主要機能で構成されています!

1. Cognito ユーザープール (User Pools)

アプリ独自のユーザーデータベースを提供する機能です。

  • ユーザー管理: アプリのユーザー(例:メールアドレスとパスワード)を安全に登録・管理します。
  • 認証機能: サインアップ、サインイン、パスワード忘れ、MFA(多要素認証)といった一連のログインフローを提供します。
  • ソーシャルログイン連携: Google、Facebook、Amazon、Appleなどの外部IDプロバイダー(IdP)と連携し、「Googleでログイン」機能を簡単に実装できます。
  • SAML/OIDC連携: 企業のID基盤(例:Microsoft Entra ID)と連携し、BtoB向けのSSO(シングルサインオン)を実現します。

2. Cognito IDプール (Identity Pools / IDフェデレーション)

認証されたユーザー(Cognitoユーザープールのユーザーや、Googleログインしたユーザーなど)に対して、一時的なAWS認証情報(IAMロール)を発行する機能です。

  • AWSサービスへのアクセス制御: 「ログインしたユーザーだけが、S3の自分のフォルダにファイルをアップロードできる」「API Gatewayの特定のAPIを実行できる」といった、AWSリソースへのきめ細かなアクセス制御を実現します。

Cognitoの主な機能とメリット

Cognitoは、認証・認可の仕組みをAWS上で簡単に構築・運用できるマネージドサービスです。
安全なユーザーデータベースの構築やスケーリングなどの煩雑な作業をAWSが自動で行うため、開発者はアプリケーション開発に専念できます!
ここでは、Cognitoの主な機能と特徴を紹介します。

1. 認証基盤の管理が不要(フルマネージド)

最大の特長は「サーバーレス」であることです。
ユーザーデータベースの運用、セキュリティパッチの適用、認証トラフィックの増減に応じたスケーリングなど、認証基盤に関わる管理はすべてAWSが自動で実施します。
そのため、開発者はインフラを意識せず、アプリの認証機能の実装に集中できます!

2. 多様な認証プロバイダーとの柔軟な連携

Cognitoはアプリの「認証ゲートウェイ」として機能し、ユーザーはさまざまな方法でログインできます。
クライアントは複数の認証方式を意識することなく、統一されたトークン(JWT)を通してアプリを利用可能です。

  • Cognitoユーザープール(メールアドレスやユーザー名での独自認証)
  • ソーシャルログイン(Google、Facebook、Apple など)
  • SAML 2.0(Microsoft Entra ID などの企業IdP連携)
  • OpenID Connect (OIDC)

3. 組み込みの認証フローとUI

Cognitoの強力な機能のひとつが「ユーザープール」です。
サインアップ(新規登録)、サインイン、パスワードリセット、MFA(多要素認証)など、複雑な認証フローを標準で提供します。
また、AWSがホストするログイン画面(ホストUI)も利用できるため、フロントエンドでログイン画面を一から構築する必要がありません!

4. AWSリソースへのアクセス制御(IDプール)

Cognitoは「認証(誰であるか)」だけでなく、「認可(何ができるか)」も管理します。
「IDプール」機能を使うことで、認証されたユーザー(ゲストを含む)に対し、IAMロールに基づいた一時的なAWS認証情報を発行可能です。
これにより、「S3の特定フォルダのみアクセス許可」「DynamoDBの自分のデータのみ読み書き許可」といった細かなアクセス制御を安全に実現できます!

5. 強力なセキュリティ機能

認証情報の保護はアプリの信頼性を左右します。
Cognitoはセキュリティのベストプラクティスを標準で備え、アプリを脅威から守ることができます!

  • 安全なトークン(JWT)の発行・管理
  • 高度なセキュリティ機能(不正アクセス検知、リスクベースの適応型認証)
  • AWS WAFとの連携(ボット攻撃や不正リクエストからの保護)
  • 各種コンプライアンス準拠(HIPAA、PCI DSS、SOC など)

料金体系

※以下の料金は、2025年10月28日時点での米ドル表示(東京リージョン)を基にしています。
為替レートおよびリージョン別料金の違いにより、実際の請求額は異なる可能性があります。最新情報は必ず
Amazon Cognito の公式料金ページ
をご確認ください。

Cognitoの料金は、他のサーバーレス型サービス同様に「従量課金制」を採用しています。固定費・ライセンス料はなく、主に “MAU(月間アクティブユーザー数)” に応じて、実際に使用した分に対してのみ課金されます。

※MAU とは、その月内にサインイン/サインアップ/トークン更新など何らかの ID 操作を行ったユニークユーザー数を指します。

Cognito ユーザープールの料金は、利用する機能に応じて「Lite」「Essentials」「Plus」の3つのティア(プラン)に分かれています!

1. ユーザープール の料金 (Lite ティア)

メールアドレス、ユーザー名、またはソーシャルログイン(Google, Facebook, Appleなど)を利用するユーザー向けの基本プランです。

料金範囲(MAU) MAU あたりの料金
最初の 10,000 (無料利用枠) USD 0.00
10,001~100,000 USD 0.0055
100,001~1,000,000 USD 0.0046
1,000,001~10,000,000 USD 0.00325
10,000,000 超 USD 0.0025

2. ユーザープール の料金 (Essentials ティア)

Liteティアの機能に加え、企業向けSSO(SAMLまたはOIDCフェデレーション)を利用するユーザー向けのプランです!
(例:Microsoft Entra ID や Okta と連携する場合)

MAU MAU あたりの料金
最初の 10,000 (無料利用枠) USD 0.00
10,000 超 USD 0.015

3. ユーザープール の料金 (Plus ティア)

Essentialsティアの機能に加え、「高度なセキュリティ機能」(侵害された認証情報の検出、リスクベースの適応型認証など)を利用するユーザー向けの最上位プランです!

機能 MAU あたりの料金
Plus ティア MAU US $0.02

※Plus設定がされたユーザープールでユーザーが月に1回以上アクティブになると、そのユーザーはPlus MAUとしてカウントされます。

4. IDプール(フェデレーテッドアイデンティティ) の料金

認証されたユーザーに AWS の一時的アクセス権限(IAMロール)を発行する機能に関する料金です。

AWS公式日本語ページでは「アイデンティティプールを使用しても料金は発生しません」と明記されています。

つまり、ユーザープールを通じて認証されたユーザーがそのままIDプールを使う場合、別途 MAU課金は発生しません。

5. その他の料金

Cognito本体のMAU課金とは別に、認証プロセスで使用される以下のサービスに対して別途料金が発生します!

  • SMSメッセージ送信:
    MFA(多要素認証)、パスワードリセット、電話番号認証などで SMS を送信する場合、Amazon SNS の SMS 送信料金が適用されます(国・通信事業者により単価が異なります)。
  • Eメール送信:
    Cognitoではデフォルトで一定数の Eメール(招待メール、検証コードなど)送信が可能ですが、上限を超えるかカスタマイズする場合は Amazon SES を使う必要があり、SES の送信料金が別途発生します。

無料利用枠について

Cognito の Lite ティアと Essentials ティアには、AWSアカウントの利用期間に関わらず継続的に適用される 無期限無料枠 があります。

  • Lite ティア(ソーシャルIDプロバイダー経由など): 毎月 10,000 MAU まで無料
  • Essentials ティア(SAML/OIDCフェデレーション): 毎月 50 MAU まで無料
  • Plus ティア: 無料利用枠はありません
  • IDプール使用時: 料金は発生しません(MAU課金対象外)

この無料枠のおかげで、多くのアプリケーションや検証用途では、料金を支払うことなく利用を開始できます!

ユースケース

Cognitoは、安全なユーザー認証基盤を最小限の開発工数で構築できる点が特長です。
あらゆるWeb・モバイルアプリの「認証・認可の基盤」として、ログインやアクセス制御が必要なケースで広く採用されています。

ユースケース1:Web/モバイルアプリの「ログイン機能」

例:一般消費者向けECサイト、SNS、モバイルゲームなど

Cognitoは、ユーザープールを通じて、アプリ独自のユーザー管理機能(サインアップ、サインイン)を提供します。
パスワードリセット、メール検証、MFAなどの複雑なフローを自前で構築する必要がなくなります。

さらに、Google、Facebook、Appleなどのソーシャルログインも簡単に追加でき、開発工数を大幅に削減します!

主な利用機能: ユーザープール、ソーシャルログイン、ホストUI

ユースケース2:AWSリソースへのセキュアなアクセス制御

例:ユーザー専用のS3フォルダへの写真アップロード、DynamoDBの自分専用データの読み書きなど

Cognitoは、IDプールと連携することで、認証済みユーザーに対して一時的なAWS認証情報(IAMロール)を発行します。
これにより、クライアントアプリがサーバーを経由せず、AWSリソース(S3やDynamoDBなど)に直接アクセスする場合でも、「誰が」「何を」できるかを安全に制御できます!

主な利用機能: IDプール、IAMロール、AWS SDK(Amplify)

ユースケース3:B2B/SaaSアプリケーションのSSO(シングルサインオン)

例:企業向けSaaS、社内ポータルサイトなど

B2Bサービスでは、顧客企業が自社で使っているID(例:Microsoft Entra ID)でログインしたいという要求が一般的です。
Cognitoは、SAMLやOIDCフェデレーションをサポートしており、外部のIDプロバイダーと連携したSSO(シングルサインオン)を実現します!

主な利用機能: SAML/OIDC IDプロバイダー連携、ユーザープール

実践:Cognito認証ユーザーとしてS3に画像をアップロード&表示してみる

ここからは、実際に Amazon Cognito を使ってユーザー認証を行い、ログインしたユーザーだけが S3バケット 内の画像を表示できるシンプルなWebページを作ってみます!

流れとしては、まず Cognitoユーザープール でユーザーを認証し、その後 IDプール から一時的なAWSクレデンシャルを取得してS3へアクセスする、というステップです。

アーキテクチャ(処理のフロー)

実装を始める前に、今回作成するアプリケーションが「どのように認証し、S3にアクセスするか」のフロー図です!



フロー図の通り、処理は大きく3つのステップに分かれます。

ステップ1:ユーザー認証
ユーザーがWebサイトでサインインすると、Cognito ユーザープールが認証を行い、IDトークン(JWT)を発行します。

ステップ2:クレデンシャル取得
アプリ(Amplify SDK)は、そのIDトークンを Cognito ID プール に提示し、S3アクセス許可を持つ一時的なAWSクレデンシャル(認証キー)を取得します。

ステップ3:S3アクセス
取得した一時クレデンシャルを使い、Storage.putやStorage.getを実行して、S3バケットへの画像のアップロードや表示を行います。

  このように、Cognito ユーザープールによる「認証(本人確認)」と、Cognito ID プールによる「認可(権限付与)」を明確に分離して利用しています!

事前準備

チュートリアルをスムーズに進めるために、以下の環境を用意しておく必要があります。

  • テキストエディタ: 説明では VSCode(Visual Studio Code)を使いますが、お好きなエディタでも問題ありません。
  • ローカル実行環境: HTMLとJavaScriptをブラウザで動かすために、簡単なローカルサーバーを用意します。
    • 例1: VSCodeの拡張機能「Live Server」を使えば、ワンクリックでHTMLをプレビューできます。
    • 例2:Node.jsとnpmがあれば、http-serverなどの軽量サーバーを立ち上げることも可能です。
    • その他: 普段使っているローカルサーバーがあれば、それを使ってもOKです。

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

ここでは、AWS マネジメントコンソールから Amazon Cognito のユーザープールを作成する手順をスクリーンショット付きで紹介します!

1-1. 作成の開始

まず Amazon Cognito コンソールを開き、「ユーザープール」メニューへ移動して、右上の「ユーザープールを作成」ボタンをクリックします。

1-2. サインインオプションとアプリケーションの定義

次に、ユーザーのサインイン方法やアプリケーション(クライアント)の設定を行います。

「サインイン識別子のオプション」で、ユーザーが何を使ってサインインするかを選択します。ここでは「ユーザー名」にチェックを入れます。

「アプリケーションを定義」セクションで、「アプリケーションタイプ」として「シングルページアプリケーション (SPA)」を選択します。

「アプリケーションに名前を付ける」に、任意のクライアント名(例: my-spa-client)を入力します。


1-3. アプリケーションのリリース設定

最後に、アプリケーションのリリースに関する詳細設定を行います。

「サインアップのための必須属性」で、ユーザー登録時に必須とする情報を選択します。ここでは「email」を選択しています。

すべての設定を確認したら、右下の「ユーザーディレクトリを作成する」ボタンをクリックして完了です。


2. Cognito IDプールの作成

次に、ユーザープールで認証されたユーザーにAWSの一時認証情報を発行する「IDプール」を作成します。
これにより、ユーザーはS3などのAWSリソースにアクセスできるようになります!

2-1. IDプールの作成開始

Cognitoコンソールの左側メニューから「ID プール」を選択します。
画面右上の「ID プールを作成」ボタンをクリックします。

2-2. IDプールの信頼を設定

「ID プールの信頼を設定」画面で、ユーザーアクセスの種類として「認証されたアクセス」にチェックを入れます。
認証された ID ソースとして「Amazon Cognito ユーザープール」にチェックを入れ、「次へ」ボタンをクリックします。


2-3. 許可を設定

「許可を設定」画面で、認証されたユーザーが引き受けるIAMロールを設定します。
新しい IAM ロールを作成」を選択し、「IAM ロール名」に任意の名前(例: my-app-cognito-auth-role)を入力して「次へ」をクリックします。


2-4. IDプロバイダーを接続

「ID プロバイダーを接続」画面で、先ほど作成したユーザープールの「ユーザープール ID」と「アプリクライアント ID」を入力します。
「ロール設定」では「デフォルトの認証されたロールを使用」が選択されていることを確認します。

そのまま下にスクロールし、他の設定はデフォルト(クレームマッピング:非アクティブ)のまま「次へ」をクリックします。

2-5. IDプールのプロパティを設定

「ID プールのプロパティを設定」画面で、ID プールの「Name」に任意の名前(例: my-app-id-pool)を入力します。
「基本 (クラシック) 認証」セクションで、「基本フローをアクティブ化」にチェックを入れます。これにより、Amplifyライブラリなしでも認証情報を取得しやすくなります。「次へ」をクリックします。

2-6. 確認および作成

最後の「確認および作成」画面で、すべての設定内容を確認します。
問題がなければ、右下の「ID プールを作成」ボタンをクリックして完了です。

3. S3バケットの作成

次に、認証されたユーザーだけがアクセスできる画像を保存するためのS3バケットを作成します。Cognitoから取得した一時認証情報を使って、このバケット内のオブジェクトにアクセスできるようにします!

3-1. S3バケット作成

AWS マネジメントコンソールで「S3」サービスを開きます。
左側メニューから「バケット」を選択し、「バケットを作成」ボタンをクリックします。

3-2. 一般設定

「バケットを作成」画面で、以下の項目を設定します。

  • バケット名: グローバルで一意となる任意の名前を入力します。(例:my-app-secure-images-xxxx)
  • AWS リージョン: 任意のリージョンを選択します。(例: アジアパシフィック (東京) ap-northeast-1)
  • オブジェクト所有者:ACL 無効 (推奨)」が選択されていることを確認します。これにより、バケットポリシーでのみアクセスコントロールを行うシンプルな設定になります。

3-3. パブリックアクセス設定

下にスクロールし、「このバケットのブロックパブリックアクセス設定」を確認します。

  • パブリックアクセスをすべてブロック」にチェックが入っていることを確認します。

今回はCognitoで認証された特定のユーザーにのみアクセスを許可するため、バケット全体がパブリック(公開)にならないよう、この設定を有効にしておくことが重要です。


3-4. バケットの作成完了

その他の設定(バージョニング、暗号化など)はデフォルトのまま一番下までスクロールし、「バケットを作成」ボタンをクリックして作成を完了します。

4. IAMポリシーの作成とロールへのアタッチ

JavaScriptからS3バケットを操作するために、まず「Cognitoで認証されたユーザーが、自分のS3フォルダに対して読み書きできる」という権限ポリシー(ルール)を作成します。次に、そのポリシーを「手順2」で作成したIAMロールに関連付けます。

4-1. IAMポリシーの作成

AWS マネジメントコンソールで「IAM」サービスを開き、左側メニューから「ポリシー」を選択します。
画面右上の「ポリシーの作成」ボタンをクリックします。


ポリシーエディタ」画面が開いたら、「JSON」タブを選択します。
表示されたエディタに、以下のJSONコードを貼り付けます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListMultipartUploadParts",
                "s3:AbortMultipartUpload"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME/private/${cognito-identity.amazonaws.com:sub}/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME"
            ]
        }
    ]
}

貼り付けたJSONの以下の2箇所を、ご自身の環境に合わせて必ず書き換えてください

  • YOUR_BUCKET_NAME: 「手順3-2」で作成したご自身のS3バケット名に置き換えます。

書き換えたら、右下の「次へ」ボタンをクリックします。


「確認して作成」画面で、「ポリシー名」に任意の名前(例:S3CognitoUserAccessPolicy)を入力します。


オプションは空のままで問題ありません。一番下までスクロールし、許可の概要を確認したら「ポリシーの作成」ボタンをクリックします。


4-2. S3 CORS (クロスオリジンリソース共有) の設定

次に、ブラウザ(http://127.0.0.1:5500など)からS3バケットに直接GETやPUTリクエストを送信できるように許可します。

「アクセス許可」タブの一番下にある「クロスオリジンリソース共有 (CORS)」セクションの「編集」ボタンをクリックします。

表示されたエディタ(JSON形式)に、以下のJSONを貼り付けます。

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "DELETE",
            "HEAD"
        ],
        "AllowedOrigins": [
            "http://127.0.0.1:5500",
            "http://localhost:5500"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]


AllowedOrigins の部分は、ご自身のローカルサーバーのURLに合わせてください。
右下の「変更の保存」ボタンをクリックします。

これでS3バケット側の準備がすべて完了しました。

4-3. IAMロールへのポリシーのアタッチ

次に、今作成したポリシーを、「手順2」で作成したCognito IDプールのIAMロールにアタッチ(関連付け)します。

IAMコンソールの左側メニューから「ロール」を選択します。
検索ボックスに、「手順2-3」で作成したロール名を入力して検索し、該当するロール名をクリックします。

ロールの詳細画面が開いたら、「許可」タブ内の「許可を追加」ボタンをクリックし、ドロップダウンから「ポリシーをアタッチ」を選択します。


「許可をアタッチ」画面で、「その他の許可ポリシー」の検索ボックスに、先ほど作成したポリシー名(例: S3CognitoUserAccessPolicy)を入力します。
表示されたポリシーにチェックを入れ、右下の「許可を追加」ボタンをクリックします。

これで、Cognitoで認証されたユーザーが、S3バケット内の自分のフォルダにアクセスするためのIAM権限の設定が完了しました。

【TIPS】S3アップロードを安定させるための設定ポイント

S3では、5MBを超えるファイルをアップロードする際、自動的に「マルチパートアップロード」という方式が使用されます。
IAMポリシーやCORS設定で必要な操作が許可されていない場合、5MB以上のファイルをアップロードしようとすると 400 Bad Request のようなエラーが発生します。

そのため、以下の設定を行っておく必要があります。

  • IAMポリシー:s3:PutObject に加えて、(エラー処理用の)s3:AbortMultipartUploadを許可
  • CORS設定:PUT、POST、DELETE、ETag などのメソッドを許可

これにより、マルチパートアップロード時でもエラーを防ぎ、ファイルサイズに関係なく安定したアップロードを実現することができます!

5. HTMLとJavaScriptの実装

AWS側の設定が完了したら、次はフロントエンド(Webページ)の実装です。以下のファイルを作成し、ローカルサーバー(VSCodeのLive Serverなど)でindex.htmlを開いて動作を確認します!

5-1. ファイル構成

以下の構成でファイルを作成します。

.
├── css/
│   └── style.css
├── js/
│   └── app.js
├── app.html
├── confirm.html
├── index.html
└── signup.html

5-2. ライブラリの読み込み (aws-amplify.min.js)

aws-amplify.min.jsは、CDNから直接読み込むため、ローカルにダウンロードする必要はありません。各HTMLファイルの末尾で読み込みます。(この後のHTMLコード例に含まれています)

5-3. CSS (css/style.css)

ページ全体のスタイルを定義するCSSファイルです。

/* 基本設定 */
:root {
    --primary-color: #007bff;
    --primary-hover-color: #0056b3;
    --text-dark: #343a40;
    --text-medium: #6c757d;
    --bg-light: #f8f9fa;
    --bg-white: #ffffff;
    --border-color: #dee2e6;
    --error-color: #dc3545;
    --shadow-light: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    --shadow-medium: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
}

body {
    font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
    background-color: var(--bg-light);
    margin: 0;
    padding: 2rem;
    line-height: 1.6;
    color: var(--text-dark);
}

/* サインイン/サインアップ用スタイル */

body.auth-page {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: calc(100vh - 4rem); /* paddingを考慮 */
}

.form-container {
    width: 100%;
    max-width: 420px;
    background-color: var(--bg-white);
    padding: 2.5rem;
    border-radius: 12px;
    box-shadow: var(--shadow-medium);
}

.form-container h2 {
    text-align: center;
    color: var(--text-dark);
    margin-top: 0;
    margin-bottom: 2rem;
    font-weight: 600;
}

.input-group {
    margin-bottom: 1.25rem;
}

.input-group label {
    display: block;
    margin-bottom: 0.5rem;
    color: var(--text-dark);
    font-weight: 500;
}

.input-group input {
    width: 100%;
    padding: 0.8rem 1rem;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    box-sizing: border-box;
    font-size: 0.95rem;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.input-group input:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
    outline: none;
}

.form-button {
    width: 100%;
    padding: 0.85rem;
    border: none;
    border-radius: 6px;
    background-color: var(--primary-color);
    color: white;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s, box-shadow 0.2s;
}

.form-button:hover {
    background-color: var(--primary-hover-color);
    box-shadow: var(--shadow-light);
}

.form-button:active {
    transform: translateY(1px);
}

.form-links {
    text-align: center;
    margin-top: 1.5rem;
}
.form-links p {
    margin: 0.5rem 0;
    color: var(--text-medium);
    font-size: 0.9rem;
}
.form-links a {
    color: var(--primary-color);
    text-decoration: none;
    font-weight: 500;
}
.form-links a:hover {
    text-decoration: underline;
}

/* (B) メインアプリ (app.html) 用スタイル */

.app-container {
    max-width: 960px;
    margin: 0 auto;
    background-color: var(--bg-white);
    padding: 2.5rem 3rem;
    border-radius: 12px;
    box-shadow: var(--shadow-medium);
}

.header {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    margin-bottom: 2.5rem;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid var(--border-color);
}
.header h1 {
    margin: 0;
    font-size: 2.2rem;
    font-weight: 700;
}
.header .status {
    font-size: 1rem;
    color: var(--text-medium);
    margin: 0;
    margin-left: 1.5rem;
    font-weight: 500;
    padding: 0.2rem 0.6rem;
    background-color: #e9ecef;
    border-radius: 5px;
}
.header .signout-button {
    margin-left: auto;
    padding: 0.6rem 1.2rem;
    background-color: var(--error-color);
    color: white;
    border: none;
    border-radius: 6px;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}
.header .signout-button:hover {
    background-color: #c82333;
}

.content h2 {
    font-size: 1.8rem;
    font-weight: 600;
    margin-top: 2rem;
    margin-bottom: 1.5rem;
    padding-bottom: 0.5rem;
    border-bottom: 2px solid var(--primary-color);
    display: inline-block;
}

.app-box {
    margin-bottom: 2rem;
    padding: 1.5rem;
    border: 1px solid var(--border-color);
    border-radius: 8px;
    box-shadow: var(--shadow-light);
}

.app-box p {
    margin-top: 0;
    color: var(--text-medium);
}

/* 共通スタイル */
.inline-form {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
    align-items: center;
}
.inline-form input {
    flex: 1 1 180px;
    padding: 0.8rem 1rem;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    box-sizing: border-box;
    font-size: 0.95rem;
    transition: border-color 0.2s, box-shadow 0.2s;
}
.inline-form input:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
    outline: none;
}
.inline-form .form-button {
    flex: 0 0 auto;
    width: auto;
    padding: 0.8rem 1.8rem;
}

.image-preview-box {
    margin-top: 1.5rem;
    padding: 1rem;
    border: 2px dashed var(--border-color);
    border-radius: 8px;
    min-height: 100px;
    background-color: var(--bg-light);
    display: flex;
    justify-content: center;
    align-items: center;
}
.image-preview-box img {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
}

.error-message {
    color: var(--error-color);
    background-color: #f8d7da;
    border: 1px solid var(--error-color);
    padding: 1rem 1.5rem;
    border-radius: 8px;
    margin-top: 1.5rem;
    text-align: center;
    font-weight: 600;
    display: none;
}

.success-message {
    color: #155724;
    background-color: #d4edda;
    border: 1px solid #c3e6cb;
    padding: 1rem 1.5rem;
    border-radius: 8px;
    margin-top: 1.5rem;
    text-align: center;
    font-weight: 600;
    display: none;
}

5-4. HTMLファイルの作成

次に、4つの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>サインイン</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body class="auth-page">
<div class="form-container">
<h2>サインイン</h2>

<div class="input-group">
<label for="signInUser">ユーザー名</label>
<input type="text" id="signInUser" placeholder="ユーザー名" required>
</div>
<div class="input-group">
<label for="signInPass">パスワード</label>
<input type="password" id="signInPass" placeholder="********" required>
</div>

<div id="errorMessage" class="error-message" style="display: none;"></div>
<div id="successMessage" class="success-message" style="display: none;"></div>
<button id="btnSignIn" class="form-button">サインイン</button>

<div class="form-links">
<p><a href="#">パスワードをお忘れですか?</a></p>
<p>アカウントをお持ちでないですか? <a href="signup.html">新規登録</a></p>
</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/aws-amplify/dist/aws-amplify.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
signup.html (新規登録ページ)

新しいユーザーがアカウントを作成する画面です。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>アカウント登録</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body class="auth-page"> 
<div class="form-container">
<h2>アカウント登録</h2>
<div class="input-group">
<label for="signUpUser">ユーザー名</label>
<input type="text" id="signUpUser" placeholder="ユーザー名" required>
</div>
<div class="input-group">
<label for="signUpEmail">メールアドレス</label>
<input type="email" id="signUpEmail" placeholder="example@email.com" required>
</div>
<div class="input-group">
<label for="signUpPass">パスワード</label>
<input type="password" id="signUpPass" placeholder="8文字以上、英数字混合" required>
</div>
<div id="errorMessage" class="error-message" style="display: none;"></div>
<div id="successMessage" class="success-message" style="display: none;"></div>
<button id="btnSignUp" class="form-button">登録する</button>
<div class="form-links">
<p>すでにアカウントをお持ちですか? <a href="index.html">サインイン</a></p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/aws-amplify/dist/aws-amplify.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
confirm.html (確認コード入力ページ)

新規登録時に送信されるメール確認コードを入力する画面です。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>確認コードの入力</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body class="auth-page"> 
<div class="form-container">
<h2>確認コードの入力</h2>
<p style="text-align: center; margin-top: -1.5rem; margin-bottom: 1.5rem; color: var(--text-medium);">
<strong id="usernameDisplay"></strong> 宛に送信された<br>確認コードを入力してください。
</p>
<div class="input-group">
<label for="confirmationCode">確認コード</label>
<input type="text" id="confirmationCode" placeholder="123456" required>
</div>
<div id="errorMessage" class="error-message" style="display: none;"></div>
<div id="successMessage" class="success-message" style="display: none;"></div>
<button id="btnConfirmSignUp" class="form-button">アカウントを有効化する</button>
<div class="form-links">
<p><a href="#" id="btnResendCode">コードを再送する</a></p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/aws-amplify/dist/aws-amplify.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>
app.html (メインアプリケーションページ)

サインイン後に表示される、S3への画像アップロードと取得を行うメイン画面です。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cognito S3 検証</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="app-container">
<header class="header">
<h1>Cognito S3画像アクセス検証</h1>
<p id="status" class="status">ステータス: 読み込み中...</p>
<button id="btnSignOut" class="signout-button">サインアウト</button>
</header>
<main class="content">
<div id="storageUploadSection">
<h2>S3 画像アップロード (Storage)</h2>
<div class="app-box">
<p>
認証ユーザー専用の <code>private/</code> 領域にファイルをアップロードします。<br>
</p>
<div class="inline-form">
<input type="file" id="fileUpload" style="flex-grow: 2;">
<button id="btnPutImage" class="form-button">アップロード</button>
</div>
<div id="uploadErrorMessage" class="error-message" style="display: none;"></div>
<div id="uploadSuccessMessage" class="success-message" style="display: none;"></div>
</div>
</div>
<div id="storageSection">
<h2>S3 画像取得 (Storage)</h2>
<div class="app-box">
<p>
アップロードしたファイル名(キー)を指定して取得します。<br>
(例: <code>sample.png</code>)
</p>
<div class="inline-form">
<input type="text" id="fileKey" placeholder="例: sample.png" style="flex-grow: 2;">
<button id="btnGetImage" class="form-button">画像を取得</button>
</div>
<div id="imageContainer" class="image-preview-box">
</div>
<div id="getErrorMessage" class="error-message" style="display: none;"></div>
<div id="getSuccessMessage" class="success-message" style="display: none;"></div>
</div>
</div>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/aws-amplify/dist/aws-amplify.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

5-5. メインスクリプト (js/app.js)

認証とS3操作のロジックを記述するメインのJavaScriptファイルです。(注:awsconfig内の値は、ご自身のAWS環境の値に置き換えてください)

// 1. AWS Amplifyの設定 (設定オブジェクトのみを先に定義)
const awsconfig = {
    Auth: {
        // リージョン (★ご自身の値に書き換えてください★)
        region: 'ap-northeast-1', 
        
        // Cognito ユーザープールの ID (★ご自身の値に書き換えてください★)
        userPoolId: 'ap-northeast-1_XXXXXXXXX', 
        
        // Cognito ユーザープールのアプリクライアント ID (★ご自身の値に書き換えてください★)
        userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxx', 
        
        // Cognito ID プール (フェデレーテッドアイデンティティ) の ID (★ご自身の値に書き換えてください★)
        identityPoolId: 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' 
    },
    Storage: {
        AWSS3: {
            //S3バケット名 (★ご自身の値に書き換えてください★)
            bucket: 'your-s3-bucket-name',
            //S3バケットのリージョン (★ご自身の値に書き換えてください★)
            region: 'ap-northeast-1'
        }
    }
};

// 2. 共通関数
// id引数を追加し、どのメッセージ欄を操作するか指定できるようにします。
// 認証ページ用にデフォルトのidも設定しておきます。

function showError(message, id = 'errorMessage') {
    // 他のメッセージはすべて消す
    clearAllMessages();
    
    const errorDiv = document.getElementById(id);
    if (errorDiv) {
        errorDiv.innerText = message.message || message;
        errorDiv.style.display = 'block';
    }
}
function clearError(id = 'errorMessage') {
    const errorDiv = document.getElementById(id);
    if (errorDiv) errorDiv.style.display = 'none';
}

function showSuccess(message, id = 'successMessage') {
    // 他のメッセージはすべて消す
    clearAllMessages();

    const successDiv = document.getElementById(id);
    if (successDiv) {
        successDiv.innerText = message;
        successDiv.style.display = 'block';
    }
}
function clearSuccess(id = 'successMessage') {
    const successDiv = document.getElementById(id);
    if (successDiv) successDiv.style.display = 'none';
}

// 全てのメッセージ欄をクリアするヘルパー
function clearAllMessages() {
    clearError('errorMessage');
    clearSuccess('successMessage');
    clearError('uploadErrorMessage');
    clearSuccess('uploadSuccessMessage');
    clearError('getErrorMessage');
    clearSuccess('getSuccessMessage');
}


// 3. メイン処理
document.addEventListener('DOMContentLoaded', () => {
    try {
        // 修正:CDNから読み込むため、ローカルファイルのエラーメッセージを変更
        if (typeof aws_amplify === 'undefined') { throw new ReferenceError("aws-amplify.min.js が見つかりません。CDNから読み込んでください。"); }
        aws_amplify.Amplify.configure(awsconfig);
    } catch (e) { console.error("Amplifyの設定失敗:", e); showError(e.message || "設定失敗"); return; }
    
    const path = window.location.pathname;
    
    if (path.endsWith('/') || path.endsWith('index.html')) { initSignInPage(); } 
    else if (path.endsWith('signup.html')) { initSignUpPage(); } 
    else if (path.endsWith('confirm.html')) { initConfirmPage(); } 
    else if (path.endsWith('app.html')) { initAppPage(); }
});

// 4. 各ページの初期化関数
async function initSignInPage() {
    try {
        await aws_amplify.Auth.currentAuthenticatedUser();
        window.location.href = 'app.html';
    } catch (e) {
        /* OK */
    }
    const t = document.getElementById("btnSignIn");
    t && t.addEventListener("click", async () => {
        clearAllMessages();
        const e = document.getElementById("signInUser").value,
            n = document.getElementById("signInPass").value;
        if (!e || !n) return void showError("ユーザー名とパスワードを入力してください。");
        try {
            t.disabled = !0;
            t.innerText = "サインイン中...";
            await aws_amplify.Auth.signIn(e, n);
            window.location.href = "app.html";
        } catch (e) {
            showError(e);
            t.disabled = !1;
            t.innerText = "サインイン";
        }
    })
}

function initSignUpPage() {
    const t = document.getElementById("btnSignUp");
    t && t.addEventListener("click", async () => {
        clearAllMessages();
        const e = document.getElementById("signUpUser").value,
            n = document.getElementById("signUpEmail").value,
            o = document.getElementById("signUpPass").value;
        if (!e || !n || !o) return void showError("すべての項目を入力してください。");
        try {
            t.disabled = !0;
            t.innerText = "登録中...";
            await aws_amplify.Auth.signUp({
                username: e,
                password: o,
                attributes: {
                    email: n
                }
            });
            window.location.href = `confirm.html?username=${encodeURIComponent(e)}`;
        } catch (e) {
            showError(e);
            t.disabled = !1;
            t.innerText = "登録する";
        }
    })
}

function initConfirmPage() {
    const t = new URLSearchParams(window.location.search).get("username"),
        e = document.getElementById("usernameDisplay");
    t && e ? e.innerText = `ユーザー名「${t}」` : showError("確認対象のユーザー名が不明です。");
    const n = document.getElementById("btnConfirmSignUp");
    n && n.addEventListener("click", async () => {
        clearAllMessages();
        const e = document.getElementById("confirmationCode").value;
        if (!e) return void showError("確認コードを入力してください。");
        if (!t) return void showError("ユーザー名が不明です。");
        try {
            n.disabled = !0;
            n.innerText = "確認中...";
            await aws_amplify.Auth.confirmSignUp(t, e);
            alert("アカウントが有効化されました。サインインページに移動します。");
            window.location.href = "index.html";
        } catch (e) {
            showError(e);
            n.disabled = !1;
            n.innerText = "アカウントを有効化する";
        }
    });
    const o = document.getElementById("btnResendCode");
    o && o.addEventListener("click", async e => {
        e.preventDefault();
        clearAllMessages();
        if (!t) return void showError("ユーザー名が不明です。");
        try {
            await aws_amplify.Auth.resendSignUp(t);
            alert("確認コードを再送しました。");
        } catch (e) {
            showError(e);
        }
    })
}


/**
 * メインアプリページ (app.html) の処理
 */
async function initAppPage() {
    // cognitoIdentityId 変数と currentCredentials 呼び出しを削除
    try {
        const user = await aws_amplify.Auth.currentAuthenticatedUser();
        document.getElementById('status').innerText = `ステータス: サインイン中 (${user.username})`;
    } catch (e) {
        window.location.href = 'index.html';
        return; 
    }
    
    // アップロード機能のロジック 
    const btnPutImage = document.getElementById('btnPutImage');
    const fileUpload = document.getElementById('fileUpload');

    if (btnPutImage && fileUpload) {
        btnPutImage.addEventListener('click', async () => {
            clearAllMessages(); // 全メッセージをクリア
            const file = fileUpload.files[0];
            if (!file) {
                showError('ファイルが選択されていません。', 'uploadErrorMessage');
                return;
            }
            btnPutImage.disabled = true;
            btnPutImage.innerText = 'アップロード中...';

            try {
                // identityId の指定を削除
                const result = await aws_amplify.Storage.put(file.name, file, { 
                    level: 'private',
                    contentType: file.type
                });
                
                console.log("Upload 成功:", result);
                showSuccess(`「${file.name}」のアップロードに成功しました。`, 'uploadSuccessMessage');

            } catch (error) {
                console.error("Storage.put エラー:", error);
                showError(error, 'uploadErrorMessage');
            }
            btnPutImage.disabled = false;
            btnPutImage.innerText = 'アップロード';
        });
    }
    
    //  取得機能 
    const btnGetImage = document.getElementById('btnGetImage');
    if (btnGetImage) {
        btnGetImage.addEventListener('click', async () => {
            clearAllMessages(); // 全メッセージをクリア
            const fileKey = document.getElementById('fileKey').value;
            if (!fileKey) {
                showError('ファイル名(キー)を入力してください。', 'getErrorMessage');
                return;
            }
            const imageContainer = document.getElementById('imageContainer');
            imageContainer.innerHTML = '画像を取得中...'; 

            try {
                // identityId の指定を削除
                const imageUrl = await aws_amplify.Storage.get(fileKey, { 
                    level: 'private', 
                    expires: 60 
                });
                
                const img = document.createElement('img');
                img.src = imageUrl;
                img.alt = fileKey;
                
                img.onerror = function() {
                    console.error("画像読み込み失敗(onerror):", imageUrl);
                    showError(`画像の読み込みに失敗しました。\n1. IAMロールの権限\n2. S3のCORS設定\n3. S3のファイルパス('private/<あなたのID>/${fileKey}')\nを確認してください。`, 'getErrorMessage');
                    imageContainer.innerHTML = '画像の取得に失敗しました。';
                };
                img.onload = function() {
                    imageContainer.innerHTML = '';
                    imageContainer.appendChild(img);
                    showSuccess(`「${fileKey}」の取得に成功しました。`, 'getSuccessMessage');
                };

            } catch (error) {
                console.error("Storage.get エラー:", error);
                showError(error, 'getErrorMessage');
                imageContainer.innerHTML = '画像の取得に失敗しました。';
            }
        });
    }
    
    // サインアウト
    const btnSignOut = document.getElementById('btnSignOut');
    if (btnSignOut) {
        btnSignOut.addEventListener('click', async () => {
            clearAllMessages(); // 全メッセージをクリア
            try { await aws_amplify.Auth.signOut(); window.location.href = 'index.html'; } catch (error) { showError(error); }
        });
    }
}

6. 動作確認

最後に、作成したHTMLファイルをローカルサーバーで起動し、Cognitoでのユーザー登録からS3の画像アップロード・取得までの一連の流れを確認します!

6-1. アカウントの新規登録

まず、ローカルサーバーで index.html(サインインページ)を開きます。「新規登録」のリンクをクリックしてアカウント登録ページ(signup.html)に移動します。


任意の「ユーザー名」「メールアドレス」「パスワード」を入力し、「登録する」ボタンをクリックします。


確認コードの入力ページ(confirm.html)に移動します。登録したメールアドレスに6桁の確認コードが届いていることを確認し、入力して「アカウントを有効化する」ボタンをクリックします。


「アカウントが有効化されました」というアラートが表示されたら「OK」をクリックします。


6-2. サインインとS3へのアップロード

自動的にサインインページに戻るので、先ほど登録した「ユーザー名」と「パスワード」でサインインします。

登録したアカウントでサインイン

サインインに成功すると、メインのアプリケーションページ(app.html)が表示されます。ヘッダーに自分のユーザー名が表示されていることを確認します。


「S3 画像アップロード」セクションで、「ファイルを選択」ボタンから任意の画像ファイル(例:sample.png)を選択し、「アップロード」ボタンをクリックします。


「(ファイル名)のアップロードに成功しました。」という緑色のメッセージが表示されれば成功です。


6-3. S3からの画像取得

次に、「S3 画像取得」セクションのテキストボックスに、先ほどアップロードしたファイル名(sample.png)を入力し、「画像を取得」ボタンをクリックします。


S3バケットに保存された画像が正しく読み込まれ、プレビューボックスに表示されます。「(ファイル名)の取得に成功しました。」というメッセージが表示されることを確認します。


最後に、右上の「サインアウト」ボタンをクリックすると、セッションが切れ、最初のサインインページ(index.html)に戻ることを確認します。

これで、Cognitoによる認証と、認証されたユーザーのみがS3の特定領域にアクセスできる一連の流れが完成しました!

7. まとめ

今回は、Amazon Cognitoの概要から、ユーザープール(認証)とIDプール(認可)の役割、そしてS3バケットと連携して「認証したユーザーだけが画像を表示できる」シンプルなWebアプリケーションの実装手順までを紹介しました。

Cognitoの魅力は、これまで自前で構築・運用が大変だったユーザー認証(サインアップ、サインイン、確認コード発行)の仕組みを、安全かつスケーラブルなマネージドサービスとして簡単に導入できる点にあります。

さらに、IDプールとIAMロール、S3(今回はIAMポリシーで設定)を組み合わせることで、認証されたユーザー単位でAWSリソースへのアクセスをセキュアに制御できる柔軟性も大きな特徴です!

この記事が、Webサイトに認証機能を導入したい方や、Cognitoと他のAWSサービス連携に興味のある方の参考になれば幸いです。