複数の S3 バケットを跨いでも Cognito の認証状態は維持されるのか教えてください
この記事はアノテーション株式会社 AWS Technical Support Advent Calendar 2022 | Advent Calendar 2022 - Qiita 16日目の記事です。
困っていた内容
各画像(A, B)が異なる S3 バケットに保管されている状況下で下記手順のように、② の Web ページを仲介して S3 バケットを跨ぐ形で各画像にアクセスを行っても、Cognito ユーザープールで認証された状態は維持されるのか教えて下さい。
- S3 バケット A
- 画像 A が保管されている
- S3 バケット B
- 画像 B が保管されている
- 手順
- Cognito ユーザープールに対して認証を行います
- 認証成功後、画像 A と画像 B のリンクが表示された Web ページに遷移する
- 2.の Web ページで画像 A のリンクをクリックして、画像 A を表示させる
- 3.と同じ様に 2.の Web ページで画像 B のリンクをクリックして、画像 B を表示させる
Cognito の認証された状態は維持されるの?
はい、S3 バケットを跨いでのアクセスには関係なく、JSON Web トークン (JWT)の有効期限内は Amazon Cognito によって認証された状態となり、トークン の有効期限内は維持(保持)される仕組みとなっています。
Cognito ユーザープールに認証された状態とは?
Cognito ユーザープールに対して、ユーザーが正常に認証されると AWS 認証情報と交換することができる JSON Web トークン (JWT) を Amazon Cognito が発行します。
Amazon Cognito user pools - Amazon Cognito
ユーザーが正常に認証されると、ユーザー独自の API へのアクセスをセキュア化して承認するために使用する、または AWS 認証情報と交換することができる JSON Web トークン (JWT) を Amazon Cognito が発行します。
ユーザーはこの発行されたトークンを使用することで、自身が Cognito ユーザープールで認証された状態であることを示すとともに、各 AWS サービスのリソースにアクセスするために必要な一時的な AWS 認証情報と交換します。
サインイン後に ID プールを使用して AWS サービスへアクセスする - Amazon Cognito
ユーザーがユーザープールを使用してサインインし、その後 ID プールを使用して AWS のサービスにアクセスできるようにすることが可能です。
認証が正常に行われると、ウェブまたはモバイルアプリが Amazon Cognito からユーザープールトークンを受け取ります。
これらのトークンを使用して、アプリケーションが AWS のその他サービスにアクセスできるようにする AWS 認証情報を取得できます。
また、各 AWS サービスのアクセス先のリソースでは、この発行されたトークンを検証することで、その有効性を確認します。
JSON web トークンの検証 - Amazon Cognito
JWT クレームを検証する
- トークンの有効期限が切れていないことを確認します。
ID トークンの aud クレームとアクセストークンの client_id クレームは、Amazon Cognito ユーザープールで作成されたアプリクライアント ID と一致する必要があります。
発行者 (iss) のクレームは、ユーザープールと一致する必要があります。例えば、us-east-1 リージョンで作成されたユーザープールには、以下の iss 値があります。https://cognito-idp.us-east-1.amazonaws.com/.
token_use クレームをチェックします。
・Web API オペレーションでアクセストークンのみを受け入れている場合は、その値を access にする必要があります。
・ID トークンのみを使用している場合、その値は id にする必要があります。
・ID とアクセストークンのいずれも使用している場合、token_use クレームは、id または access になります。
以上より、Cognito ユーザープールに認証された状態とは、Amazon Cognito によって発行されたトークンが有効である状態を示します。
検証して確認してみた
上記の状況を再現して、S3 バケットを跨いでのアクセスには関係なく、JSON Web トークン (JWT)の有効期限内は Amazon Cognito によって認証された状態となり、トークン の有効期限内は維持されるのか実際に検証して確認してみます。
検証に必要な各 AWS リソース等を下記の順番で準備・作成していきます。
- Amazon S3
- Amazon Cognito
- AWS IAM
- AWS Cloud9
Amazon S3
バケットの作成
検証用に子猫の画像を格納する「test-bucket-cat」と、子犬の画像を格納する「test-bucket-dog」をバケット名とする、2 つの S3 バケットを作成します。
以降では子猫の画像を格納する S3 バケット(test-bucket-cat)の作成および各種設定手順を例に説明します。
- バケット名(命名要件に従い任意の名前を入力してください)
- “test-bucket-cat” or “test-bucket-dog”
- AWS リージョン
- アジアパシフィック(東京)ap-northeast-1
- オブジェクト所有者
- ACL 無効(推奨)
- このバケットのブロックパブリックアクセス設定
- パブリックアクセスをすべて ブロック にチェック
- バケットのバージョニング
- 無効にする
- タグ
- 設定なし
- デフォルトの暗号化
- サーバー側の暗号化 → 無効にする
- 詳細設定
- オブジェクトロック → 無効にする
Cross-Origin Resource Sharing (CORS)の設定
2つの S3 バケットに保管された画像に確認用 Web ページ からアクセスする際に必要となる CORS の設定を行います。
- Amazon S3 > バケット > [作成したバケット名] の順にアクセス
- [アクセス許可]のタブを選択
- 画面最下部の[Cross-Origin Resource Sharing (CORS)] > 編集 を押下
- 下記の CORS の設定をコピーして、編集エリアに貼り付けて、[変更の保存]を押下
[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET", "HEAD" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": [] } ]
アクセスポイントの作成
確認用 Web ページのプログラムから、S3 バケットに対する「listObjects」メソッドと「getObject」メソッドの引数として指定するため、アクセスポイントを作成します。
- アクセスポイント名(命名要件に従い任意の名前を入力してください)
- test-bucket-cat バケット: cat
- test-bucket-dog バケット: dog
- ネットワークオリジン
- インターネット
- [アクセスポイントの作成]を押下
作成した S3 バケットに画像を格納
検証確認用の画像を作成した S3 バケットに保管します。
今回はフリー画像素材の pixabay から2つの画像(子猫と子犬)をダウンロードして使用したいと思います。
- Amazon S3 > バケット > test-bucket-cat の順にアクセス
- [オブジェクト]タブを選択して、ドラッグアンドドロップで画像を格納します(今回は検証確認で分かりやすくするため以下のように画像を各 S3 バケットに保管しています)
- "test-bucket-cat" バケットには「子猫」の画像
- "test-bucket-dog" バケットには「子犬」の画像
- アップロード画面に自動で遷移されましたら、アップロードした画像を確認して問題なければ画面右下の[アップロード]を押下
Amazon Cognito
今回の検証用で使用する Cognito ユーザープールと ID プールを作成します。
ユーザープールの作成
- サインインエクスペリエンスを設定
- プロバイダーのタイプ
- Cognito ユーザープール
- Cognito ユーザープールのサインインオプション
- E メール
- プロバイダーのタイプ
- セキュリティ要件を設定
- パスワードポリシー
- パスワードポリシーモード
- Cognito のデフォルト
- パスワードポリシーモード
- 多要素認証
- MFA なし
- パスワードポリシー
- ユーザーアカウントの復旧
- デフォルト設定のままでOK
- サインアップエクスペリエンスを設定
- デフォルト設定のままでOK
- メッセージ配信を設定
- Cognito で E メールを送信
- アプリケーションを統合
- ユーザープール名(命名要件に従い任意の名前を入力してください)
- TestUserPool
- ホストされた認証ページ
- Cognito のホストされた UI を使用(今回の検証では使用しません : チェック不要)
- ユーザープール名(命名要件に従い任意の名前を入力してください)
- 最初のアプリケーションクライアント
- アプリケーションタイプ
- パブリッククライアント
- アプリケーションクライアント名(命名要件に従い任意の名前を入力してください)
- TestAppClient
- クライアントのシークレット
- クライアントのシークレットを生成しない
- アプリケーションタイプ
- 高度なアプリケーションクライアントの設定
- 認証フロー
- ALLOW_REFRESH_TOKEN_AUTH(デフォルト設定)
- ALLOW_USER_SRP_AUTH
- 認証フローセッション持続期間
- 3 分(デフォルト設定)
- 更新トークンの有効期限
- 30 日(デフォルト設定)
- アクセストークンの有効期限
- 5 分
- ID トークンの有効期限
- 5 分
- 高度なセキュリティ設定 - オプション
- デフォルト設定のままでOK
- 認証フロー
- 属性の読み取りおよび書き込み許可
- デフォルト設定のままでOK
ID プールの作成
- 新しい ID プールの作成
- ID プール名(命名要件に従い任意の名前を入力してください)
- TestIdPool
- ID プール名(命名要件に従い任意の名前を入力してください)
- 認証されていない ID
- 今回の検証では使用しません(チェック不要)
- 認証フローの設定(基本 (クラシック) フローを許可する)
- 今回の検証では使用しません(チェック不要)
- 認証プロバイダー
- [Cognito] のタブを選択
- ユーザープール ID
- ap-northeast-1_*********
- アプリクライアント ID
- Amazon Cognito > ユーザープール > [作成したユーザープール] > [アプリケーションの統合]タブを選択
- 画面最下部の [アプリクライアントと分析] の クライアントID を指定
- Amazon Cognito > ユーザープール > [作成したユーザープール] > [アプリケーションの統合]タブを選択
- ユーザープール ID
- [Cognito] のタブを選択
- Identify the IAM roles to use with your new identity pool
- Cognito ユーザープールに対して認証されたユーザーに対する IAM ロールは後ほど設定しますので、右下の[許可]を選択でOKです
- IdentityPoolId は後ほど使用するのでメモする等控えておいてください
AWS IAM
Cognito_[作成した ID プール名]Auth_Role の編集
- IAM > ロール > Cognito_[作成したID プール名]Auth_Role に移動する
- 該当のポリシー名付近の+ボタンをクリックして展開後、[編集]を押下
- [JSON]タブを選択して、下記の IAM ポリシーをコピーして、編集エリアに貼り付けて、[ポリシーの確認]を押下
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:*" ], "Resource": [ "*" ] } ] }
- [変更の保存]を押下
AWS Cloud9
Cloud9 環境を作成
- Details
- Name(命名要件に従い任意の名前を入力してください)
- Environment type
- New EC2 instance(デフォルト設定)
- New EC2 instance
- Instance type
- t3.small (2 GiB GiB RAM + 2 vCPU)
- Platform
- Amazon Linux 2
- Timeout
- 30 minutes
- Instance type
- Network settings
- デフォルト設定のままでOK
- Tags
- 設定不要
検証用ソースコードの準備
- 検証ソースコードのダウンロード
- 今回の検証用のソースコードはこちらの記事を参考にさせていただきました
git clone https://github.com/junjis0203/restricted-s3-access-example.git
- 検証用にソースコードを編集
- 今回の検証用のため、ソースコードを一部下記のように書き換えます
config.js
- 下記の例を参考に各変数にリソースID(ARN)等の値(文字列)を設定する
// リージョン const REGION = 'ap-northeast-1' // S3 - バケットアクセスポイントのARN const S3_BUCKET_CAT = 'arn:aws:s3:ap-northeast-1:012345678912:accesspoint/cat' const S3_BUCKET_DOG = 'arn:aws:s3:ap-northeast-1:012345678912:accesspoint/dog' // Cognito - ユーザープール ID、クライアント ID、ID プールの ID const USER_POOL_ID = 'ap-northeast-1_*********' const APP_CLIENT_ID = '1a2bc3d45efg6hijkl7m8no9pq' const IDENTITY_POOL_ID = 'ap-northeast-1:1a234bc5-67d8-9ef0-g1hi-2jk3lmn4567o'
index.html
- 今回の検証用に一部変更や追記した下記ソースコードをコピーしてご活用ください
<html> <head> </head> <body> <div id="app"> </div> <script src="https://sdk.amazonaws.com/js/aws-sdk-2.734.0.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/amazon-cognito-identity.min.js"></script> <script src="config.js"></script> <script src="common.js"></script> <script> const cognitoUser = userPool.getCurrentUser(); if (!cognitoUser) { document.location.href = 'signin.html' } cognitoUser.getSession(function(err, result) { if (err) { console.error(err); return } AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: IDENTITY_POOL_ID, Logins: { [LOGINS_KEY]: result.getIdToken().getJwtToken() }, }) AWS.config.credentials.refresh(function(err) { if (err) { console.error(err) return } console.log('Refreshing AWS credentials is succeed') getImagesCat() getImagesDog() }) }) const s3 = new AWS.S3() function getImagesCat() { s3.listObjects({Bucket: S3_BUCKET_CAT}, function(err, data) { if (err) { console.error(err) return } const ul = document.createElement('ul') data.Contents.forEach(function(content) { if (content.Key.endsWith('.jpg')) { const li = document.createElement('li') const a = document.createElement('a') const br = document.createElement('br') const img = document.createElement('img') a.href = '#' a.innerHTML = content.Key a.addEventListener('click', function(e) { e.preventDefault() s3.getObject({Bucket: S3_BUCKET_CAT, Key: content.Key}, function(err, data) { if (err) { console.error(err) return } console.log(data) const blob = new Blob([data.Body], {type: data.ContentType}) img.src = URL.createObjectURL(blob) }) }) li.appendChild(a) li.appendChild(br) li.appendChild(img) ul.appendChild(li) } }) const app = document.getElementById('app') app.appendChild(ul) }) } function getImagesDog() { s3.listObjects({Bucket: S3_BUCKET_DOG}, function(err, data) { if (err) { console.error(err) return } const ul = document.createElement('ul') data.Contents.forEach(function(content) { if (content.Key.endsWith('.jpg')) { const li = document.createElement('li') const a = document.createElement('a') const br = document.createElement('br') const img = document.createElement('img') a.href = '#' a.innerHTML = content.Key a.addEventListener('click', function(e) { e.preventDefault() s3.getObject({Bucket: S3_BUCKET_DOG, Key: content.Key}, function(err, data) { if (err) { console.error(err) return } console.log(data) const blob = new Blob([data.Body], {type: data.ContentType}) img.src = URL.createObjectURL(blob) }) }) li.appendChild(a) li.appendChild(br) li.appendChild(img) ul.appendChild(li) } }) const app = document.getElementById('app') app.appendChild(ul) }) } </script> </body> </html>
Cognito ユーザープールに対して認証を行う検証用ユーザーの作成
- 検証用ユーザーを作成するシェルスクリプトファイルの作成および編集
$ touch create-testuser.sh $ vi create-testuser.sh #(もしくは、Cloud9のエディタでファイルを開いて編集)
- create-testuser.sh に以下を記述して保存
USER_POOL_ID=ap-northeast-1_********* USER_NAME=<メールアドレス> MAIL=<USER_NAMEと同じメールアドレス> $ aws cognito-idp admin-create-user \ --user-pool-id ${USER_POOL_ID} \ --username ${USER_NAME} \ --temporary-password <任意の一時的なパスワードを入力> \ --user-attributes \ Name=email,Value=${MAIL} \ Name=email_verified,Value=True
- create-testuser.sh を編集後、以下を実施
$ chmod +x create-testuser.sh $ ./create-testuser.sh { "User": { "Username": "a123bc45-abc1-1234-5678-12ab345cd6ef", "Enabled": true, "UserStatus": "FORCE_CHANGE_PASSWORD", "UserCreateDate": **********.***, "UserLastModifiedDate": **********.***, "Attributes": [ { "Name": "sub", "Value": "a123bc45-abc1-1234-5678-12ab345cd6ef" }, { "Name": "email_verified", "Value": "True" }, { "Name": "email", "Value": "[email protected]" } ] } }
検証
[Preview] をより確認
- Cloud9 より index.html を右クリックして、[Preview] を選択
ログイン
- Email と Password を入力して、[sign in] をクリック
- New Password が表示されるため、新しいパスワードを入力して、[change password] をクリック
認証成功後
- 認証が成功すると、各バケットに保管されている画像へのリンクが表示されます
- Google Chrome デベロッパー ツールにおいて、ローカルストレージ内に、各種 JSON Web トークン (JWT)が取得できていることも確認できます
リンクをクリックして、異なる S3 バケットに保管されている画像を確認用 Web ページから表示してみる
- 子猫の画像
- 子犬の画像
JSON Web トークン (JWT)の有効期限が切れた場合の挙動確認
今回の検証では、JSON Web トークン (JWT)における、ID トークン(および、アクセストークン)の有効期限を 5 分に設定しているため、5分を経過した場合にどのような挙動になるか確認してみました。
1. トークンの有効期限が切れたとき - 画面の表示
- 以下のとおり、画面には何も表示されなくなります
2. トークンの有効期限が切れたとき - コンソールログ
- 以下のとおり、”CredentialsError” が記録されていることを確認しました
CredentialsError: Missing credentials in config, if using AWS_CONFIG_FILE, set AWS_SDK_LOAD_CONFIG=1
【補足】 ”CredentialsError” 後に、S3 バケットに格納している画像のリンクを再度表示させるには・・・
- Cloud9の [Preview] を ”Refresh” すると、ローカルストレージに保管されている 更新トークン(リフレッシュトークン)を使用して再度新しい ID トークンとアクセストークンを取得することができます
- その後、各 S3 バケットに保管されている画像へのリンクが再び表示されます
おまけ
JSON Web トークン (JWT) を検証するソースコードは GitHub - awslabs/aws-jwt-verify: JS library for verifying JWTs signed by Amazon Cognito, and any OIDC-compatible IDP that signs JWTs with RS256, RS384, and RS512 より、Amazon Cognito によって署名された JWT を検証するための JavaScript ライブラリが公開されています。
今回の検証で Amazon Cognito ユーザープールから取得した JSON Web トークン (JWT) を使用して、Cloud9 上から実行して確認してみました。
ソースコード - aws-jwt-verify.mjs
import { CognitoJwtVerifier } from "aws-jwt-verify"; // Verifier that expects valid access tokens: const verifier = CognitoJwtVerifier.create({ userPoolId: "ap-northeast-1_*********", tokenUse: "id", clientId: "1a2bc3d45efg6hijkl7m8no9pq", }); try { const payload = await verifier.verify( "[ID トークンを入力してください]" ); console.log("Token is valid. Payload:", payload); } catch { console.log("Token not valid!"); }
実行例 - 有効期限内の ID トークンの場合
$ node aws-jwt-verify.mjs Token is valid. Payload: { sub: '1234abcd-123a-1234-abcd-abcd1234abcd', email_verified: true, iss: 'https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_*********', 'cognito:username': '1234abcd-123a-1234-abcd-abcd1234abcd', origin_jti: '5678abcd-567a-5678-abcd-abcd5678abcd', aud: '1a2bc3d45efg6hijkl7m8no9pq', event_id: '1256abcd-125a-1256-abcd-abcd1256abcd', token_use: 'id', auth_time: **********, exp: **********, iat: **********, jti: 'ab1234dd-rta1-45f1-kd9d-89dsd3bjs9np', email: '[email protected]' }
実行例 - 有効期限切れの ID トークンの場合
$ node aws-jwt-verify.mjs Token not valid!
まとめ
- S3 バケットを跨いでのアクセスには関係なく、JSON Web トークン (JWT)の有効期限内は Amazon Cognito(ユーザープール) によって認証された状態となり、トークンの有効期限内は維持(保持)されること
- Amazon Cognito(ユーザープール)に認証された状態とは、Amazon Cognito(ユーザープール)によって発行されたトークンが有効である状態を示すこと
この記事が少しでも誰かのお役にたてば幸いです。
参考資料
-
Cross−Origin Resource Sharing (CORS) の使用 - Amazon Simple Storage Service
-
Amazon S3 アクセスポイントを使用したデータアクセスの管理 - Amazon Simple Storage Service
-
Class: AWS.CognitoIdentityServiceProvider — AWS SDK for JavaScript
-
GitHub - aws/aws-sdk-js: AWS SDK for JavaScript in the browser and Node.js
-
amplify-js/packages/amazon-cognito-identity-js at main · aws-amplify/amplify-js · GitHub