- 「auth」は実は3つの役割
- サインイン後にCognitoが返すもの
- サービスがCognitoに問い合わせずにトークンを信頼できる仕組み
- ユーザーのアイデンティティとワークロードのアイデンティティ
- StrobeアプリにおけるCognitoの位置づけ
- 参考資料
基本的なルールはこうです。アイデンティティは検証済みのトークンから得るものであり、クライアントやモデルの言い分から得るものでは決してありません。Cognitoはユーザーが誰なのかを一度だけ証明します。その後は、各サービスが署名付きトークンを自分自身でチェックします。そして、そこから取り出したsubクレームだけが、リクエストが誰のデータに触れられるかを決めます。
本稿では基礎に絞って、Cognitoが何を発行し、サービスがそれをどのようにチェックするのかを扱います。Strobe Assistant自体の中でトークンがどのように流れるのかは、パート4で追いかけます。
「auth」は実は3つの役割
私は、別々の3つの役割をまとめて「auth」と呼びがちだと気づきました。これらを分けて考えると、アーキテクチャ全体がずっと理解しやすくなります:
| 問い | 名前 | 誰が答えるか | Strobeでは |
|---|---|---|---|
| あなたは誰? | 認証(Authentication) | Cognitoが一度だけ、パスワードを使って | A1の認証Lambda → Cognito InitiateAuth |
| このトークンは本物で、有効期限内か? | 検証(Verification) | 各サービスがそれぞれ、公開鍵を使って | API GatewayのJWTオーソライザー、auth.ts、FastMCPのJWTVerifier |
| このユーザーは何に触れてよいか? | 認可(Authorization) | アプリ自身のコードが、検証済みのsubを使って | filter={"userId": sub}、post.userId == sub |
Cognitoが担うのは1つ目の役割だけです。Cognitoは投稿や写真やベクトルについては何も知らないので、認可は常にアプリ自身の役割です。
サインイン後にCognitoが返すもの
ユーザープールは、ユーザーディレクトリとトークン発行者を兼ねたものです。サインインに成功すると、3つのトークンを返します:
- IDトークン:ユーザーが誰なのか(
sub、email、cognito:username)と、どのアプリクライアント向けに発行されたのか(aud)。 - アクセストークン:アプリが何を呼び出してよいか(
scope)。audの代わりにclient_idを持ちます。 - リフレッシュトークン:新しいIDトークンとアクセストークンに交換するための、有効期間の長いトークン(デフォルトは30日)。検証できるJWTではありません。暗号化されていてCognitoだけが読むことができ、Cognitoは取り消すことができます。
IDトークンとアクセストークンは**JWT**(JSON Web Token)です。base64urlでエンコードされた3つの部分をドットでつないだもので、header.payload.signatureという形をしています。
StrobeがアクセストークンではなくIDトークンを使う理由。 Strobe Assistantの検証側(エージェントのauth.tsとMCPサーバーのJWTVerifier)は、audをアプリクライアントIDと照合します。Cognitoのアクセストークンにはaudクレームがないため、このチェックは設計上アクセストークンを拒否します。また、IDトークンにはエージェントが表示名に使うユーザー情報も含まれています。どちらのファイルもこのことをdocstringで説明しています。両方がトークンを検証する理由はパート3で解説しています。
サービスがCognitoに問い合わせずにトークンを信頼できる仕組み
Cognitoはすべてのトークンを、Cognitoの外に決して出ない秘密鍵で署名し、対応する公開鍵をJWKS(JSON Web Key Set)として既知のURLで公開しています:
https://cognito-idp.ap-southeast-2.amazonaws.com/<poolId>/.well-known/jwks.json
これらの鍵があれば、サービスはリクエストのたびにCognitoを呼び出すことなく、ローカルでトークンを検証できます:
- ヘッダーから
kidを読み取り、JWKSから一致する公開鍵を選びます(一度ダウンロードした後はキャッシュします)。 - RS256署名をチェックします。ヘッダーやペイロードが1バイトでも変わっていれば失敗します。
issは自分のプールのURLでなければなりません。これにより、クラスメイトのものなど、他のプールのトークンを拒否します。audは自分のアプリクライアントIDでなければなりません。これにより、別のアプリ向けに発行されたトークンを拒否します。expは未来の時刻でなければなりません。token_useは"id"でなければなりません(エージェントが使うaws-jwt-verifyはこれもチェックします)。
期待される値(プールのURL、クライアントID、鍵)はすべて事前にわかっているため、リクエストごとにCognitoを呼び出す必要はありません。すべてのチェックに通れば、クレームは事実として扱うことができ、subをユーザーのアイデンティティとして安全に使えます。
署名が証明するのは、トークンが本物であり改ざんされていないということであって、中身を隠すわけではない点に注意してください。この設計には、覚えておくべき2つの帰結があります:
- 読める ≠ 信頼できる。 base64urlは暗号化ではなくエンコードなので、トークンを持っている人なら誰でもペイロードを読めます。クレームに秘密情報を入れてはいけません。Strobe AssistantのWebUIは、名前を表示するためだけにトークンを自分でデコードしており(
main.tsのtokenLabel())、何も検証していません。表示にはそれで問題ありませんが、判断に使ってはいけません。 - ベアラー = 持っている人。 有効なトークンは、
expまでは持っている人なら誰でも使えます。そのため、トークンはTLS上でのみ送り、URLに入れることは決してせず(URLに入れるとログやブラウザ履歴に残ります)、有効期間を短く保つ必要があります。さらに、リクエストごとにCognitoに問い合わせる人はいないので、サービスはサインアウトも知ることができません。リフレッシュトークンを取り消しても、その影響は届きません。AWSも、取り消されたトークンであっても、署名と有効期限だけをチェックするライブラリは通過してしまうと注意しています。だからこそ、IDトークンとアクセストークンは有効期間が短く(Cognitoのデフォルトは60分)、取り消し可能なのはリフレッシュトークンの方なのです。
ユーザーのアイデンティティとワークロードのアイデンティティ
このシステムには2種類のアイデンティティがあり、混同してはいけません:
- ユーザーのアイデンティティ:CognitoのJWT。 *このリクエストは誰のデータのためのものか?*に答えます。
- ワークロードのアイデンティティ:IAMロール。 *どのコンポーネントがAWSを呼び出しているのか?*に答えます。エージェントとMCPサーバーのECSタスクロール、そしてLambdaの実行ロールが、すべてのAWS API呼び出しに署名します(SigV4)。
Strobeでは、ユーザーのトークンでAWSが使えるようになることは決してありません。Bedrock、S3 Vectors、DynamoDBにはIAMロールでアクセスします。トークンが決めるのは、それらの呼び出しがどの行を求めるかだけです。
StrobeアプリにおけるCognitoの位置づけ
Assessment 1(Strobe Assistantが拡張している写真共有アプリ。シリーズの紹介記事を参照)では、Cognitoは薄い認証Lambdaの背後にあり、API Gatewayの**JWTオーソライザー**がすべての保護されたルートを守っています。
サインインはブラウザから直接Cognitoに行くのではなく、Lambdaを経由します。 アプリクライアントはコンフィデンシャルクライアントで、クライアントシークレットを持っており、InitiateAuthを呼び出すたびに、そこから計算したシークレットハッシュが必要です。ブラウザは秘密を保持できないため、POST /v1/auth/login(公開ルート)が認証Lambdaを呼び出し、認証Lambdaがサーバー側でCognitoを呼び出して{ user, token }を返します。このtokenがIDトークンです。
保護されたルートは、コードが動く前にチェックされます。 HTTP APIのJWTオーソライザーは、署名、iss、オーディエンス(aud、アクセストークンの場合はclient_id)、expをプールに照らして検証します。不正なトークンには401が返り、Lambdaは呼び出されません。そのため呼び出しの料金はかからず、チェックを忘れるコードパスもあり得ません。Lambdaはイベントの中で検証済みのクレームを受け取ります。以下はHTTP APIのイベントでの形です。A1のLambdaはA2のリポジトリにはないため、あくまで例示です:
claims = event["requestContext"]["authorizer"]["jwt"]["claims"]
user_id = claims["sub"] # never read the user id from the request body
subは2つの場所でルールになります:
- 所有者チェック。 たとえば、投稿を編集するには
post.userId == subである必要があり、そうでなければ403になります。 - ストレージのキー。 アップロードLambdaは、キー
{sub}/{postId}/{fileId}に対するPUTの署名付きURLを発行します。{sub}の部分をクライアントが選ぶことはありません。A2の取り込みワーカーはこのプレフィックスを信頼するため、これは後で重要になります(パート4を参照)。
Cognitoにはモデレーターグループもあります。グループへの所属は、IDトークンの中でcognito:groupsとして現れます。
参考資料
- Understanding user pool JSON web tokens (JWTs) — Cognitoの3つのトークンと、それぞれの用途。
- Verifying JSON web tokens — Cognito自身によるチェックリスト。JWKSのURL、
iss、aud/client_id、token_use、exp。 - Ending user sessions with token revocation — 取り消しで何が起きるのか、そしてローカルで検証されるトークンが有効期限まで取り消しの影響を受けない理由。
- aws-jwt-verify — CognitoのJWTを検証するためのAWS製JavaScriptライブラリ。エージェントで使っています。
- Control access to HTTP APIs with JWT authorizers in API Gateway — オーソライザーが検証する内容。
audからclient_idにフォールバックする条件も含みます。 - How to secure API Gateway HTTP endpoints with JWT authorizer(AWS Security Blog) — Cognitoユーザープールを使った手順解説。
- JSON Web Token Introduction(jwt.io) — JWTの構造をわかりやすく紹介する入門。サイトのデバッガーはトークンのデコードに便利ですが、本番のトークンではなくテスト用のトークンを使ってください。
- RFC 7519: JSON Web TokenとRFC 6750: Bearer Token Usage — JWTとベアラートークンの仕様。