MCPの認可 - OAuth 2.1とメタデータ探索
MCP の認可仕様 (HTTP トランスポート向け) を、OAuth 2.1 と関連 RFC の対応づけとともに解説します。2026-07-28 版の仕様を基準にします。認可でよく起こる攻撃と対策は MCPセキュリティベストプラクティス に、マシン間認証や企業 IdP との統合は MCPの認可拡張とエンタープライズ導入 にまとめています。
1. このドキュメントで扱うこと
- 役割と適用範囲
- 認可フローの全体像
- 401 と Protected Resource Metadata の探索 (RFC 9728)
- 認可サーバーのメタデータ探索
- クライアント登録
- 認可コードフローと PKCE
- Resource Indicators (RFC 8707)
- 認可レスポンスの検証 (
iss、RFC 9207) - アクセストークンの送り方と検証
- スコープ設計とステップアップ
- リフレッシュトークン
- エラー応答
役割と適用範囲
MCP の認可は、HTTP ベースのトランスポートで、制限された MCP サーバーにクライアントが、リソースの所有者に代わってリクエストするための仕組みです。OAuth 2.1 の次の役割に対応します。
| MCP の登場人物 | OAuth 2.1 での役割 | 責務 |
|---|---|---|
| MCP サーバー | リソースサーバー | アクセストークンを検証し、保護されたリソースを返す |
| MCP クライアント | OAuth クライアント | 利用者に代わって、保護されたリソースへリクエストする |
| 認可サーバー | 認可サーバー | 利用者とやりとりし、MCP サーバー向けのアクセストークンを発行する。MCP サーバーと同居しても別でもよい。実装の詳細は仕様の範囲外 |
認可は任意です。実装する場合の方針は、トランスポートで変わります。
- HTTP ベースのトランスポートは、この仕様に従うことが推奨される (SHOULD)
- stdio は、この仕様に従わず、環境から資格情報を取得する (従うことは SHOULD NOT)
- それ以外のトランスポートは、そのプロトコルで確立されたセキュリティのベストプラクティスに従う (MUST)
MCP 公式のチュートリアルは、次のような場合に認可を強く勧めています。利用者固有のデータ (メール、ドキュメント、データベース) にアクセスする、誰がどの操作をしたかを監査したい、利用者の同意が必要な API へアクセスさせる、アクセス制御が厳しい企業向け、利用者ごとのレート制限や利用状況の追跡をしたい、の場合です。
仕様は、OAuth 2.1 (IETF ドラフト)、Bearer Token (RFC 6750)、Authorization Server Metadata (RFC 8414)、Dynamic Client Registration (RFC 7591)、Resource Indicators (RFC 8707)、Protected Resource Metadata (RFC 9728)、Authorization Server Issuer Identification (RFC 9207)、Client ID Metadata Documents、OpenID Connect Discovery を、一部の機能に絞って組み合わせています。
認可フローの全体像
未認証のリクエストから、アクセストークンを使った通信までの流れは次のとおりです。
401 と Protected Resource Metadata の探索
MCP サーバーは、OAuth 2.0 Protected Resource Metadata (RFC 9728) を必ず実装します。メタデータには、少なくとも 1 つの認可サーバーを含む authorization_servers を含めます。サーバーは、次のどちらか (または両方) の方法で、メタデータの場所を知らせます。
WWW-Authenticateヘッダー:401 Unauthorizedのレスポンスに、resource_metadataとして URL を入れる- Well-Known URI:
/.well-known/oauth-protected-resourceで提供する。MCP のエンドポイントのパスに対応する場合 (https://example.com/public/mcpならhttps://example.com/.well-known/oauth-protected-resource/public/mcp) と、ルートの場合がある
クライアントは、両方に対応します (MUST)。WWW-Authenticate に resource_metadata があればその URL を使い、なければ、パス付き、ルートの順で Well-Known URI を試します。
メタデータが複数の認可サーバーを挙げる場合、どれを使うかはクライアントが決めます (RFC 9728 セクション 7.6)。それぞれは独立した OAuth 2.0 の認可サーバーで、クライアント ID はそれを発行した認可サーバーに固有です。クライアントは、認可サーバーごとに別々に登録情報とトークンを保持し、1 つの認可サーバーの認証情報が他で通ると仮定してはいけません (MUST NOT)。
認可サーバーのメタデータ探索
認可サーバーは、RFC 8414 の Authorization Server Metadata と、OpenID Connect Discovery 1.0 の少なくとも一方を提供します (MUST)。クライアントは、両方に対応し (MUST)、次の順でエンドポイントを試します。
issuer にパスがある場合 (https://auth.example.com/tenant1):
https://auth.example.com/.well-known/oauth-authorization-server/tenant1(OAuth 2.0 のパス挿入)https://auth.example.com/.well-known/openid-configuration/tenant1(OIDC のパス挿入)https://auth.example.com/tenant1/.well-known/openid-configuration(OIDC のパス追加)
issuer にパスがない場合 (https://auth.example.com):
https://auth.example.com/.well-known/oauth-authorization-serverhttps://auth.example.com/.well-known/openid-configuration
取得したメタデータは、文書内の issuer が、探索の URL を組み立てるのに使った issuer と完全に一致することを検証します (RFC 8414 セクション 3.3)。一致しなければ、メタデータを使ってはいけません (MUST NOT)。たとえば、https://attacker.example/.well-known/oauth-authorization-server から取得した文書に "issuer": "https://honest.example" と書かれていたら、拒否します。
クライアント登録
認可フローの前に、クライアントは client_id を取得します。仕様は 3 つの方式と、最後の手段としての利用者による入力を定め、すべてに対応するクライアントは、次の優先順位に従います (SHOULD)。
| 優先 | 方式 | 使う場面 |
|---|---|---|
| 1 | 事前登録 | クライアントとサーバーに、すでに関係がある。クライアント ID (必要なら資格情報) を埋め込む、または利用者に入力させる |
| 2 | Client ID Metadata Documents (CIMD) | 認可サーバーが、メタデータの client_id_metadata_document_supported: true で対応を示している。関係のない MCP クライアントとサーバーの、最も一般的なケース |
| 3 | Dynamic Client Registration (DCR) | 認可サーバーが registration_endpoint を持つ。非推奨で、CIMD に未対応の認可サーバーとの後方互換のために残る。2026-07-28 版で非推奨になり (PR #2858)、最短の削除時期は 2027-07-28 以降に公開される最初のリビジョン。削除は Core Maintainers の判断で、自動ではない |
| 4 | 利用者に入力させる | 他の手段がない |
CIMD では、クライアントが、HTTPS の URL を client_id として使い、その URL に、クライアントのメタデータの JSON を置きます。認可サーバーは、URL 形式の client_id を見たら、その文書を取得して検証します。
- クライアント:
client_idはhttpsスキームでパスを含む (例https://example.com/client.json)。文書に、少なくともclient_id、client_name、redirect_urisを含め、client_idが文書の URL と完全に一致すること - 認可サーバー: 文書の
client_idが URL と完全に一致することを検証し (MUST)、認可リクエストのredirect_uriを、文書の値と照合する (MUST)。JSON の構造と必須項目も検証する。HTTP のキャッシュヘッダーに従って、キャッシュする (SHOULD)
CIMD の client_id は、自己ホストの HTTPS URL なので、認可サーバーが変わっても移植できます。DCR で得た登録情報や事前登録の資格情報は、発行した認可サーバーの issuer に紐づけて保持し、認可サーバーが変わったら再登録します (MUST)。別の認可サーバーの資格情報を使い回してはいけません。事前登録の資格情報と合わない認可サーバーが示されたら、黙って試さずにエラーにします (SHOULD)。
DCR を使う場合、クライアントは application_type を適切に指定します (MUST)。省略すると、OIDC では web が既定になり、ネイティブ向けのリダイレクト URI と衝突し得ます。ネイティブアプリ (デスクトップ、モバイル、CLI、localhost で動くローカルの Web アプリ) は native、リモートの Web アプリは web を使います。
認可コードフローと PKCE
ユーザーの認可は、ブラウザーを使う認可コードフローです。クライアントは、PKCE を必ず実装し (MUST)、可能な限り S256 の方式を使います (MUST)。OAuth 2.1 と PKCE は、クライアントが認可サーバーの PKCE 対応を知る方法を定めていないため、MCP クライアントは、認可サーバーのメタデータで確認します。
- OAuth 2.0 の Authorization Server Metadata:
code_challenge_methods_supportedがなければ、PKCE に対応していないので、クライアントは先へ進んではいけません (MUST) - OpenID Connect Discovery: OpenID Provider のメタデータは、このフィールドを定義していませんが、多くの OP が含めています。クライアントは、フィールドの存在を確認し、なければ進んではいけません (MUST)。OIDC の Discovery を提供する認可サーバーは、MCP との互換のために、
code_challenge_methods_supportedを含めます (MUST)
リダイレクト URI は、localhost か HTTPS でなければなりません (MUST)。認可サーバーのすべてのエンドポイントは HTTPS で提供します (MUST)。クライアントは、リダイレクト URI を認可サーバーに登録し、state を使い、一致しなければ破棄します (SHOULD)。認可サーバーは、リダイレクト URI を、登録済みの値と完全一致で検証します (MUST)。
Resource Indicators (RFC 8707)
クライアントは、トークンの宛先となるリソースを明示する、resource パラメーターを実装します (MUST)。
- 認可リクエストとトークンリクエストの両方に含める
- 使おうとしている MCP サーバーを指す
- MCP サーバーの正規 URI (RFC 8707 セクション 2) にする
MCP クライアントは、認可サーバーが resource に対応しているかどうかに関係なく、このパラメーターを必ず送ります。送ることで、将来の対応でトークンが宛先に結びつけられ、サーバー側の audience の検証が機能します。
正規 URI は、RFC 9728 の resource と一致する、リソース識別子です。できるだけ具体的なものを使い、小文字のスキームとホストが正規の形ですが、堅牢性のために、大文字も受け入れます (SHOULD)。
| 例 | 判定 |
|---|---|
https://mcp.example.com/mcp | 有効 |
https://mcp.example.com | 有効 |
https://mcp.example.com:8443 | 有効 |
https://mcp.example.com/server/mcp | 有効 (個々のサーバーの識別にパスが必要な場合) |
mcp.example.com | 無効 (スキームなし) |
https://mcp.example.com#fragment | 無効 (フラグメントを含む) |
末尾のスラッシュは、意味がない限り付けない形に統一します (SHOULD)。
認可レスポンスの検証 (iss、RFC 9207)
リダイレクトの前に、クライアントは、選択した認可サーバーの検証済みメタデータから issuer を記録し、PKCE の code_verifier (と state) と同じ、リクエストごとの記録に紐づけます (MUST)。この検証は、記録した値が本物であることに依存します。検証されていない出所から得た issuer を使うと、保護になりません。
認可サーバーは、認可レスポンスに iss を含めます (SHOULD。エラー応答も含む)。含める認可サーバーは、メタデータで authorization_response_iss_parameter_supported: true を示します。クライアントは、認可コードをトークンエンドポイントへ送る前に、次のとおり検証します (MUST)。
authorization_response_iss_parameter_supported | レスポンスの iss | クライアントの動作 |
|---|---|---|
true | あり | 記録した issuer と、単純な文字列比較をする |
true | なし | レスポンスを拒否する |
false または未設定 | あり | 記録した issuer と、単純な文字列比較をする |
false または未設定 | なし | そのまま進める |
application/x-www-form-urlencoded から iss をデコードした後、比較の前に、スキームやホストの大文字小文字の正規化、既定ポートの省略、末尾のスラッシュ、パーセントエンコーディングの正規化を行ってはいけません (MUST NOT)。この検証は、エラーレスポンスにも同様に適用し、不一致なら error、error_description、error_uri を処理も表示もしてはいけません。将来の改訂で、認可サーバーの iss の付与が SHOULD から MUST に上がる見込みで、今のうちから付与と検証を実装しておくことが推奨されています。
アクセストークンの送り方と検証
クライアントは、OAuth 2.1 のセクション 5 に従い、トークンを Authorization ヘッダーで送ります。
Authorization: Bearer <access-token>
- 認可は、クライアントからサーバーへのすべての HTTP リクエストに含める (MUST)
- アクセストークンを、URI のクエリ文字列に含めてはならない (MUST NOT)
- MCP クライアントは、MCP サーバーの認可サーバーが発行したトークン以外を、MCP サーバーへ送ってはいけない (MUST NOT)
MCP サーバーは、リソースサーバーとして、OAuth 2.1 のセクション 5.2 に従い、アクセストークンを検証します (MUST)。特に、トークンが自分を対象として発行されたことを、RFC 8707 に従い検証します (MUST)。検証に失敗したら、OAuth 2.1 のセクション 5.3 のエラー処理に従い、無効または期限切れのトークンには、HTTP 401 で応答します。
- MCP サーバーは、自分のリソースに有効なトークンだけを受理する (MUST)
- 他のトークンを、受理も転送もしてはいけない (MUST NOT)。下流の API を呼ぶなら、MCP サーバー自身が OAuth クライアントになり、下流の認可サーバーが発行した別のトークンを使う。受け取ったトークンをそのまま渡す「トークンパススルー」は禁止 (詳細は MCPセキュリティベストプラクティス)
スコープ設計とステップアップ
サーバーは、WWW-Authenticate ヘッダーに scope を含め (SHOULD)、リソースへのアクセスに必要なスコープをクライアントに示します。最小権限の原則に沿い、クライアントが過剰な権限を要求しないよう導きます。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
チャレンジのスコープは、scopes_supported と一致しても、部分集合や上位集合でも、別の集合でもよく、クライアントは、いずれの関係も仮定してはいけません (MUST NOT)。クライアントは、チャレンジのスコープを、現在の操作について権威があるものとして扱います (MUST)。
初回の認可で、クライアントは次の優先順位でスコープを選びます (SHOULD)。
- 最初の 401 の
WWW-Authenticateにあるscopeを使う - なければ、Protected Resource Metadata の
scopes_supportedのすべてを使う (scopes_supportedがなければ、scopeを省略する)
scopes_supported は、基本機能に必要な最小のスコープを表すものとして設計し、追加のスコープは、ステップアップのフローで段階的に要求します。
実行時の insufficient_scope: 有効なトークンがあるのに、操作のスコープが足りない場合、サーバーは、次のように応答します (SHOULD)。
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error_description="File write permission required for this operation"
- 現在の操作に必要なスコープを、1 回のチャレンジにまとめて含める。1 つずつ足して再試行させると、1 回の操作のために認可が何度も往復し、体験が悪くなる
- スコープの含め方は、サーバー内で一貫させる
- スコープの階層 (広いスコープが狭いスコープを含む) を、トークンが十分かどうかの判断で考慮する (MUST)
クライアントは、スコープのエラーに、ステップアップ認可で応じます (利用者に代わる場合は SHOULD)。
WWW-Authenticateなどから、エラー情報を解析する- 以前に要求したスコープと、今回のチャレンジのスコープの和集合を求める。操作ごとのチャレンジでも、すでに付与された権限を失わないようにするため。この蓄積は、クライアント側の責務
- その集合で、再認可を始める
- 元のリクエストを、新しい認可で再試行する。再試行は数回までとし、それを超えたら、恒久的な認可の失敗として扱う
クライアントは、再試行の上限を設け、同じリソースと操作の組み合わせで、スコープの引き上げが繰り返し失敗しないよう、試行を記録します (SHOULD)。クライアント自身の資格情報で動く (client_credentials) クライアントは、ステップアップを試みても、すぐに中断してもかまいません (MAY)。
リフレッシュトークン
リフレッシュトークンを求めるクライアントは、次を守ります。
- リフレッシュトークンを、送信中も保管中も機密に保つ (MUST)
- クライアントのメタデータの
grant_typesに、refresh_tokenを含める (SHOULD) - 認可サーバーのメタデータの
scopes_supportedにoffline_accessがあれば、認可・トークンのリクエストのscopeに追加してもよい (MAY) - リフレッシュトークンが発行されると仮定しない (MUST NOT)。発行するかは認可サーバーの裁量
MCP サーバーは、リフレッシュトークンがリソースの要件ではないため、WWW-Authenticate の scope や、Protected Resource Metadata の scopes_supported に offline_access を含めません (SHOULD NOT)。公開クライアントに対しては、認可サーバーがリフレッシュトークンをローテーションします (MUST)。
エラー応答
サーバーは、認可のエラーに適切な HTTP ステータスコードで応答します (MUST)。
| ステータス | 意味 | 用途 |
|---|---|---|
| 401 | Unauthorized | 認可が必要、またはトークンが無効 |
| 403 | Forbidden | スコープが無効、または権限が不足している |
| 400 | Bad Request | 認可リクエストの形式が不正 |
エラーの詳細は、クライアントへは一般的なメッセージを返し、詳しい理由は、相関 ID とともにサーバー内部のログへ残すことが、公式チュートリアルでも勧められています。
2. 認可フローと関連する標準
| ステップ | 標準 | MCP での扱い |
|---|---|---|
| リソースサーバーのメタデータ | RFC 9728 | MCP サーバーが必須実装 |
| 認可サーバーのメタデータ | RFC 8414 / OpenID Connect Discovery | 認可サーバーはどちらか一方を提供、クライアントは両方に対応 |
| クライアント識別 | Client ID Metadata Documents / DCR (RFC 7591) | CIMD を推奨、DCR は互換用に非推奨 |
| 認可リクエスト | OAuth 2.1 + PKCE | 全クライアントで PKCE (S256) を使用 |
| トークンの宛先指定 | RFC 8707 | resource を必ず送信し、サーバーは audience を検証 |
| 発行元の検証 | RFC 9207 | iss を記録済みの issuer と比較 |
| トークンの送信 | RFC 6750 (Bearer) | Authorization ヘッダー。クエリ文字列は禁止 |
実装者ごとの要点
| 実装者 | 主な責務 |
|---|---|
| MCP サーバー | RFC 9728 のメタデータを提供する。未認証には resource_metadata と scope を付けて 401 を返す。トークンの署名、期限、issuer、audience を検証する。不足するスコープは 403 の insufficient_scope で返す。トークンを転送しない |
| MCP クライアント | WWW-Authenticate と Well-Known の両方で探索する。認可サーバーのメタデータの issuer を検証する。PKCE (S256) の対応を確認する。resource を必ず送る。iss を検証する。トークンを保護して保管し、ヘッダーで送る。スコープの和集合で、回数を制限してステップアップする |
| 認可サーバー | RFC 8414 または OIDC Discovery のメタデータを提供する。code_challenge_methods_supported を示す。リダイレクト URI を完全一致で検証する。CIMD を使うなら、SSRF とドメインの信頼ポリシーを考慮する。アクセストークンは短命にし、公開クライアントのリフレッシュトークンはローテーションする |
3. 開発者がつまずきやすいポイント
stdio のサーバーに OAuth を実装しようとする
stdio のサーバーは、この仕様に従わず、環境から資格情報を取得します。OAuth の認可フローは、リモートの HTTP のサーバーのためのものです。ローカルのサーバーの資格情報の扱いは、ローカルMCPサーバーの安全な運用 を参照してください。
トークンの署名と期限だけを検証して、audience を見ない
署名が正しくても、他のリソース向けのトークンを受理してはいけません。IdP が付ける account のような汎用の audience や、api のような一般的な値も許可しません。audience は、自分の正規 URI と照合します。IdP によっては、resource で受け取った値を aud に入れる設定が必要です。固定値の audience は、テスト用の構成にとどめます。
検証ロジックを自作する
署名の検証、JWKS の取得とローテーションへの追随、クレームの検証は、実績のあるライブラリに任せます。自分で書くのは、audience、issuer、スコープといった、サービス固有の判断だけにします。
resource の表記がずれる
https://mcp.example.com/ と https://mcp.example.com を、クライアント、認可サーバー、サーバーの検証で混在させると、audience の不一致になります。末尾のスラッシュなしに統一し、認可サーバーの audience の設定と、サーバー側の audience の比較も、同じルールにそろえます。
認可サーバーのメタデータの issuer を検証しない
探索の URL と、文書内の issuer が一致するかを確認しないと、攻撃者のサーバーが、正直な認可サーバーのふりをできます。記録した issuer が信頼できないと、iss の検証も意味を失います。
PKCE の対応を確認せずに進む
code_challenge_methods_supported がない認可サーバーでは、PKCE が使えず、認可コードの傍受と注入を防げません。クライアントは、先へ進んではいけません。
iss を正規化して比較する
iss の比較は、単純な文字列の比較です。末尾のスラッシュや大文字小文字を直してから比較すると、検証の意味が弱まります。
全スコープを scopes_supported に載せる
初回に、利用者へ、広すぎる同意を求め、漏れたときの被害も広がります。scopes_supported は、基本機能に必要な最小の集合にし、特権操作は、403 の insufficient_scope でステップアップさせます。
スコープの昇格を 1 つずつ要求する
操作に必要なスコープを、1 回のチャレンジにまとめないと、利用者は何度も同意することになります。クライアント側も、和集合を求めずに再認可すると、以前の権限を失い、無限ループの原因になります。
offline_access を、MCP サーバーのメタデータに載せる
リフレッシュトークンはリソースの要件ではありません。クライアントが必要に応じて要求します。
トークンをクエリ文字列やログに出す
アクセストークンは、必ずヘッダーで送り、Authorization ヘッダー、トークン、コード、シークレットを、ログに出さないようにします。クエリ文字列のログへの混入を防ぎ、構造化ログでは、機微なフィールドを伏せます。
DCR を、認証なしで開放する
認証のない DCR は、誰でも任意のクライアントを認可サーバーに登録できることを意味します。許可するホストの制限や、審査、登録の監査を決めます。新規には CIMD を優先します。
マルチテナントの issuer を区別しない
意図的なマルチテナント構成でない限り、1 つの issuer (テナント) に固定します。同じ認可サーバーが署名していても、他の realm のトークンは拒否します。
4. コード例
401 のレスポンスと Protected Resource Metadata
未認証のリクエストへの応答で、メタデータの場所と、必要なスコープを知らせます。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="mcp:tools-basic"
メタデータの例です (/.well-known/oauth-protected-resource)。
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["mcp:tools-basic"]
}
認可サーバーのメタデータ (MCP に関係する項目)
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true,
"authorization_response_iss_parameter_supported": true,
"scopes_supported": ["mcp:tools-basic", "mcp:tools-write", "offline_access"]
}
Client ID Metadata Document
クライアントが https://app.example.com/oauth/client-metadata.json に置く文書です。client_id は、この URL と完全に一致させます。
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"client_uri": "https://app.example.com",
"logo_uri": "https://app.example.com/logo.png",
"redirect_uris": [
"http://127.0.0.1:3000/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
認可リクエストとトークンリクエスト
認可リクエストに resource、code_challenge、scope を含めます (見やすさのため改行しています)。
GET https://auth.example.com/authorize
?response_type=code
&client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient-metadata.json
&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&resource=https%3A%2F%2Fmcp.example.com%2Fmcp
&scope=mcp%3Atools-basic
&state=af0ifjsldkj
トークンリクエストにも resource を含めます。
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback
&client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient-metadata.json
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&resource=https%3A%2F%2Fmcp.example.com%2Fmcp
クライアント: PKCE、メタデータと iss の検証、ステップアップ (TypeScript)
認可サーバーのメタデータの検証、PKCE の対応の確認、iss を含む認可レスポンスの検証、スコープの和集合を示します。
import { createHash, randomBytes } from "node:crypto";
export interface AsMetadata {
issuer: string;
code_challenge_methods_supported?: string[];
authorization_response_iss_parameter_supported?: boolean;
}
/** 認可リクエストごとに記録する。リダイレクトの前に作る。 */
export interface AuthRequestRecord {
issuer: string; // 検証済みのメタデータから取った issuer
issParamSupported: boolean;
state: string;
verifier: string;
}
/** 探索の URL を組み立てた issuer と、文書内の issuer が完全一致することを確認する。 */
export function assertIssuerMatches(expectedIssuer: string, meta: AsMetadata): void {
if (meta.issuer !== expectedIssuer) {
throw new Error(`issuer mismatch: expected ${expectedIssuer}, got ${meta.issuer}`);
}
}
/** code_challenge_methods_supported がなければ、PKCE 非対応として中止する。 */
export function assertPkceSupported(meta: AsMetadata): void {
if (!meta.code_challenge_methods_supported?.includes("S256")) {
throw new Error("authorization server does not advertise S256 PKCE support");
}
}
export function newPkce(): { verifier: string; challenge: string } {
const verifier = randomBytes(32).toString("base64url"); // 43 文字
const challenge = createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
/**
* 認可レスポンスを検証する。認可コードをトークンエンドポイントへ送る前に呼ぶ。
* iss は正規化せず、単純な文字列比較をする。エラー応答にも適用する。
*/
export function validateAuthorizationResponse(
params: URLSearchParams,
rec: AuthRequestRecord,
): { code: string } {
const iss = params.get("iss"); // URLSearchParams が form-urlencoded をデコードする
if (iss === null) {
if (rec.issParamSupported) throw new Error("missing iss in authorization response");
} else if (iss !== rec.issuer) {
// 不一致のとき、error や error_description を表示・処理しない。
throw new Error("iss mismatch: possible mix-up attack");
}
if (params.get("state") !== rec.state) throw new Error("state mismatch");
const error = params.get("error");
if (error !== null) throw new Error(`authorization failed: ${error}`);
const code = params.get("code");
if (code === null) throw new Error("missing code");
return { code };
}
/** ステップアップ: 以前に要求したスコープと、今回のチャレンジの和集合を求める。 */
export function stepUpScopes(previous: string[], challengeScope: string): string[] {
const challenged = challengeScope.split(" ").filter(Boolean);
return [...new Set([...previous, ...challenged])];
}
サーバー: トークンを検証するミドルウェア (Go)
署名、有効期限、issuer、audience を検証し、足りないスコープは 403 の insufficient_scope で返します。JWT の検証は github.com/golang-jwt/jwt/v5 を使い、鍵の取得 (JWKS の取得とローテーション) は、jwt.Keyfunc として渡します。
package mcpauth
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
jwt.RegisteredClaims
Scope string `json:"scope"`
}
type ctxKey struct{}
// FromContext は、検証済みのクレームを返す。ツールの実行時に、sub を利用者の識別に使う。
func FromContext(ctx context.Context) (*Claims, bool) {
c, ok := ctx.Value(ctxKey{}).(*Claims)
return c, ok
}
type Verifier struct {
Resource string // この MCP サーバーの正規 URI (audience として期待する値)
Issuer string // 信頼する認可サーバーの issuer (1 つに固定する)
MetadataURL string // Protected Resource Metadata の URL
Keyfunc jwt.Keyfunc
}
func (v *Verifier) challenge(w http.ResponseWriter, status int, extra string) {
h := fmt.Sprintf(`Bearer resource_metadata=%q`, v.MetadataURL)
if extra != "" {
h += ", " + extra
}
w.Header().Set("WWW-Authenticate", h)
w.WriteHeader(status)
}
// Require は、有効なアクセストークンを要求する。
// 初回の 401 には、必要なスコープ (最小の集合) を scope として含める。
func (v *Verifier) Require(initialScope string, next http.Handler) http.Handler {
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"RS256", "ES256"}), // 署名アルゴリズムを限定する (none を拒否)
jwt.WithIssuer(v.Issuer),
jwt.WithAudience(v.Resource), // 自分宛てのトークンだけを受理する
jwt.WithExpirationRequired(),
)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization") // トークンはヘッダーだけから受け取る (クエリは見ない)
raw, ok := strings.CutPrefix(auth, "Bearer ")
if !ok || raw == "" {
v.challenge(w, http.StatusUnauthorized, fmt.Sprintf("scope=%q", initialScope))
return
}
claims := &Claims{}
if _, err := parser.ParseWithClaims(raw, claims, v.Keyfunc); err != nil {
// 詳しい理由はクライアントへ返さず、サーバー内部のログへ残す。
v.challenge(w, http.StatusUnauthorized, `error="invalid_token"`)
return
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, claims)))
})
}
// RequireScope は、操作に必要なスコープが足りなければ、403 の insufficient_scope を返す。
// 現在の操作に必要なスコープを、1 回のチャレンジにまとめて含める。
func (v *Verifier) RequireScope(next http.Handler, required ...string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := FromContext(r.Context())
if !ok {
v.challenge(w, http.StatusUnauthorized, "")
return
}
have := map[string]bool{}
for _, s := range strings.Fields(claims.Scope) {
have[s] = true
}
for _, s := range required {
if !have[s] {
v.challenge(w, http.StatusForbidden, fmt.Sprintf(
`error="insufficient_scope", scope=%q`, strings.Join(required, " ")))
return
}
}
next.ServeHTTP(w, r)
})
}
スコープの階層 (たとえば admin が read を含む) があるなら、have を作るときに展開します。
サーバー: ASP.NET Core での設定 (C#)
MCP の C# SDK と、ASP.NET Core の標準の JWT Bearer 認証を組み合わせた構成です (公式チュートリアルの構成を、要点に絞っています)。audience を、自分の URI に限定します。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ModelContextProtocol.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
var serverUrl = "https://mcp.example.com/";
var authorizationServerUrl = "https://auth.example.com/";
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = authorizationServerUrl;
options.RequireHttpsMetadata = true; // 本番では HTTPS を必須にする
var audience = serverUrl.TrimEnd('/');
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = authorizationServerUrl,
// 自分宛てのトークンだけを受理する。"account" のような汎用の audience は通さない。
AudienceValidator = (audiences, _, _) =>
audiences != null && audiences.Any(a =>
string.Equals(a.TrimEnd('/'), audience, StringComparison.OrdinalIgnoreCase)),
};
})
.AddMcp(options =>
{
// Protected Resource Metadata (RFC 9728) として公開される
options.ResourceMetadata = new()
{
Resource = serverUrl,
AuthorizationServers = { authorizationServerUrl },
ScopesSupported = ["mcp:tools-basic"],
};
});
builder.Services.AddAuthorization();
builder.Services.AddMcpServer().WithHttpTransport();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp().RequireAuthorization();
app.Run();
5. まとめ
- MCP サーバーは OAuth 2.1 のリソースサーバー、MCP クライアントは OAuth クライアント。認可は任意で、HTTP は仕様に従うことが推奨、stdio は環境から資格情報を取得する
- 未認証には 401 と
WWW-Authenticate(resource_metadata、scope) を返す。RFC 9728 の Protected Resource Metadata は MCP サーバーの必須実装で、クライアントはWWW-Authenticateと Well-Known の両方で探索する - 認可サーバーのメタデータは RFC 8414 と OIDC Discovery の両方に対応し、文書内の
issuerが探索に使った値と一致することを検証する - クライアント登録は、事前登録、CIMD、DCR (非推奨) の順で優先する。認証情報は発行した認可サーバーに紐づけて保持する
- 認可コードフローは PKCE (
S256) を必須とし、code_challenge_methods_supportedがなければ中止する。resourceを認可・トークン両方のリクエストに含め、サーバーは audience を検証する - 認可レスポンスの
issは、記録した issuer と、正規化せずに文字列で比較する。PKCE だけでは Mix-up を防げない - トークンは
Authorization: Bearerで全リクエストに付け、クエリには入れない。サーバーは自分宛てのトークンだけを受理し、他のトークンを転送しない - スコープは最小から始め、足りなければ 403 の
insufficient_scopeで、必要なスコープをまとめて返す。クライアントは和集合で、回数を制限してステップアップする - リフレッシュトークンは機密に保管し、
offline_accessは MCP サーバーのメタデータに載せない。検証はライブラリに任せ、audience、issuer、スコープだけを自分で設定する
関連ドキュメント
参考資料
- Authorization (2026-07-28)
- Authorization Server Discovery
- Client Registration
- Authorization Security Considerations
- Understanding Authorization in MCP
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9207: Authorization Server Issuer Identification
- OAuth Client ID Metadata Document (IETF draft)
- OAuth 2.1 (IETF draft)