Cognito でアプリケーションへのサインインを追加する

AWSBeginner
オンラインで実践に進む

はじめに

注文アプリケーションには、ユーザーをサインインさせる方法が必要です。このラボでは、ユーザーディレクトリと公開アプリクライアントを作成し、架空のユーザーでサインインします。発行されたトークンを検証し、セッションを更新してから後片付けをします。

最初に 注文 API を構築して検証する を完了してください。この新しい環境には、独自の公開バックエンド、使い捨てのサインイン入力、トークン検証の例が用意されています。パスワードとトークンのファイルは非公開にしてください。

認定試験との関連

認定資格 試験タスク 演習内容
Solutions Architect – Associate (SAA-C03) タスク 1.2 Cognito のサインイン、トークン検証、セッション更新。
Developer – Associate (DVA-C02) タスク 2.1 Cognito のサインイン、トークン検証、セッション更新。
Security – Specialty (SCS-C03) タスク 4.1 基礎演習:Cognito のサインイン、トークン検証、セッション更新。

ラボの概要

概念図:サインインクライアントは Cognito ユーザープールと公開アプリクライアントを使ってセッショントークンを取得、検証、更新します。独立した公開ヘルス API はサインインだけでは保護されません。

アプリケーションのユーザーディレクトリを作成する

このステップでは、ユーザープール、公開アプリクライアント、確認済みの架空のユーザーを用意します。

作業ディレクトリに移動し、umask 077 を使って、新しく作成するファイルを自分のユーザーだけがアクセスできるようにします。これはファイル権限に影響し、API の認可ルールには影響しません。

cd /home/labex/project
umask 077

Cognito ユーザープールは、アプリケーションのユーザーディレクトリです。そのユーザーはアプリケーションにサインインします。一方、準備済みの IAM CLI アイデンティティは、クラウドリソースを管理します。上部の AWS View を確認し、これらのサービスのリソースを比較してください。

所有するユーザープールを作成します。$(...) は、返されたプール ID を保存し、後続のサービスコマンドで使えるようにします。

POOL_ID=$(aws cognito-idp create-user-pool \
  --pool-name labex-a04-users \
  --query UserPool.Id \
  --output text)
echo "$POOL_ID"

その ID を記録します。> は、値をリソース一覧のファイルに書き込みます。

printf '%s\n' "$POOL_ID" > pool-id.txt

アプリクライアントは、このプールを通じてユーザーをサインインさせるアプリケーションを識別します。公開アプリケーションは、クライアントシークレットを秘密に保てません。シークレットなしでこのクライアントを作成し、この手順で使うユーザー名とパスワードのフロー、およびリフレッシュトークンのフローを許可します。

CLIENT_ID=$(aws cognito-idp create-user-pool-client \
  --user-pool-id "$POOL_ID" \
  --client-name orders-client \
  --no-generate-secret \
  --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH \
  --query UserPoolClient.ClientId \
  --output text)
echo "$CLIENT_ID"
printf '%s\n' "$CLIENT_ID" > client-id.txt

秘密の値を要求せずに、安全なクライアント設定を確認します。

aws cognito-idp describe-user-pool-client \
  --user-pool-id "$POOL_ID" \
  --client-id "$CLIENT_ID" \
  --query '{Name:UserPoolClient.ClientName,Flows:UserPoolClient.ExplicitAuthFlows}'

メール送信を抑制して、実演用ユーザーを作成します。これは管理者がユーザーを用意する操作であり、後でユーザーが行うサインインとは異なります。

aws cognito-idp admin-create-user \
  --user-pool-id "$POOL_ID" \
  --username labex-demo \
  --message-action SUPPRESS \
  --user-attributes Name=email,Value=demo@example.test \
  --query '{Username:User.Username,Status:User.UserStatus}'

初期状態は FORCE_CHANGE_PASSWORD です。この使い捨てのテストユーザーだけに対して、提供された恒久パスワードの入力ファイルを CLI で適用します。ファイルには Username、Password、Permanent フィールドがあり、file:// 構文でパスワードを表示せずに読み込めます。

aws cognito-idp admin-set-user-password \
  --user-pool-id "$POOL_ID" \
  --cli-input-json file://demo-password.json

安全なユーザー状態だけを確認します。

aws cognito-idp admin-get-user \
  --user-pool-id "$POOL_ID" \
  --username labex-demo \
  --query '{Username:Username,Status:UserStatus,Enabled:Enabled}'

labex-demo、CONFIRMED、true が返るはずです。AWS View で、所有するプール ID、orders-client、クライアントシークレットがないこと、確認済みで有効なユーザーを比較します。実際のサービスのプール、クライアント、ユーザーの状態の例です。

Cognito プール、公開アプリクライアント、確認済みの実演用ユーザー

無関係な labex-a04-reference プールは空のままです。ユーザートークンは、まだ要求していません。

サインインし、アプリケーションのアイデンティティを読み取る

このステップでは、確認済みユーザーの実際のトークンを取得し、誤ったパスワードの拒否を観察します。

USER_PASSWORD_AUTH を使う InitiateAuth は、アプリケーションのユーザー名とパスワードを認証します。CLI の IAM 管理者アイデンティティを通じて、ユーザーを認可するものではありません。提供された sign-in.json には、この実演の架空のユーザー名とパスワードのパラメータが含まれます。

AuthenticationResult だけを非公開のファイルに保存します。このレスポンスには Bearer トークンが含まれるため、cat で表示しないでください。

aws cognito-idp initiate-auth --client-id "$CLIENT_ID" --auth-flow USER_PASSWORD_AUTH --auth-parameters file://sign-in.json --query AuthenticationResult --output json > tokens.json

jq で、秘密ではないレスポンスのフィールドだけを表示します。トークンの存在を確認する式は、トークンの値ではなく真偽値を返します。

jq '{TokenType,ExpiresIn,TokensPresent:(has("AccessToken") and has("IdToken") and has("RefreshToken"))}' tokens.json

Bearer、正の有効期間、TokensPresent: true が返るはずです。アクセストークンは、適切なユーザーや API のリクエストを認可します。ID トークンは、アプリケーションにアイデンティティ情報を渡します。リフレッシュトークンは、新しいアクセストークンと ID トークンを取得できます。それぞれ目的が異なり、相互に置き換えられるものではありません。

アクセストークンと ID トークンを、受け渡し用の非公開ファイルに取り出します。jq -r は文字列をそのまま書き込み、リダイレクトによってターミナルに表示させません。

jq -r '.AccessToken' tokens.json > access-token.txt
jq -r '.IdToken' tokens.json > id-token.txt

GetUser は、アクセストークンで認証します。出力にはユーザー名だけを要求します。

aws cognito-idp get-user \
  --access-token "$(cat access-token.txt)" \
  --query Username \
  --output text

labex-demo が返るはずです。これは、発行されたトークンに対する実際の Cognito のレスポンスであり、ローカルの JSON ファイルだけから推測した証拠ではありません。

提供された誤ったパスワードのパラメータを試します。予期しないトークンレスポンスが返った場合も、別の非公開ファイルにリダイレクトします。

aws cognito-idp initiate-auth --client-id "$CLIENT_ID" --auth-flow USER_PASSWORD_AUTH --auth-parameters file://wrong-sign-in.json --query AuthenticationResult --output json > rejected-login.json

NotAuthorizedException: Incorrect username or password が返るはずです。このコマンドは、意図的にゼロ以外の終了コードで終了します。その正確な拒否を読んでから、次のステップに進んでください。接続や認証通信のエラーは、誤ったパスワードが拒否された証拠ではありません。この失敗の結果で、有効なトークンを上書きしないでください。このステップの自動チェックは、発行されたトークンと実際の GetUser の結果を独立して検証します。特定の誤ったパスワードのエラーは、手動で観察してください。

アプリクライアントと確認済みユーザーは同じプールに属し、サインインに成功するとトークンが返ります。

署名付きトークンを検証し、セッションを更新する

このステップでは、トークンの署名と意図された受信先を検証し、セッションを更新して、サインインと API ルートの保護を区別します。

JWT には、エンコードされたヘッダーとクレームのデータ、および署名が含まれます。デコードするだけでは、誰が発行したかは証明できません。JWKS は発行者の公開署名キーの集合です。RS256 は、それらの公開キーで検証できる RSA 署名を使います。

提供された通常のアプリケーションの検証例を読みます。パスワードやトークンのリテラルは含まれていません。

cat verify_tokens.py

この例は、準備済みの作業環境のエンドポイントからプールの公開 JWKS を取得し、RS256 とキー ID を検証してから、発行者、現在時刻、token_use、アプリクライアントを確認します。署名された発行者は、Cognito プールのアイデンティティを使います。アクセストークンは client_id でクライアントを識別し、ID トークンは aud を使います。保存したトークンファイルと、実際のプールおよびクライアントの ID で実行します。

/home/labex/project/client-venv/bin/python verify_tokens.py tokens.json "$POOL_ID" "$CLIENT_ID"

access と id の安全な概要が返り、どちらも RS256 と current_time_valid: true を示すはずです。クレームを受け入れる前に、メモリー内で署名の 1 バイトを変更して拒否を確認する処理も行います。改ざんしたトークンは保存しません。アクセストークンの aws.cognito.signin.user.admin スコープは、Cognito のユーザーセルフサービス操作を許可します。このスコープは、AWS 管理者のアクセス、注文の所有権ルール、独自の業務スコープではありません。

元のリフレッシュトークンから、非公開の更新パラメータを作成します。これはファイルに書き込み、値を公開しません。

jq '{REFRESH_TOKEN:.RefreshToken}' tokens.json > refresh-parameters.json

ユーザーのパスワードを再送せずに、新しいアクセストークンと ID トークンを取得します。

aws cognito-idp initiate-auth --client-id "$CLIENT_ID" --auth-flow REFRESH_TOKEN_AUTH --auth-parameters file://refresh-parameters.json --query AuthenticationResult --output json > refreshed.json

同じ公開キーとクレームのチェックで、更新したトークンを検証します。

/home/labex/project/client-venv/bin/python verify_tokens.py refreshed.json "$POOL_ID" "$CLIENT_ID"

同じプールとクライアントに対する、有効な access と id の概要が返るはずです。更新レスポンスに、別のリフレッシュトークンが含まれる必要はありません。削除するまでは、元の非公開レスポンスを保持してください。

サインインだけでは、準備済みの公開 API は変わりません。生成された ID を見つけ、Bearer トークンなしで公開のヘルスリクエストを送ります。

API_ID=$(aws apigatewayv2 get-apis \
  --query "Items[?Name=='labex-a04'].ApiId | [0]" \
  --output text)
echo "$API_ID"
printf '%s\n' "$API_ID" > api-id.txt
curl -i "http://127.0.0.1:8081/api/$API_ID/health"

HTTP 200 と healthy: true が返るはずです。AWS View には、すべてのルートで認可が NONE、注文アイテムがない状態が引き続き表示されます。ユーザー認証は成功しましたが、API はまだトークンを要求していません。次の単元で、JWT によるルート認可を追加します。有効なトークンがあっても、特定の注文の所有権が自動的に確立されるわけではありません。

更新は、同じアプリクライアントのログインセッションを継続します。API ルートの認可設定を変更したり、注文へのアクセス権を与えたりする操作ではありません。更新後の ID 概要と公開のままのヘルスレスポンスを比較するときは、これらの判断を分けて考えてください。

サインインと API のリソースを削除する

このステップでは、無関係な参照プール、テーブル、ログを保持しながら、所有するサインインリソースと、バックエンドの準備用リソースを削除します。

リソース一覧は、pool-id.txt の labex-a04-users、client-id.txt の orders-client、labex-demo ユーザー、api-id.txt の API ID、labex-a04-orders-api、その関数ロググループ、labex-a04-orders、ロールの OrdersData ポリシーです。これらの名前付きリソースだけを削除してください。

架空のユーザーを削除し、その後、アプリクライアントと所有するプールを削除します。

aws cognito-idp admin-delete-user --user-pool-id "$POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client --user-pool-id "$POOL_ID" --client-id "$CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$POOL_ID"

準備済みの動作する API と、その所有するバックエンドリソースを削除します。

aws apigatewayv2 delete-api --api-id "$API_ID"
aws lambda delete-function --function-name labex-a04-orders-api
aws logs delete-log-group --log-group-name /aws/lambda/labex-a04-orders-api
aws dynamodb delete-table \
  --table-name labex-a04-orders \
  --query TableDescription.TableName \
  --output text
aws iam delete-role-policy --role-name labex-a04-execution --policy-name OrdersData

サービスから正常に一覧をクエリします。通信の失敗は、不在の証明にはなりません。

aws cognito-idp list-user-pools --max-results 60 --query 'UserPools[].Name'
aws apigatewayv2 get-apis --query 'Items[].Name'
aws lambda list-functions --query 'Functions[].FunctionName'
aws dynamodb list-tables --query TableNames

参照プールと参照テーブルだけが残り、API と関数のリストは空になるはずです。AWS View も同じ状態を示し、参照アイテムと INFO platform reference keep unchanged を保持していなければなりません。準備済みの実行ロールには、FunctionLogs が残ります。

提供された架空のパスワード入力も含め、自分の非公開の認証ファイルを削除します。rm -f は、明示的に列挙したローカルファイルだけを削除します。

rm -f tokens.json access-token.txt id-token.txt refreshed.json refresh-parameters.json rejected-login.json sign-in.json wrong-sign-in.json demo-password.json

ローカルファイルの削除だけでは、サーバー側のセッションは失効しません。ここでは、架空のユーザー、クライアント、プールも削除しました。このラボは、一般的な JWT のログアウトや失効の仕組みを実装したとは主張しません。制作者による後片付けと VM の終了まで、無関係な参照リソースを保持してください。

まとめ

アプリケーションのユーザーディレクトリと公開アプリクライアントを作成し、架空のユーザーを用意してサインインし、誤ったパスワードの拒否を観察しました。実際の RS256 署名と、発行者、クライアント、時刻、種類のクレームを検証し、実際のユーザーアイデンティティを読み取り、アクセストークンと ID トークンを更新しました。Cognito ユーザーと IAM アイデンティティ、サインインと API の認可を区別し、最後に、参照データを保持しながら、所有するリソースと非公開ファイルを削除しました。

詳しくは、AWS 公式ドキュメントを参照してください:ユーザープールトークンの用途; JWT 署名とクレームの検証; セッション更新とリフレッシュトークンフロー。