5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude CodeからAgentCore GatewayにEntra ID × CognitoでSSO接続するついでに認証認可の勉強をする

5
Posted at

調査・下書きにClaude Codeを使用し、最終的に人間がすべて確認しました。
問題等あればご指摘ください。

はじめに

こんにちは、ふくちと申します。
皆様AgentCore Gateway使ってらっしゃいますでしょうか。
私は最近、組織向けMCPサーバーをAgentCore Gatewayを通して提供する機会があったのですが、その認証をどうするか悩んでいました。

個人アクセストークンや長期APIキーを配るのではなく、会社のSSOを通してログインできる人だけがAgentCore Gateway経由でMCPサーバーを使えるようにしたい、というのが理想でした。

そこでこの記事では、Claude Code -> AgentCore Gatewayの認証を、Entra ID × Amazon Cognito × AgentCore GatewayのJWT認証でつなぐ構成をまとめてみました。

結論

ユーザーは会社のSSOでログインし、ログイン結果を基に発行された署名付きトークンを使ってAgentCore Gatewayへ接続します。
Claude Codeに長期APIキーやAWSの認証情報を設定する必要がないのでとってもセキュア!

まずざっくり概要となる構成図としてはこんな感じ。

用語解説

認証に登場する要素を、次の3層へ分けて順番に解説します。私も認証認可初心者なので、間違いなどがあればご指摘ください。

  1. 標準規格
  2. AWS・Microsoftのマネージドサービス
  3. この構成のために自作するもの

1. 標準規格

OAuth 2.0

OAuth 2.0は、限定されたアクセス権をアプリケーションへ渡すための標準規格です。ユーザーのパスワードそのものはアプリケーションへ渡さず、許可された操作を表すトークンを渡す形です。

イメージとしてはGoogle Driveへの画像アップロードアプリみたいなものを想像していただけると良いでしょうか。

これを本構成に置き換えるとこのような役割分担になります。

役割 意味 本構成での担当
リソースオーナー アクセスを許可する本人 ユーザー
クライアント トークンを取得して保護対象へアクセスする
アプリケーション
Claude Code
認可サーバー ユーザーの許可を確認し、トークンを発行するサーバー Cognito
リソースサーバー トークンを検証し、保護対象へのアクセスを許可するサーバー AgentCore Gateway

注意点としては、OAuthは認証の仕組みではなく認可の仕組みです。
これで提供されるのは操作の許可であり、「Googleでログイン」みたいな画面そのものではありません。

OIDC(OpenID Connect)

OIDCは、OAuth 2.0へログインしたユーザーの識別情報を追加する標準規格です。
こちらが前述した「Googleでログイン」みたいな機能を提供しているものになります。
image.png

これを使うとそのサービスのユーザーとしてあなたのGoogleアカウントが登録されますよね。これがOIDCです。

OAuth 2.0はアクセス権(認可)を扱い、OIDCはOAuthに誰がログインしたか(認証)を追加する、と考えると整理しやすくなるでしょうか。

JWT(JSON Web Token)とクレーム

JWTは署名を付けられるJSON形式のトークンです。
トークンの種類や署名方式を示すヘッダー、クレームを格納するペイロード、改ざんを検知する署名の3部分で構成されます。

クレームは、JWTのペイロードに入る1項目です。
例えば発行者、主題、有効期限、スコープ(許可されたアクセス範囲)、所属グループなどがクレームとして入ります。

なんかたまにiss, sub, aud, expみたいな3文字の略語出てきてよくわからん!となったことないですか?私はあります。それがどうやらこのクレームというやつらしいです。

  • iss(Issuer): JWT発行者
    • この構成ではCognitoユーザープールのURL
  • aud(Audience): JWTの受け取り手(どのアプリなのか)
    • この構成ではAgentCore Gateway
    • AgentCore Gateway側では、自分向けに発行されたトークン化を確認する
  • sub(Subject): ユーザーの一意識別子
  • exp(Expiration Time): JWTの有効期限

などが代表的なものみたいです。3文字にしてあるのは、JWTをコンパクトにすることが目的だとか。

また上記で出てきたスコープは、クライアントへ許可されたアクセス範囲を表します。
例えばopenidemailprofileなどがあり、これらはOIDCでユーザー情報を取得するためのスコープです。

業務API固有の操作権限を表す場合は、API固有のアクセス権を表すためのカスタムスコープを定義する必要があります。本構成ではカスタムスコープを用います。

3種類のトークン

さらっと出てきたトークンというやつですが、これは暗号化された文字列です。
今回の構成ではこのトークンが3種類出てきます。

用語 定義 この構成での用途
アクセストークン 保護されたAPIへアクセスする権限を表すトークン Claude CodeがAgentCore Gatewayへ送る
IDトークン ログインしたユーザーの識別情報をクライアントへ伝えるトークン ユーザー情報を確認する
リフレッシュトークン 新しいアクセストークンを取得するためのトークン アクセストークンの期限切れ時に使う

AgentCore Gatewayへ送るのはアクセストークンです。ここではひとまず3種類あるんだな〜くらいの理解でもOKです。

認可コード

認可コードは、ログイン成功後に認可サーバーがクライアントへ一時的に返す短命な値です。クライアントはこの認可コードをトークンと交換します。

認可コードを使う理由は、よりセキュアにトークンを交換するためです。
アクセストークンをユーザーのブラウザへ渡さずに済み、かつブラウザを使うログイン処理とクライアントが行うトークン取得処理を分離できるため、よりセキュアにできるとのこと。

フローとしては以下のとおりです。

  1. ユーザーがMCP接続を開始すると、Claude CodeはCognitoのログインURLをブラウザで開く
  2. ユーザーはブラウザ上でログインし、クライアントによる利用を許可する
  3. Cognitoはブラウザへリダイレクトレスポンスを返し、Claude Codeのlocalhost callback URLへ認可コードを送信する
  4. Claude Codeはブラウザから受け取った認可コードを、Cognitoのトークンエンドポイントへ直接送信する
  5. Cognitoは認可コードを検証し、Claude Codeへアクセストークンを返す

このままでは、認可コードを第三者に横取りされた場合、その第三者もトークン交換を試せます。そこでPKCEというものを追加します。

PKCE(Proof Key for Code Exchange)

PKCE(ぴくしー と読む)は、認可コードを横取りされても第三者がトークンへ交換できないようにする仕組みです。これはOAuthの標準仕様です。

Proof Key for Code Exchangeを訳すと 認可コード横取り攻撃対策 ということで、言葉通りの意味ですね。

ログイン開始時にクライアントだけが知る検証用情報を作り、トークン交換時に同じクライアントであることを確かめることで、セキュアなトークン交換を実現しています。

ちなみにMCPの認可仕様では、MCPクライアントに対してPKCEの実装を要求しています。
(この構成ではClaude CodeがMCPクライアントなので、Claude Codeに内蔵されたOAuthクライアントがPKCEを自動実行します)

PKCEは、元になる秘密の値と、そこから作る公開用の値を組み合わせます。標準仕様として定義されているの正式名と担当領域は次の通りです。

定義 この構成での担当
code_verifier クライアントだけが保持する、十分に長いランダム値 Claude Code内蔵のOAuthクライアントが生成・保持する
code_challenge code_verifierを変換して作る、認可サーバーへ送信可能な値 Claude Code内蔵のOAuthクライアントが計算し、Cognitoへ送る

この構成では、code_verifierをSHA-256でハッシュ化し、Base64URL形式にしてcode_challengeを作ります。この変換方式をS256と呼びます。

Cognitoはログイン開始時にcode_challengeを記録し、トークン交換時に受け取るcode_verifierと対応するかを検証します。

  1. Claude Codeはcode_verifierを手元に保持し、そこから作ったcode_challengeを先にCognitoへ送信する
  2. Cognitoはcode_challengeを、後で発行する認可コードへ紐付ける
  3. 第三者は認可コードだけを横取りしても、code_verifierを提示できないため拒否される
  4. 最初にログインを開始したClaude Codeだけが、認可コードと正しいcode_verifierを送信できる
  5. Cognitoは両者の対応を確認できた場合だけアクセストークンを発行する

ということで、認可コードだけを使う場合よりもずっとセキュアにアクセストークンの交換ができるようにする仕組みをPKCEと呼びます。

デジタル署名とJWKS

デジタル署名は、改ざん検知の仕組みです。
もう少し噛み砕くと、トークンが正しい発行者から発行され、発行後に書き換えられていないことを確認する仕組みです。Cognitoは秘密鍵でJWTへ署名します。

JWKS(JSON Web Key Set)は、JWTの署名検証に使う公開鍵の一覧です。AgentCore Gatewayは公開鍵で署名を検証します。

公開鍵からトークンを新しく発行したり、既存トークンを書き換えたりはできません。

well-knownエンドポイントとOIDC Discovery

well-knownエンドポイントは、サーバーの設定情報を決められたURLで公開するためのURL規約です。

URLに/.well-known/が含まれるため、この名前で呼ばれます。
これは認証専用の仕組みではなく、案内用エンドポイントは必ず決まった場所に置くというルールとして、さまざまな規格が共用しています。

well-knownエンドポイントを使う規格の1つに、OIDC Discoveryというのがあります。

これに対応している場合、認可サーバーは自分の設定情報を/.well-known/openid-configurationというURLで公開しています。
クライアントはそのURLにGETリクエストをコールして設定情報を取得し、接続設定を自動的に組み立てる、ということが可能です。

つまり、well-knownエンドポイントというサーバー情報に関する置き場所のルールがあり、OIDC Discoveryはこのエンドポイントに何を置きどう使うかを定めた規格という関係です。

HTTP 401とWWW-Authenticate

HTTP 401(Unauthorized)は、有効な認証情報がないことを示すHTTPステータスです。

WWW-Authenticateは、HTTP 401レスポンスに付けられるヘッダーです。
クライアントはこのヘッダーから、どの認証方式を使い、認証に必要な情報をどこで取得するかを知ります。

本構成ではBearer方式を用いるので、WWW-Authenticate: Bearer realm=Access to the AgentCore Gateway みたいな形になります。
これをクライアント(Claude Code)は読んで、正しい認証方法を認識します。

DCR(Dynamic Client Registration)

DCRは、認可サーバーが認可できるOAuthクライアントを自動的に登録するための標準規格です。

通常のOAuthでは、アプリ(クライアント)が認可サーバーを利用する前に、開発者がクライアントID、リダイレクトURI、必要に応じてクライアントシークレットを手動で登録しておく必要があります。

今回の構成で言えば、Cognitoユーザープールでアプリケーションクライアントを作成してそのクライアントIDを発行しておき、Claude Codeに登録しておく、みたいな形です。

DCRはこの登録プロセスをAPIで自動化する仕組みです。

クライアントが登録用のURLへ自分の情報(アプリケーション名、リダイレクトURIなど)をPOSTすると、認可サーバーがその場でclient_id(認可サーバーがClientを識別するための公開識別子)を発行して返します。

クライアントは受け取ったclient_idを使って、通常の認可コードとPKCEのフローを開始できます。

なぜDCRを使うのか

従来のOAuthは、開発者が1回登録したアプリを多数のユーザーが使う、多対一の関係でした。

一方MCPでは、ユーザーの端末ごとにMCPクライアントが存在し、接続先のMCPサーバーも多数あるため、必要な登録の数がクライアントの数 × 接続先の数へ膨らみます。
この数を手動登録で賄うのは現実的ではないため、登録の自動化が求められたというのが背景にあります。

DCRというのは2015年頃から存在していたらしいのですが、MCPの普及に伴って再度注目を浴びているような形のようです。

なお、最新のMCP認可仕様では、DCRは非推奨となり、後継としてCIMD(Client ID Metadata Documents)という方式が推奨されています。

それでもこの構成ではDCRを使う必要があります。それについては後述します。

SAML(Security Assertion Markup Language)とフェデレーション

SAMLは組織の認証基盤とアプリケーションの間で認証結果を受け渡すための標準規格です。企業SSOでよく用いられるもので、組織に属している方は毎日恩恵を受けているはずです。

例えば、皆さん自組織のMicrosoftアカウントに一度ログインしておけば社内ポータル・Zoom・Outlook・Teamsなど複数のサービスへ追加のログインなしでアクセスできるようになっていると思います。

この体験がSSOであり、そしてSSOの裏側で使われている仕組みがフェデレーションです。
そしてこのフェデレーションを実現する規格の1つがSAMLです。ややこしいですね〜

フェデレーションというのは、あるシステムでの認証結果を、別のシステムが信頼する仕組みです。
この信頼関係があることで、一度のログインで複数のサービスへアクセスできるようになります。

フェデレーションには以下2つの用語が登場します。

  • IdP(Identity Provider): 認証基盤側、本人確認して"認証済み"のステータスを発行する
  • SP(Service Provider): サービス提供側、IdPの結果を受け取り検証してサービスを提供する

そしてSAMLは、IdPからSPへ「認証済み」という結果を渡すためのルールです。
SPは、IdPでのログイン完了を示すデータを受け取って検証し、ユーザーがサービスへアクセスできるようにします。

SPをZoom、IdPをEntra IDとした場合の一般的な流れは次の通りです。実際の通信はブラウザのリダイレクトを経由しますが、図では省略しています。

SAMLアサーションは、認証されたユーザーと属性を記載した署名付きXMLです。本構成ではEntra IDが発行し、Cognitoが受け取って検証します。
つまり本構成では、CognitoがSPの位置に立ちます。

本構成での実際の流れ(Entra IDとCognitoの間のSAML連携)は、後の「認証フロー」章のフェーズ2で示します

メタデータ

メタデータは、接続に必要な設定情報をまとめた、機械が読むための文書です。

例えば「ログインはこのURLで開始する」「トークンはこのURLで交換する」「発行者はこれ」といった情報が入ります。

{
  "issuer": "https://oidc.example.com",
  "authorization_endpoint": "https://oidc.example.com/authorize",
  "token_endpoint": "https://oidc.example.com/token",
  //...
}

これら接続の設定を人が1つずつ手作業で配ると、間違いやすく、変更にも追従できません。

そこで、サーバー側が決められたURL(前述のwell-knownエンドポイント)へメタデータを公開し、クライアント側がそれを取得して自動的に設定を組み立てます。

このブログでは名前が似ている3種類のメタデータが登場します。別々の目的を持つため、ここで整理しておきます。後々しれっと出てくるので、何だっけ?となったら戻ってきてください。

正式名 形式 誰が公開し、誰が読むか 主な中身
OAuth Protected Resource Metadata JSON Gatewayが公開し、Claude Codeが読む 「このGatewayを守っている認可サーバーはここ」という案内
OAuth 2.0 Authorization Server Metadata / OIDC Discovery JSON 認可サーバーが公開し、Claude CodeとAgentCore Gatewayが読む。この構成ではDCR Proxyが公開を代行 認可エンドポイント、トークンエンドポイント、jwks_uri、DCRの登録先
SAMLメタデータ XML(SAML) Entra IDとCognitoが互いに交換する entity ID、SSO URL、署名証明書

よくある誤解

認証周りは難しいですよね…私もよく間違えるところを備忘として少し整理しておきます。

誤解 実際
SSOはSAMLの別名 SSOはユーザー体験、SAMLは実現手段の1つ
OAuth 2.0はユーザー認証の規格 OAuth 2.0は認可(アクセス権の委譲)が中心
OIDCがユーザー識別を補う
JWTをdecodeすれば検証できる decodeは内容を読めるだけで、
署名検証にはならない
DCRがユーザーを認証する DCRはクライアント登録の規格であり、
ユーザー認証は行わない

2. マネージドサービス

Entra ID

Microsoft Entra IDは、組織のユーザーを認証するマネージドIDサービスです。
この構成ではSAML IdPとして動き、会社のサインインポリシーや多要素認証を適用します。

本記事はproductionで想定するEntra IDを例に説明します。
現実装はSSO_METADATA_URLが設定された場合に、IAMIdentityCenterという論理名でCognitoのSAML providerを作成します。

この論理名は接続先製品を固定するものではなく、指定したメタデータを提供するSAML 2.0対応IdPなら同じ構成を適用できます。

Cognitoユーザープール

Cognitoユーザープールは、ユーザー管理とOAuth 2.0 / OIDCの認可サーバー機能を提供するAWSマネージドサービスです。

ログインページには、今回はClassic Hosted UIを使っています。Cognitoが提供するマネージドなログイン画面とOAuthエンドポイント群です。
ユーザーをEntra IDへリダイレクトし、戻ってきたSAMLアサーションを受け取った後、Claude Code向けの認可コードとトークンを発行します。

CognitoはSAMLの観点ではSP、OAuth 2.0 / OIDCの視点では認可サーバーです。
つまり、SAMLによる認証基盤の認証結果をOAuth 2.0 / OIDCのトークンへ橋渡しする二重の役割を持ちます。

cognito:groupsとtoken_use

cognito:groupsは、ユーザーが所属するCognitoユーザープールグループの一覧を示すクレームです。
この構成では、AgentCore Gatewayを利用できるグループかどうかの判定に使います。

token_useはCognitoがJWTへ追加するクレームで、トークン種別を意味します。
値がaccessならアクセストークン、idならIDトークンであることを示します。

image.png

Entra IDのグループとCognitoユーザープールグループは自動的に同じになるとは限りません。
明示的にユーザーをグループへ追加するか、SAML属性のマッピングやトークン発行直前にクレームを調整するLambdaトリガーを使うかを設計する必要があります。

AgentCore Gateway

AgentCore Gatewayは、APIやLambdaなどをMCPツールとして公開するAWSマネージドサービスです。
本構成ではリソースサーバーとして、Claude Codeから届くアクセストークンを検証します。

その際、AgentCore Gatewayが外部の認可サーバーから発行されたJWTを検証するための設定をいくつか入れています。具体的には下記の設定などです。

  • discoveryUrl: JWT検証に必要なissuerやjwks_uriを取得するOIDC DiscoveryエンドポイントのURL
    • 今回は後続で説明するDCR ProxyのLambda関数URLを設定

image.png

  • allowedScopes: 受信したトークンに許可するスコープを列挙するAgentCore Gatewayの設定
    • 設定した場合、受信したトークンのスコープのうち少なくとも1つが設定値と一致する必要がある
    • openidemailprofileはAgentCore Gateway固有の操作権限ではないため、この例の中心的な利用者認可はcognito:groupsで行う

image.png

3. この構成のために自作するもの

DCR Proxy

DCR Proxyは、Cognitoへクライアント登録を橋渡しする自作Lambdaです。

CognitoはOAuth 2.0の認可エンドポイント、トークンエンドポイント、OIDC Discovery、JWKSを提供しますが、DCRの登録用エンドポイントは提供しません。

そこでDCR Proxy Lambdaが、DCRの登録用エンドポイントとして機能します。

Claude Codeから登録リクエストを受けたら、AWS SDKでCognitoユーザープールのアプリケーションクライアント(client_id、コールバックURL、利用可能なOAuth 2.0 flowなどを管理するリソース)を都度作成します。

DCR Proxyが行うのは次の2つです。

  1. Claude CodeへOAuth 2.0 Authorization Server Metadata / OIDC Discoveryを返す
  2. DCRリクエストをCognito User Pool App Client作成へ変換する

DCR Proxyはユーザーを認証せず、トークンも発行しません。ユーザー認証はEntra ID、トークン発行はCognitoが担当します。

流れを図にすると次の通りです。
メタデータの取得とクライアント登録のリクエストだけがDCR Proxyへ向かい、ログインやトークン取得はCognitoと直接通信します。
認証フロー全体の中での位置づけは、後の「認証フロー」章のフェーズ1で示します。

ここには重要な互換性上の注意があります。

OIDC Discoveryには、「メタデータを取得したURL」と「メタデータに書かれたissuer」の対応を確認する、という検証ルールがあります。これは偽のメタデータを配って接続先をすり替える攻撃を防ぐためのルールです。

例えばissuerが https://auth.example.com なら、メタデータもhttps://auth.example.com/.well-known/... から取得できているはずだ、という対応関係を確認します。

ところがこの構成では、メタデータを配るのはDCR Proxyなのに、その中に書かれているissuerはCognitoのURLです。

# メタデータを取得したURL=DCR Proxy Lambdaの関数URL
https://<id>.lambda-url.ap-northeast-1.on.aws/

# メタデータの中身
{
  "issuer": "https://cognito-idp.<region>.amazonaws.com/<user-pool-id>",
  //...
}

取得元とissuerのオリジンが別物なので、このルールを厳密に検証するOAuthクライアントは、このメタデータを「取得元とissuerが一致しない」として拒否します。
そりゃそうですよね、パッと見だとめちゃくちゃ怪しいですし。

ただ、Claude Codeは現状この不一致を接続エラーとして扱わないため、この構成が成立しています。

つまりDCR Proxyは、標準に完全準拠した独立の認可サーバーではなく、Claude Codeとの接続を成立させるための互換アダプターです。

他のOAuthクライアントが同じように寛容である保証はないため、利用するクライアント(ex: Codex, Kiro, Pi)ごとに動作確認が必要です。
また、Claude Code側の検証が将来のバージョンで厳密になる可能性もあります。

標準準拠を優先する場合の選択肢は2つです。
1つは、DCRをあきらめて固定callback portと事前登録したCognitoアプリケーションクライアントを使い、Cognito公式のOIDC Discoveryへ直接接続する方法です。

もう1つは、DCR Proxy自身のURLをissuerとし、トークンの発行と署名まで自前で担う独立した認可サーバーを構築する方法ですが、こちらは大きく複雑になります。

認証フロー

ここからは、すでに定義した用語を使って実際の通信を5フェーズに分けます。

1. DCR Proxyとのやり取り

このフェーズの目的は、Claude Codeが認証先を発見し、Cognitoで利用するclient_idを取得することです。

  1. Claude Codeは登録済みのGateway URLへ接続しようとする
  2. Gatewayは認証情報がないためHTTP 401とWWW-Authenticateを返す
  3. Claude CodeはOAuth Protected Resource Metadataを取得し、認可サーバーの場所を把握する
  4. Claude CodeはDCR Proxyが返すClaude Code向け互換メタデータを読み、登録用エンドポイントを見つける
  5. Claude CodeはDCR Proxyへリクエストを送信する
  6. DCR ProxyはリクエストをCognitoユーザープールのアプリケーションクライアント作成へ変換し、client_idを受け取る
  7. DCR ProxyはClaude Codeへclient_idを返す

ここまでで、Claude Codeはログインを開始するためのOAuthクライアントとして登録されました。

2. SSOログイン

このフェーズの目的は、ユーザーがEntra IDで認証され、Claude Codeが認可コードを受け取ることです。

  1. Claude CodeはPKCEの検証情報を作り、Cognitoの認可エンドポイントへアクセスする
  2. Cognito Hosted UIはユーザーをEntra IDへリダイレクトする
  3. Entra IDは会社のポリシーに従ってユーザーを認証する
  4. Entra IDはSAMLアサーションをCognitoへ返す
  5. CognitoはSAMLアサーションを検証し、Claude CodeのコールバックURLへ認可コードを返す

ここまでで、会社のSSOは完了し、Claude Codeは認可コードを手に入れました。

3. トークン取得

このフェーズの目的は、Claude Codeが認可コードをアクセストークンとリフレッシュトークンへ交換することです。

  1. Claude Codeは認可コードとPKCEの検証情報をCognitoのトークンエンドポイントへ送る
  2. Cognitoは認可コードが未使用であり、PKCEの検証結果が一致することを確認する
  3. Cognitoは署名付きアクセストークンとリフレッシュトークンを返す

ここまでで、Claude CodeはAgentCore Gatewayへ提示できるアクセストークンを取得しました。

4. AgentCore Gatewayでのトークン検証

このフェーズの目的は、AgentCore Gatewayがアクセストークンの発行元、改ざんの有無、有効期限、利用条件を確認することです。

Bearer形式とは、HTTPのAuthorizationヘッダーへトークンを入れ、トークンを持っているクライアントを利用者として扱う送信方式です。

  1. Claude CodeはHTTPのAuthorizationヘッダーへBearer形式でアクセストークンを入れる
  2. GatewayはdiscoveryUrlからissuerとjwks_uriを取得する
  3. GatewayはJWKSの公開鍵でJWTの署名を検証する
  4. Gatewayは有効期限、スコープ、token_use = accesscognito:groupsを確認する
  5. Gatewayは検証に成功したリクエストを処理し、拒否する場合はエラーをClaude Codeへ返す

AgentCore Gatewayは受信リクエストごとにCognitoのトークンエンドポイントへ「このトークンは有効か」と問い合わせるわけではありません。

トークンエンドポイントを使うのは、Claude Codeが認可コードやリフレッシュトークンをアクセストークンへ交換するときです。

AgentCore GatewayによるOIDC Discovery情報と公開鍵の取得・更新はAWS側で管理され、利用者がキャッシュ処理を実装する必要はありません。

ここまでで、AgentCore Gatewayはリクエストを許可するか拒否するかを判断できました。

5. 対象API呼び出し

このフェーズの目的は、認可済みリクエストをAgentCore Gatewayから業務APIへ安全に転送することです。

  1. Gatewayは受信したMCPリクエストを対象API向けのリクエストへ変換する
  2. Gatewayは自分のサービスロールの権限でAWS APIをコールする
  3. APIは結果をGatewayへ返す
  4. GatewayはAPIレスポンスをMCPツールの結果へ変換し、Claude Codeへ返す

この5つのフローを経て、Claude Codeは無事MCPサーバーを使うことができるようになりましたとさ。

AWS上での実装ポイント

Cognito User PoolとEntra ID連携

Cognitoユーザープールでは、Hosted UIドメイン、SAML IdP、認可コードグラント、必要なスコープを設定します。

Entra IDとCognitoの間では、次のSAML設定を合わせます。

設定 役割
Cognito ACS URL Entra IDがSAML Assertionを返すReply URL
Cognito SP Entity ID Entra ID側のIdentifier
Entra IDのSAMLメタデータ CognitoがSSO URLや署名証明書を取得するXML
NameIDと属性mapping NameIDはSAMLでユーザーを識別する値。Entra IDの属性をCognitoのusernameやemailへ対応付ける
ユーザー割り当て Entra IDの連携アプリを表すEnterprise Applicationへ、サインイン可能なユーザーを割り当てる

openidemailprofileはログインとユーザー情報取得のためのOIDCスコープです。
AgentCore Gateway呼び出し権限をスコープとして明示する場合は、Cognitoリソースサーバーへgateway/invokeのようなcustom scopeを定義します。

ここは今日の本題ではないので簡略化して説明しています。
後日ここにフォーカスした認証認可周りのブログを書ければと思います。

DCR Proxy

DCR Proxyは2つのwell-knownエンドポイントとPOST /registerを公開します。

GET  /.well-known/openid-configuration
GET  /.well-known/oauth-authorization-server
POST /register

DCR Proxyが返すClaude Code向け互換メタデータは次のようになります。
中身はOAuth 2.0 Authorization Server Metadata / OIDC Discoveryに合わせています(下記リンク4.2参照)。

ただ、前述のとおりDCR Proxy自身が認可サーバーになるわけではありません。

S256は、PKCEの検証値をSHA-256でハッシュ化する方式です。

{
  "issuer": "https://cognito-idp.<region>.amazonaws.com/<user-pool-id>",
  "authorization_endpoint": "https://<cognito-domain>/oauth2/authorize",
  "token_endpoint": "https://<cognito-domain>/oauth2/token",
  "jwks_uri": "https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json",
  "registration_endpoint": "https://<dcr-proxy>/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["openid", "email", "profile"]
}

DCRリクエストとレスポンスの最小例は次の通りです。

リクエスト
{
  "client_name": "claude-code",
  "redirect_uris": ["http://127.0.0.1:49152/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "openid email profile"
}
レスポンス
{
  "client_id": "<cognito-app-client-id>",
  "client_name": "<configured-prefix>-claude-code-<uuid8>",
  "client_id_issued_at": 1760000000,
  "redirect_uris": ["http://127.0.0.1:49152/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "openid email profile"
}

下記はDCR Proxy LambdaがPOST /registerを処理するときに実行するAWS SDK for JavaScript v3のコードの一部です。
GenerateSecret: falseにする理由は、Claude Codeがクライアントシークレットを安全に保持できないパブリッククライアントだからです。

new CreateUserPoolClientCommand({
  UserPoolId: userPoolId,
  GenerateSecret: false,
  SupportedIdentityProviders: [samlProviderName],
  CallbackURLs: validatedLoopbackRedirectUris,
  AllowedOAuthFlowsUserPoolClient: true,
  AllowedOAuthFlows: ['code'],
  AllowedOAuthScopes: allowedScopes,
  PreventUserExistenceErrors: 'ENABLED',
  EnableTokenRevocation: true,
  ExplicitAuthFlows: ['ALLOW_REFRESH_TOKEN_AUTH'],
});

もちろん、このコードだけではDCRエンドポイントとして完成しません。
実際には、リクエスト本文、ループバックリダイレクトURI、token_endpoint_auth_methodgrant_typesresponse_types、スコープを検証し、成功時はHTTP 201 Created、失敗時はDCRを定めたRFC 7591形式のエラーを返す必要があります。

AgentCore GatewayのカスタムJWT認証

AWS CDKで書く場合は以下のように書けます。多分今ならL2 Constructでも書けるかもしれませんが。

customClaimsが、AgentCore Gatewayが独自のクレーム名と期待値を検証するための設定項目です。
本構成ではcognito:groupstoken_useの2つを検証します。

new agentcore.CfnGateway(this, 'Gateway', {
  name: '<gateway-name>',
  roleArn: gatewayServiceRole.roleArn,
  authorizerType: 'CUSTOM_JWT',
  authorizerConfiguration: {
    customJwtAuthorizer: {
      discoveryUrl: `${trimTrailingSlash(dcrProxyUrl)}/.well-known/openid-configuration`,
      allowedScopes: ['openid', 'email', 'profile'],
      customClaims: [
        {
          inboundTokenClaimName: 'cognito:groups',
          inboundTokenClaimValueType: 'STRING_ARRAY',
          authorizingClaimMatchValue: {
            claimMatchOperator: 'CONTAINS_ANY',
            claimMatchValue: {
              matchValueStringList: ['<allowed-group>'],
            },
          },
        },
        {
          inboundTokenClaimName: 'token_use',
          inboundTokenClaimValueType: 'STRING',
          authorizingClaimMatchValue: {
            claimMatchOperator: 'EQUALS',
            claimMatchValue: {
              matchValueString: 'access',
            },
          },
        },
      ],
    },
  },
  protocolType: 'MCP',
});

この例ではissuer(JWT発行者)、JWT署名、有効期限、スコープ、token_usecognito:groupsを検証します。
allowedScopesへ設定した3つのOIDCスコープはGateway固有の操作権限ではないため、利用者認可の中心はグループクレームです。

設計判断FAQ

Cognitoアプリケーションクライアントを1つ作るだけでも良いのでは?

Claude Codeは既定でOAuthコールバックにランダムなループバックポートを使います。
固定ポートを指定して事前登録する運用なら、自前作成した1つのアプリケーションクライアントも利用できます。

端末ごとに固定設定を配らず、実行時のコールバックURLへ対応する場合はDCRが適しています。

そんなに制限事項でもないので、ぶっちゃけこれで事足りるかもしれませんね…

なぜCognitoだけでは足りないのか

Cognitoは認可エンドポイント、トークンエンドポイント、OIDC Discovery、JWKSを提供しますが、DCRの登録用エンドポイントを提供しません。

DCR対応を必要とするClaude Codeとの間を、自作LambdaであるDCR Proxyが補っている形です。

DCR Proxyを認証なしで公開して大丈夫なのか

結論としては、認証なしの公開はこの構成の前提であり、代わりの防御とセットで成立させます。

DCR Proxyが提供するDiscoveryと/registerは、ユーザーがログインする前に呼ばれる入口です。
ここでアクセストークンを要求すると、トークンを得るための入口にトークンが必要という循環になり、フローが始まりません。
Cognitoの認可エンドポイントやOIDC Discoveryが認証なしで公開されているのと同じ性質です。
なので認証はつけないというより、つけられません。

認証なしでも直ちに危険にならないのは、登録に成功しても得られるものがclient_idだけだからです。client_idはクライアントの識別子であり、アクセス権ではありません。

アクセストークンを得るには、その後Entra IDでのログインに成功する必要があり、さらにAgentCore Gatewayがcognito:groupsの許可グループを検証します。
利用可否を守っているのはこの2段の検証であり、これを突破しないとAgentCore Gatewayにはアクセスできません。

また、攻撃者が自分で登録したアプリケーションクライアントへ正規ユーザーを誘導しても、リダイレクトURIがループバック限定のため、認可コードはログインしたユーザー自身の端末にしか戻りません。
後述の「リダイレクトURIをloopbackへ限定する」は、この遠隔での認可コード横取りを防ぐための制約です。

一応リスクとしては、登録の乱発によるアプリケーションクライアントの増加です。サービスクォータ上限(デフォルト1000)へ到達すると正規ユーザーの新規登録が失敗し、サービス妨害(DoS)につながります。
image.png

認証の代わりに、DCR Proxyでのリクエスト検証、Cognito登録回数の制限、WAFなどを用いた前段の防御、定期的なアプリケーションクライアントクリーンアップが防御線になります。

audienceを固定しないのはリスクではないのか

AgentCore GatewayにはallowedAudienceallowedClientsという設定があります。
固定アプリケーションクライアントを使う構成では、受取先やクライアントを絞るために有効です。

ただ、DCRではclient_idが動的に増えるため、すべてを固定値で列挙する方式とは相性がよくありません。
本構成では前述の通り、Cognitoのissuer、署名、スコープ、token_use、許可グループを用いています。

ただしこの対応では、同じユーザープールが別用途へ発行したトークンを再利用される余地が残ります。
本番ではCognitoリソースサーバーにgateway/invokeのようなcustom scopeを定義し、Gateway専用の権限をトークンへ入れる方が良いかもしれません。

まとめ

この構成を使うと、SSOでログインさえしていればAgentCore Gateway経由で提供されるMCPサーバーを自由にかつセキュアに使うことができるようになっています。

個人的には非常に体験が良いのですが、開発者側の都合(特にアプリケーションクライアントのクォータ上限)まで考慮できていなかったというのが正直なところです。

万が一全社員が使うMCPサーバーなどを提供する必要が出てきた時にはこの構成では限界が来ます。今のところそんな未来はこなそうなのですが。

なのでこれに変わる代替案をまた探していこうと思います。

ただ、これはこれで認証認可周りの良いお勉強になったので、やってみてよかったなという感じです!

参考リンク

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?