Claude Code を MCP サーバーに接続します。サーバーには認証が必要です。クライアントがリクエストを送信します。サーバーは、401 Unauthorized および resource_metadata URL を伝送する WWW-Authenticate: Bearer ヘッダーで応答します。次に起こるのは、3 つの RFC によって定義された OAuth 2.1 フローと、一連の MCP 固有の拡張機能です。これらを組み合わせることで、エージェント セキュリティにおける最も難しい問題の 1 つを解決します。つまり、この MCP サーバーと通信するために取得されたトークンが あの サーバーに対してリプレイできないようにするというものです。
この投稿では、仕様に記載されている内容、RFC が実際に提供している内容、実際にはセキュリティの落とし穴がどこに隠れているかなど、完全なフローを追跡します。
フロー: 401 からリソース バインド トークンまで
MCP 認可モデル (リビジョン 2025-11-25、変更せずにリリース候補に引き継がれる - 2026-07-28 に予定されている最終仕様の凍結された 2026-05-21) は、2 段階のプロセスです。フェーズ 1 は検出です。WWW-Authenticate: Bearer resource_metadata="..." を運ぶ 401 は、このサーバーが OAuth で保護されていることと、そのメタデータの場所をクライアントに伝えます。フェーズ 2 は承認されたアクセスです。メタデータを消費し、承認サーバーを検出し、この特定のサーバー にバインドされたトークンを取得して、それを使用します。
具体的な手順は次のとおりです。
-
401 + WWW-Authenticate: MCP サーバーは、認証されていないリクエストを拒否します。チャレンジの
resource_metadataパラメーターは、サーバーの保護されたリソース メタデータ ドキュメントを指します。 -
PRM フェッチ (RFC 9728): クライアントは
/.well-known/oauth-protected-resourceURL を取得します。応答は、サーバーの正規リソース URI、それを保護する認可サーバー、およびリソースがサポートするスコープを宣言する JSON ドキュメントです。クライアントは、ドキュメントのresourceフィールドが到達先のサーバーと一致することを検証します。不一致は偽装信号であるため、拒否する必要があります。 -
AS 検出 (RFC 8414): PRM の
authorization_servers配列の発行者 URL を使用して、クライアントは既知の候補 (/.well-known/oauth-authorization-server、次に/.well-known/openid-configuration) をたどり、認可サーバーのメタデータを取得します。返されるドキュメント内の発行者は、取得された発行者とバイト同一である必要があります。「正規化後に同等」ではなく、「十分に近い」でもありません。バイトが同一。 -
トークン取得 (RFC 8707): クライアントは、認可リクエストとトークンリクエストの両方に
resource=<canonical server URI>を含む、AS のトークン エンドポイントからトークンをリクエストします。これにより、トークンの対象者がこの特定の MCP サーバーにバインドされます。 AS は、そのリソースを対象とするトークンを発行し、クライアントはそれをサーバーに提示します。 -
承認されたアクセス: クライアントはトークンを使用して、MCP サーバーの読み取り専用イントロスペクション メソッド (
tools/list、resources/listなど) を呼び出します。トークンはクライアントが認可されていることを証明します。リソース インジケーターは、トークンが この サーバー用に作成されたことを証明します。
RFC 9728 が実際に提供するもの
RFC 9728 (保護されたリソース メタデータ) は検出メカニズムです。これは、「どの認可サーバーがこのリソースを保護するのか、また何を期待しているのか?」と答えます。サーバーの認証、トークンの検証、アクセス制御の強制は行いません。これらは別の問題です。
PRM ドキュメントには 1 つの必須フィールド (resource) と一連のオプションフィールドがあり、そのうち authorization_servers が最も重要です。 MCP 仕様は RFC のオプション性を強化し、少なくとも 1 つの認可サーバーが必要です。何もリストされていない PRM ドキュメントはエラーとして扱われます。
resource フィールドは、保護されたリソースの正規 URI です。クライアントは、それを接続しようとしたサーバーと比較し、不一致を拒否する必要があります。
// RFC 9728 §3.3: PRM のリソース値は、保護されたリソースを識別する必要があります。
// クライアントがアドレス指定しているリソース — 単純な文字列比較
// このクライアントがトークンをバインドするリソース インジケーター。
if prm.Resource != c.resource {
return authServerMetadata{}, fmt.Errorf(
"mcp: oauth: protected resource metadata declares resource %q, "+
"expected %q (RFC 9728 §3.3 reject)", prm.Resource, c.resource)
}
このチェックにより、悪意のあるまたは誤って構成された PRM ドキュメントが別のリソースを代弁すると主張する攻撃クラスが防止されます。比較は正規化されていません。これは、クライアントが指定されたサーバー URL に対して計算した正規のリソース URI に対する文字列の直接一致です。
RFC 8707 リソース インジケーター - トークン バインディングが重要な理由
リソース インジケーターがないと、AS から取得したアクセス トークンは、AS が保護する任意のリソース サーバーで使用できる可能性があります。 2 つの MCP サーバー (読み取り専用ドキュメント サーバーとコード実行サンドボックス) が両方とも同じ ID プロバイダーの背後にある場合、一方で取得したトークンがもう一方に提示される可能性があります。それは混乱した副問題です。
RFC 8707 は、認可リクエストとトークンリクエストに resource パラメータを追加することでこれを解決します。 AS は、対象者が明示的にそのリソース URI であるトークンを発行します。適切に検証されたリソース サーバーは、対象者が自身の ID と一致しないトークンを拒否します。
実際には、トークン リクエストには許可とともにリソース インジケーターが含まれます。
form := url.Values{
"grant_type": {"client_credentials"},
"resource": {c.resource}, // RFC 8707 — トークンをオーディエンスにバインド
}
これは、コネクタが使用するすべての許可タイプ (クライアント資格情報、認証コード引き換え、リフレッシュ トークン ローテーション) に表示されます。リソース インジケーターはオプションではありません。リソース インジケーターはすべてのトークン リクエストに存在するため、すべてのトークンは構造によって視聴者に制限されます。
バイト同一の発行者チェック
フロー全体の中で最もセキュリティ クリティカルな検証は、最も簡単に述べることができ、最も間違いやすいものでもあります。AS メタデータ ドキュメント内の発行者の値は、クライアントが既知の URL を構築するために使用した発行者と バイト同一である必要があります。
大文字と小文字を区別せずに等価ではありません。スキーム正規化後は等価ではありません。末尾のスラッシュを削除した後も同じではありません。バイトが同一。 RFC 8414 セクション 3.3 はこれについて明示しており、MCP 仕様はその要件を継承しています。
// discoverASMetadata: issuer が取得元と一致しないドキュメントを
// 拒否する(RFC 8414 §3.3 に従い、issuer はバイト単位で同一でなければならない)。
// 一致しないドキュメントは、なりすましのシグナルである。
if as.Issuer != issuer {
return authServerMetadata{}, fmt.Errorf(
"mcp: oauth: AS metadata at %s declares issuer %q, "+
"expected %q (RFC 8414 §3.3 reject)", cand, as.Issuer, issuer)
}
なぜそんなに厳しいのでしょうか? DNS を制御している攻撃者、またはネットワーク パス上に存在する攻撃者は、正当な発行者であると主張しながら、独自のトークン エンドポイントを指すメタデータ ドキュメントを提供する可能性があるためです。クライアントが比較する前に発行者を正規化した場合、https://auth.example.com と https://AUTH.example.com は一致し、攻撃者のドキュメントが受け入れられます。バイト同一比較でこれが終了します。
同じ規律が RFC 9207 (認可応答発行者の検証) にも適用されます。クライアントが認証コード フローを開始すると、検証済み AS メタデータから発行者を記録します。リダイレクトバックでは、応答内の iss パラメーターは、認証コードが引き換えられる 前 に、記録された値と一致する必要があります (ここでもバイトごと、正規化なし)。これが混合防御です。これがないと、悪意のある AS がコードを傍受し、攻撃者のトークン エンドポイントでクライアントにコードを引き換えさせる可能性があります。
クライアント ID: CIMD は DCR を置き換えます
MCP 仕様では、クライアントが認可サーバーに対して自分自身を識別する方法の優先順位を定義します。
- 事前登録された資格情報 - オペレーターは事前に
client_idおよびclient_secretをプロビジョニングします。 - CIMD (クライアント ID メタデータ ドキュメント) - クライアントは HTTPS URL で JSON ドキュメントをホストします。その URL は
client_idです - 動的クライアント登録 (RFC 7591) — クライアントは、AS の登録エンドポイントで自身を登録します。
- ユーザーにプロンプトを表示 — ヘッドレス エージェントには適用されません
DCR は、2026-07-28 に予定されている最終仕様のリリース候補では非推奨となり、CIMD が優先されます。理由は運用上の理由です。DCR は認可サーバーで永続的なクライアント状態を作成します。自身を登録するすべてのエージェントは、AS が保存する必要がある client_id/client_secret ペアを残しますが、誰もそれらを追跡したり取り消したりすることはありません。多数のエージェントにとって、これは制御不能な資格情報の無秩序な広がりです。
CIMD はモデルを反転します。クライアントは、制御する URL でドキュメントをホストします。 AS は、クライアントを検証する必要があるときにドキュメントをフェッチし、HTTP キャッシュ ヘッダーに従ってドキュメントをキャッシュし、永続的には何も保存しません。キーのローテーションはドキュメントの更新です。クライアントを廃止すると、ドキュメントが削除されます。 AS には孤立した登録は蓄積されません。
トレードオフ: CIMD では、クライアントが HTTPS エンドポイントを実行する必要があります。自己ホスト型ガバナンス プレーンの場合、これは自然なことです。プレーンはすでに HTTPS サービスを実行しています。開発者のラップトップ上の CLI ツールの場合は、それほど重要ではないため、事前登録された資格情報が優先順位の最初のオプションのままです。
CIMD ID は共有シークレットを保持できません (ドキュメント URL は公開されており、その中にシークレットがあると資格情報の漏洩になります)。クライアント認証では、private_key_jwt (RFC 7523) を使用します。クライアントは、公開鍵の対応する公開鍵が CIMD ドキュメントの jwks フィールドで公開される秘密鍵を使用して、短期間の JWT に署名します。
「決してパススルーしない」ルール
見落としがちな構造的防御: コネクタは、上記の検出フローを通じて特定のサーバーに対して自身で取得したトークンのみを使用します。サードパーティからトークンを受け取って MCP サーバーに転送することはありません。受信リクエストからトークンを読み取って渡すことは決してありません。
これはプロトコルレベルでの混乱した代理防御です。クライアントが受信したトークンを転送した場合、攻撃者は低特権のリソースをスコープとするトークンを提示し、クライアントにそれを高特権のリソースに転送させることができます (またはその逆、クライアントを騙して攻撃者が制御するサーバーに高特権のトークンを提示させることで高特権のトークンを抽出する) 可能性があります。独自のトークンを取得し、他の人のトークンには決して触れないことにより、コネクタをトークン リレーとして機能させることはできません。
トークンのパススルーは推奨されないだけでなく、構造的に不可能です。 OAuth フロー用のコネクタの HTTP クライアントは、受信リクエスト ハンドラーとは別のものです。受信リクエストからベアラー トークンを読み取り、送信リクエストに書き込むコード パスはありません。
SSRF: DNS 再バインド防御
コネクタが取得するすべてのメタデータとトークン エンドポイント URL は 2 つのレイヤーで SSRF で保護されています。 1 つ目はプリフライト チェックです。URL は HTTPS である必要があり (ローカル開発用のループバックを除く)、リテラルの予約済み IP アドレスであってはなりません。
2 つ目は、DNS 再バインド TOCTOU を閉じるダイヤル時間チェックです。プリフライト チェック中にパブリック IP に解決されたホスト名は、ソケットがダイヤルされるまでにプライベート IP に再バインドされる可能性があります。コネクタの HTTP クライアントは、接続時に 具体的に解決された IP を検査し、予約されたアドレスを拒否する net.Dialer.Control 関数をインストールします。これが権威あるチェックです。明らかな場合はプリフライトですぐに拒否されますが、重要なのはダイヤルタイム チェックです。
スコープのステップアップ
MCP サーバーは、WWW-Authenticate: Bearer error="insufficient_scope" scope="mcp:tools:list mcp:resources:read" でリクエストに応答できます。仕様 (SEP-835/SEP-2350) は、クライアントがこれを処理する方法を定義しています。つまり、以前に要求したスコープとサーバーがチャレンジしたばかりのスコープの和集合を計算し、その拡張されたセットでトークンを再取得します。ユニオンは、以前に付与されたアクセス許可を保持し、新しいアクセス許可を追加します。ステップアップは 1 回だけ行われます。無限ループを防ぐため、同じリクエストに対する 2 回目の不十分なスコープのチャレンジは再試行されません。
サーバーは、そのチャレンジにおいてステートレスであることが許可されています。サーバーは、クライアントが以前に要求した可能性のある完全なセットではなく、現在の操作に必要なスコープのみに名前を付けます。クライアント側の蓄積により、サーバーがクライアントごとのスコープ履歴を追跡しなくても、これが機能します。
これが実際に何を意味するか
MCP サーバーの OAuth フローは明確に仕様化されており、厳密に実装すると、サーバー間でのトークン リプレイ、メタデータの偽装、DNS 再バインド SSRF、および制御されていないクライアント資格情報のスプロールなど、実際の攻撃対象領域に対処できます。バイト同一の発行者チェック、すべてのトークン リクエストのリソース インジケーター、および構造的なパススルー ルールが防御の中核となります。これらは中途半端に実装できる機能ではありません。それぞれは失敗して終了するハード チェックです。
さらに難しい問題は運用上の問題です。 MCP サーバーを展開するチームは、次のことを行う必要があります。
- サーバーの正規 URI と一致する
resourceフィールドを持つ有効な PRM ドキュメントを/.well-known/oauth-protected-resourceで公開します - リソース インジケーターをサポートする AS を使用します — 多くの ID プロバイダーはまだサポートしていないか、
resourceパラメーターを視聴者バインディングではなくアドバイスとして扱っています - 非推奨が削除になる前に DCR から CIMD に移行するか、認証情報を明示的に事前登録してください
- 認証情報を発行者にピン留めして、AS トポロジが変更されたときに発行者間のサイレント再利用を防止します
MCP コネクタのドキュメント では運用構成について説明し、セキュリティ モデル ではリソース バインド トークンがより広範なアクセス マップにどのように適合するかを説明します。