メインコンテンツまでスキップ

MCPの認可拡張とエンタープライズ導入

ユーザー委任以外の認可方式 (マシン間、企業 IdP 管理) と、組織で MCP を導入する際の認可・統制の設計を解説します。コアの認可フロー (認可コード + PKCE) は MCPの認可 - OAuth 2.1とメタデータ探索 を先に読み、攻撃と対策は MCPセキュリティベストプラクティス と併せて参照してください。

1. このドキュメントで扱うこと​

  • 認可拡張の位置づけ
  • OAuth Client Credentials
  • Enterprise-Managed Authorization
  • 認可サーバーの選択肢
  • MCP プロキシサーバーの設計
  • ツール単位のスコープ設計
  • ゲートウェイと MCP Server Portals
  • トークンの発行先を絞る (送信者拘束トークン)
  • 監査ログ

認可拡張の位置づけ​

コアの認可仕様は「利用者が、対話的に、MCP クライアントへ自分の代理のアクセスを許可する」という一般的なケースを扱います。しかし、すべての MCP の利用がこの型に合うわけではありません。

  • マシン間 (M2M) の連携には、人間がいない。バックグラウンドのサービス、CI パイプライン、自動化されたワークフローは、対話的な同意なしで認証する必要がある
  • 企業の環境には、すべてのアプリに対してポリシーを強制する、集中管理された IdP がある。従業員に MCP サーバーごとに認可させると、負担が増え、既存のセキュリティ制御も迂回される

この 2 つの隙間を埋めるのが、ext-auth リポジトリで公式に管理されている認可拡張です。性質は次のとおりです。

性質内容
オプトイン採用するかは実装の自由。既定では有効にならない
加算的コアの動作を変更・破壊せず、機能を追加する
組み合わせ可能モジュール式で、複数を同時に採用してもぶつからない
独立してバージョン管理コアの周期に従うが、必要なら独自にバージョンを付けられる

拡張の利用は、MCP の拡張ネゴシエーションで合意します。クライアントは、各リクエストの _meta にある io.modelcontextprotocol/clientCapabilities の extensions で対応を宣言し、サーバーは server/discover が返す capabilities で対応を知らせます。どちらかの側が対応していなければ使えません。対応状況はクライアントによって異なるため、公式の Extension Support Matrix で確認します。

現在の拡張は 2 つです。

拡張識別子解決する問題
OAuth Client Credentialsio.modelcontextprotocol/oauth-client-credentials利用者がいない、マシン間の認証
Enterprise-Managed Authorizationio.modelcontextprotocol/enterprise-managed-authorization企業の IdP による、アクセスの集中管理

OAuth Client Credentials​

OAuth 2.0 の client credentials フロー (RFC 6749 セクション 4.4) を MCP に加える拡張です。クライアントは、利用者の委任された資格情報ではなく、アプリケーション自身の資格情報で認証し、認可サーバーはブラウザーのリダイレクトや利用者の操作なしでアクセストークンを発行します。

使う場面は次のとおりです。

  • イベントやスケジュールで MCP ツールを呼ぶ、バックグラウンドのサービス
  • ビルド、テスト、デプロイの一部として MCP サーバーを呼ぶ CI/CD パイプライン
  • エンドユーザーがいない、バックエンド同士のサーバー間連携
  • MCP リソースへの継続的なアクセスが必要な、デーモンや長時間動くワーカー

人間の利用者が明示的にアクセスを許可すべき連携には、標準の認可フローを使います。

資格情報の形式は 2 つあります。

形式内容位置づけ
JWT Bearer Assertion (RFC 7523)クライアントが秘密鍵で署名した JWT を、本人の証明として提示する。認可サーバーは、登録済みの公開鍵で署名を検証する推奨。有効期間が短く、署名鍵そのものを送らない
クライアントシークレットclient_id と client_secret を、トークンエンドポイントへ直接送る単純な構成向け

JWT アサーションには、通常、次のクレームを含めます。

クレーム内容
issクライアント ID (発行者)
subクライアント ID (認証される主体)
aud認可サーバーのトークンエンドポイントの URL
exp有効期限
iat発行時刻

クライアントシークレットは、利用者の操作なしにアクセスを与える長期の資格情報です。漏れると、ローテーションされるまで、攻撃者がアプリケーションとして、気づかれずに認証できます。シークレットマネージャーに保管し (ソースコードや、バージョン管理されるファイルには置かない)、定期的に、侵害の疑いがあれば直ちにローテーションし、必要最小限のスコープに絞ります。可能なら JWT アサーションを選びます。

実装の責務は次のとおりです。

実装者責務
クライアント拡張を、リクエストごとの capabilities で宣言する。認可サーバーに client credentials で、トークンを取得してから MCP サーバーへ接続する。Authorization: Bearer で送る。トークンの期限は、利用者委任のトークンより短いことが多いため、期限前に更新する
サーバーリクエストごとに、JWT の署名とクレームを、認可サーバーの公開鍵 (通常は JWKS エンドポイント) で検証する。操作に必要なスコープを確認する。発見しやすさのため、server/discover に拡張を載せることが推奨される

コアの認可仕様との関係は次のとおりです。トークンの検証 (audience を含む)、resource パラメーターの送信、HTTPS、ログに資格情報を出さない、のようなコアの要件は、この拡張でも変わりません。また、ステップアップの扱いが異なり、自分自身の資格情報で動く client_credentials のクライアントは、再認可を試みても、すぐに中断してもかまいません (MAY)。

注意すべき点は、このトークンが表すのが人間ではなくクライアントの権限であることです。MCP サーバーは、sub が利用者ではなくサービスを指すことを前提に、認可と監査を設計します (たとえば、利用者ごとのデータへのアクセスを、このトークンに許さない)。

Enterprise-Managed Authorization​

企業の IdP (Okta、Entra ID、社内の SSO など) を、MCP の認可の決定者にする拡張です (SEP-990)。標準のモデルでは、利用者が、MCP クライアントごと、MCP サーバーごとに、個別に認可します。個人向けのアプリには理想的ですが、企業では次の問題が出ます。

  • 従業員が、組織で使うすべての MCP サーバーの認可の細部を理解する必要がある
  • 利用者がばらばらに認可すると、セキュリティ部門は一貫したアクセスポリシーを強制できない
  • 入社時に、数十のサービスを手動で認可しなければならない
  • 退社時に、すべてのサービスで個別に失効させなければならない

この拡張では、IdP が、MCP サーバーへのアクセスを、どの従業員に、どの条件で許可するかを決めます。従業員は、メールや Slack と同じ企業の資格情報で認証し、IdP が、組織のポリシーに基づいて許可または拒否します。

仕組みは、「Identity Assertion JWT Authorization Grant (ID-JAG)」という IETF ドラフトの応用です。MCP クライアントが、IdP から ID-JAG という特別なトークンを取得し、それを MCP サーバーの認可サーバーに提示して、アクセストークンと交換します。

流れは 3 段階です。

  1. SSO: 利用者が、OpenID Connect または SAML で MCP クライアントにログインし、IdP が Identity Assertion (ID トークンまたは SAML アサーション) を発行する。クライアントは、これを後で使うために保存する。SAML の場合は、アサーションをリフレッシュトークンに交換してから使う方法もある
  2. トークン交換 (RFC 8693): クライアントが IdP に、保存した Identity Assertion を提示し、接続したい MCP サーバーの識別子を指定して、ID-JAG を取得する。IdP は、管理者が定義したポリシー (グループ、ロール、条件付きアクセスなど) を評価する
  3. JWT Authorization Grant (RFC 7523): クライアントが ID-JAG を、MCP サーバーの認可サーバーのトークンエンドポイントへ提示し、アクセストークンを得る。MCP 認可サーバーの認可エンドポイントへ、利用者をリダイレクトしない

仕様が、この拡張で定めている主な要件は次のとおりです。

項目要件
トークン交換の audienceMCP サーバーの認可サーバーの issuer 識別子 (MUST)
トークン交換の resource任意。設定するなら、RFC 9728 の MCP サーバーのリソース識別子 (MUST)
ID-JAGIdP が発行・署名する JWT。ヘッダーの typ は oauth-id-jag+jwt。resource があれば、MCP サーバーのリソース識別子
アクセストークンのリクエストgrant_type=urn:ietf:params:oauth:grant-type:jwt-bearer、ID-JAG を assertion に指定
アクセストークンID-JAG の resource で特定される MCP サーバーに、audience を限定する (MUST)
対応の発見認可サーバーのメタデータの authorization_grant_profiles_supported に urn:ietf:params:oauth:grant-profile:id-jag がある

実装の責務は次のとおりです。

実装者責務
MCP クライアント拡張を宣言する。企業の IdP での SSO に対応し、Identity Assertion を保存する。サーバーが要求したら、IdP から ID-JAG を取得し、MCP の認可サーバーに交換する。管理者が IdP のエンドポイントを、組織の設定として構成できるようにする。IdP が発行するトークンのスコープの制限に、適切に対処する
MCP サーバー拡張を使う旨を、認可のメタデータで示す。管理者が IdP の管理コンソールでアクセスポリシーを設定できるよう、リソースの記述子を公開してもよい
MCP 認可サーバーIdP が発行した ID-JAG を検証する (JWKS による署名、audience、issuer、有効期限)。ID-JAG のクレーム (スコープ、リソース) を権限へ対応づける。アカウントのリンクでは、sub を主な安定した識別子とし、拡張の設定前に作られた既存のアカウントとは email で照合する

セキュリティ上の前提と限界も押さえます。

  • クライアントの登録: 多くの企業では、IdP のポリシーが、事前登録されたクライアントだけにサインインを許す。MCP クライアントは、SSO のために、企業の IdP に事前登録する必要がある
  • 可視性の範囲: IdP が見えるのは、アクセストークンの発行までで、MCP クライアントとサーバーの間の実際の通信には及ばない。IdP が強制できるのは、どの利用者が、どのクライアントで、どのサーバー (OAuth スコープの粒度によっては、どのスコープ) を使えるかのポリシー
  • ポリシーの例: 「engineering グループには、ソース管理の MCP サーバーの読み取り専用を与え、marketing グループには、社内ドキュメントの読み書きを与える」
  • 失効: 従業員のアクセスの失効は IdP で行い、すべての MCP クライアントに反映される。ただし、発行済みのアクセストークンが残る間は有効なため、トークンは短命にする

認可サーバーの選択肢​

MCP サーバーを保護するには、OAuth 2.1 の認可サーバーが必要です。選択肢は大きく 2 つです。

選択肢向いている場面注意点
既存の IdP (Entra ID、Auth0、Keycloak など)組織にすでに IdP がある。利用者の管理、MFA、監査を一元化したいMCP が求める機能への対応状況を確認する
ライブラリで自前の認可サーバーを構築するIdP が MCP の要件 (CIMD、resource の audience 化など) に対応しない。サーバーとクライアントの間を自由に設計したいトークンの発行、保管、失効の責任を負う

どちらを選ぶ場合も、次の点を確認します。

確認項目理由
PKCE (S256) とメタデータの code_challenge_methods_supportedクライアントは、広告がなければ進まない
RFC 8707 の resource を受け取り、aud に反映できるかサーバーが audience で、自分宛てのトークンかを検証するため。IdP によっては固定値の audience しか設定できない
Client ID Metadata Documents への対応関係のないクライアントとの接続の、標準の登録方法
iss の付与 (RFC 9207)クライアントが Mix-up 攻撃を防ぐため
DCR の制御使うなら、認証、許可するホスト、登録の監査を決める。DCR は非推奨で、最短の削除時期は 2027-07-28 以降に公開される最初のリビジョンなので、CIMD への移行計画も立てる
アクセストークンの有効期間とリフレッシュトークンのローテーション漏洩時の被害を限定する (公開クライアントのリフレッシュトークンのローテーションは MUST)
複数の MCP サーバーのトークンを 1 つの認可サーバーで発行できるかaudience の分離を保ったまま、集約する

Workers OAuth Provider は、Cloudflare Workers で認可サーバーを作るためのライブラリです (@cloudflare/workers-oauth-provider)。v1 では、認可サーバー (OAuthAuthorizationServer) と MCP サーバー (OAuthResourceServer) を、別々の Worker に分けられます。MCP サーバーは、Service Binding 経由で認可サーバーにトークンを検証させ、公開インターネットを通りません。MCP 2026-07-28 の認可仕様 (CIMD、iss) に対応し、insufficientScope() でステップアップを 1 行で書けます。1 つの認可サーバーが、複数の MCP サーバー向けのトークンを発行することもできます。同意画面と上流のサインインのヘルパーは、confused deputy への対策を実装しています。

Cloudflare のドキュメントは、このライブラリの使い方を 4 つ示しています。

  1. Cloudflare Access を OAuth プロバイダーとして使う
  2. GitHub や Google など、サードパーティの OAuth プロバイダーと直接統合する
  3. 自前の OAuth プロバイダー (Stytch、Auth0、WorkOS のような認可サービスを含む) と統合する
  4. Worker 自身が、認可と認証のすべてを扱う

サードパーティの OAuth プロバイダーを使う場合、MCP の仕様どおり、MCP サーバー (Worker) が自分のトークンを発行して MCP クライアントへ渡します。上流のトークンをそのまま渡してはいけません。

MCP プロキシサーバーの設計​

サードパーティ API の前段に MCP サーバーを置く構成は、client_id の関係が複雑になります。MCP クライアントごとの client_id と、サードパーティへの静的な client_id が別々にあり、これが confused deputy の温床です。設計の原則は次のとおりです。

  • 同意は、プロキシ自身が、MCP クライアントごとに、サードパーティへ転送する前に取る
  • MCP クライアント向けのトークンと、上流の API を呼ぶトークンを分離し、パススルーしない
  • 上流のトークンは、サーバー側で安全に保管し、MCP クライアントへ渡さない
  • redirect_uri は完全一致で、state は同意の後に設定して使い切りにする

具体的な要件とコード例は MCPセキュリティベストプラクティス の Confused deputy とトークンパススルーを参照してください。

ツール単位のスコープ設計​

組織で導入するときは、「誰がどのツールを使えるか」を、スコープで表します。

  • 読み取り、書き込み、破壊的な操作で、スコープを分ける (例: tickets:read、tickets:write、tickets:delete)
  • 初期のスコープは、低リスクな探索・読み取りだけにし、特権操作は、実行時の 403 insufficient_scope で、段階的に昇格させる
  • ワイルドカードや全権限のスコープは作らない。スコープを、サーバー側の認可ロジックなしに、十分だと見なさない
  • 必要なスコープは、現在の操作に対して、1 回のチャレンジにまとめて返す

tools/list の結果は、リクエストに提示された認可によって変わってもよいとされています (たとえば、付与されたスコープで許されるツールだけを返す)。ただし、接続ごとや、他のリクエストの副作用で変えてはいけません。見えないツールを返さない設計は、モデルのコンテキストを減らし、攻撃面も小さくします。ただし、見えることと、実行を許可することは別なので、tools/call のたびに、スコープと利用者の権限を検証します。

ゲートウェイと MCP Server Portals​

社内で多数の MCP サーバーが、管理されずに使われる状態を、「Shadow MCP」と呼びます。従業員が、管理されていないローカルの MCP サーバーを、機密性の高い社内のリソースに接続するリスクです。MCP のガバナンスとは、どの MCP サーバーが組織で使われ、誰が使え、どんな条件で使えるかを、管理者が制御することです。

ゲートウェイは、組織内の MCP サーバーを 1 つのエンドポイントに集約し、認証、ポリシー、監査を一元化する構成です。Cloudflare の MCP Server Portals は、Cloudflare Access の上に、この役割を実装しています。

機能内容
1 つのエンドポイントへの集約複数の MCP サーバー (認証なし、OAuth で保護されたもの、社内のプライベートなサーバーを含む) を、ポータルの 1 つの URL (https://<portal-host>/mcp) で公開する
ポータルへのログイン利用者は、Cloudflare Access を通じて IdP でログインする。OAuth が必要な上流のサーバーごとに、別に認証を求められる
ポリシー誰 (ユーザーやグループ) が、どの条件 (デバイスの状態、場所) で、どのツールを使えるかを定義する
ツールの絞り込みポータルごとに、公開するツールとプロンプトを選ぶ。許可リスト方式 (既定を無効にして、明示的に有効にしたものだけ公開) も可能
名前空間ポータルを通るツールは、{server_id}_{元のツール名} のように、サーバー ID が前置され、名前の衝突を防ぐ
監査Access が、ポータルを通る MCP のリクエストとツールの実行を記録する。Cloudflare Gateway を通すと、HTTP のログと DLP のスキャンも得られる
マシン間の接続OAuth の代わりに、Access のサービストークンで接続できる
コンテキストの最適化ツール定義の最小化や、Code Mode でトークンの使用量を減らせる

ポータルには、設計上の注意があります。

  • 個別の MCP サーバーに Access ポリシーを付けても、サーバーの直接の URL に接続すれば、ブロックされた利用者がポリシーを迂回できる。Access をそのサーバーの OAuth プロバイダーとして構成し、サーバー側でも認証を強制する
  • ポータル経由で認可された MCP サーバーには、独立した MFA、目的の入力、一時的な認証は強制されない
  • 「ユーザーの認証が必要 (Require user auth)」を無効にすると、ポータルに接続した全員が、サーバーの管理者の資格情報でアクセスする。利用者ごとの権限と監査が失われるため、既定の有効のままにする
  • 上流のサーバーのツールやプロンプトの名前の変更は、同期で反映される。別名 (alias) を付けているなら、同期のたびに確認する
  • ポータルは、アクセストークンを中継する層でもある。ポータルの管理者権限と、上流のトークンの保管が、新しい高価値な標的になる

ゲートウェイの種類を問わず、集約によって、監査と、ポリシーの強制の 1 か所を得る代わりに、そこが単一障害点と侵害の標的になります。ゲートウェイを置いても、各 MCP サーバー側のトークンの検証 (audience、スコープ) は省略しないでください。

トークンの発行先を絞る (送信者拘束トークン)​

Bearer トークンは、持っていれば誰でも使えます。盗まれたトークンの悪用を抑えるのが、トークンを特定のクライアントに結びつける、送信者拘束トークン (DPoP、mTLS) です。ただし、今回確認した MCP 2026-07-28 の認可仕様には、DPoP や mTLS の規定はなく、アクセストークンは Bearer で送る前提です。MCP で送信者拘束を使うなら、クライアント、認可サーバー、MCP サーバーが、同じ方式に対応している必要があり、標準のクライアントとの相互運用は期待できません。

仕様の範囲で取れる対策は、次のとおりです。

  • アクセストークンを短命にする (認可サーバーが SHOULD)。公開クライアントのリフレッシュトークンは、ローテーションする (MUST)
  • トークンを、resource (audience) で、特定の MCP サーバーに限定する
  • スコープを最小にし、段階的に昇格させる
  • トークンを、ログ、クエリ文字列、平文の設定ファイルに残さない

組織内で、クライアント、認可サーバー、MCP サーバーのすべてを管理できる M2M の連携では、mTLS 等の送信者拘束を、追加の防御として検討する余地があります。方式の詳細は 送信者拘束トークン(DPoP / mTLS)とリフレッシュトークンローテーション を参照してください。

監査ログ​

組織で導入するなら、事故の調査と、コンプライアンスのために、誰が、どのツールを、どの引数で、どのスコープで実行したかを記録できなければなりません。仕様も、クライアントはツールの使用を監査のために記録すること (SHOULD) を求め、MCP 公式のチュートリアルも、サーバーに、スコープの昇格のイベント (要求したスコープ、付与された部分集合) を、相関 ID とともに記録することを勧めています。

項目記録する内容
主体利用者 (sub)、クライアント (client_id や azp)、M2M なのか委任なのか
操作ツール名、結果 (成功、拒否、エラー)、所要時間
認可トークンのスコープ、insufficient_scope や昇格の履歴、ポリシーの判断
引数機微でない項目は値を、機微な項目は、ハッシュや項目名だけを
相関リクエスト ID、相関 ID。ゲートウェイ、サーバー、下流の API をまたぐ追跡のため

トークンそのもの、認可コード、シークレット、個人情報の本文は、記録しません。ログは改ざんされにくい場所に集め、保持期間とアクセス権を決めます。詳しい設計は MCPの可観測性と監査 を参照してください。

2. シナリオ別の認可方式​

シナリオ推奨される方式補足
ユーザーの権限で動かすリモートサーバー認可コード + PKCE (CIMD で登録)標準の認可フロー。拡張は不要
バッチ・CI などのマシン間Client Credentials 拡張ユーザーの権限ではなくクライアント自身の権限で動く。JWT アサーションを推奨
企業 IdP でアクセスを一元管理Enterprise-Managed Authorizationポリシーを IdP 側で制御。クライアントを IdP に事前登録する
組織内の多数のサーバーを集約ゲートウェイ (MCP Server Portals など)認証、ポリシー、監査を一元化。各サーバーのトークン検証は残す
ローカルの stdio サーバー環境変数などから資格情報を取得OAuth 仕様には従わない
下流 API を呼ぶ MCP サーバーMCP 用トークンと下流用トークンを分離トークンパススルーは禁止

3. 開発者がつまずきやすいポイント​

拡張を宣言せずに使う​

拡張は、既定では有効ではありません。クライアントは _meta の capabilities で宣言し、サーバー側も対応を示します。クライアントの対応状況は、実装ごとに違うため、導入前に Extension Support Matrix を確認します。

Client Credentials のトークンを利用者の権限として扱う​

このトークンの主体はクライアント (サービス) です。利用者ごとのデータを、サービスの権限でまとめて読めるようにすると、アクセス制御と監査が崩れます。サービスに必要な最小のスコープだけを与え、利用者のデータにアクセスさせないなら、そうサーバー側で強制します。

クライアントシークレットを使い回す、設定ファイルに置く​

長期の資格情報は、漏れると静かに悪用されます。サービスごとに別のシークレットを発行し、シークレットマネージャーに置き、ローテーションを前提にします。JWT アサーションなら、署名鍵を送らず、期間も短くできます。

Enterprise-Managed Authorization で、認可エンドポイントへ誘導する​

この拡張では、利用者を MCP の認可サーバーの認可エンドポイントへリダイレクトしません。ID-JAG を jwt-bearer の assertion として、トークンエンドポイントへ提示します。

ID-JAG の audience と resource を取り違える​

audience は、MCP サーバーの認可サーバーの issuer で、resource は、MCP サーバーのリソース識別子です。認可サーバー側は、発行するアクセストークンの audience を、ID-JAG の resource で特定される MCP サーバーに限定します。

ID-JAG の iss を信頼しすぎる​

認可サーバーは、信頼する IdP の iss を allowlist で決め、その IdP の公開鍵で署名を検証します。マルチテナントの IdP では、issuer だけでなく、sub の一意性にも注意します (sub は IdP の内部で一意で、iss と組み合わせて識別する)。

アカウントのリンクを email だけで行う​

sub が主な安定した識別子です。email は、拡張を設定する前に作られた既存のアカウントと照合するための、補助的なフォールバックです。

IdP のポリシーが MCP の通信全体を制御すると考える​

IdP が関与するのは、アクセストークンの発行までです。その後の MCP の通信の内容や、トークンの有効期間内の振る舞いは制御できません。短命のトークンと、MCP サーバー側のスコープの検証を組み合わせます。

ゲートウェイを置いたから、サーバー側の検証は不要と考える​

ゲートウェイを迂回する直接の URL、ゲートウェイの設定の誤り、管理者の資格情報での代行などで、サーバーが直接呼ばれる可能性は残ります。サーバーは、audience、スコープ、利用者の権限を、自分で検証します。

ポータルの「ユーザーの認証が必要」を無効にする​

管理者の資格情報を共有することになり、誰がどの操作をしたかが、上流のサーバーで区別できなくなります。有効のままにします。

引数をそのまま監査ログに入れる​

引数に、個人情報やシークレットが含まれることがあります。記録する項目を allowlist で決め、他はハッシュ、または項目名だけにします。

DPoP を入れれば MCP でも安全になると考える​

2026-07-28 の認可仕様に DPoP の規定はなく、標準のクライアントやサーバーは対応していない可能性があります。まず、短命のトークン、audience の限定、最小スコープを徹底します。

4. コード例​

拡張の宣言 (クライアントとサーバー)​

クライアントは、各リクエストの _meta で拡張を宣言します (Client Credentials の例)。

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/oauth-client-credentials": {}
}
}
}
}
}

サーバーは、server/discover のレスポンスの capabilities で対応を知らせます。

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"extensions": {
"io.modelcontextprotocol/oauth-client-credentials": {}
}
}
}
}

Enterprise-Managed Authorization は、識別子を io.modelcontextprotocol/enterprise-managed-authorization にします。

Client Credentials: 公式 SDK でのクライアント (TypeScript)​

公式の TypeScript SDK は、トークンの取得と更新を自動で行います。JWT の秘密鍵を使う例です (シークレットを使うなら ClientCredentialsProvider)。

import {
Client,
PrivateKeyJwtProvider,
StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";

const provider = new PrivateKeyJwtProvider({
clientId: "my-service",
privateKey: process.env.CLIENT_PRIVATE_KEY_PEM, // シークレットマネージャーから注入する
algorithm: "RS256",
});

const client = new Client(
{ name: "my-service", version: "1.0.0" },
{ capabilities: {} },
);

const transport = new StreamableHTTPClientTransport(
new URL("https://mcp.example.com/mcp"),
{ authProvider: provider },
);

await client.connect(transport);
const tools = await client.listTools();
await transport.close();

Client Credentials: JWT アサーションを自分で作る (Go)​

SDK を使えない場合の例です。署名した JWT を、jwt-bearer の assertion として、resource と scope を付けてトークンエンドポイントへ送ります。有効期間は数分に絞り、リクエストごとに jti を変えます。

package m2m

import (
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"net/http"
"net/url"
"strings"
"time"

"github.com/golang-jwt/jwt/v5"
)

// NewAssertion は、クライアント自身が署名した JWT アサーションを作る。
func NewAssertion(clientID, tokenEndpoint string, key *rsa.PrivateKey) (string, error) {
jti := make([]byte, 16)
if _, err := rand.Read(jti); err != nil {
return "", err
}
now := time.Now()
claims := jwt.RegisteredClaims{
Issuer: clientID,
Subject: clientID,
Audience: jwt.ClaimStrings{tokenEndpoint}, // 認可サーバーのトークンエンドポイント
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)), // 短命にする
ID: hex.EncodeToString(jti),
}
return jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key)
}

// TokenRequest は、トークンエンドポイントへのリクエストを組み立てる。
// resource は、MCP サーバーの正規 URI。認可サーバーが対応しているかにかかわらず必ず送る。
func TokenRequest(tokenEndpoint, assertion, resource, scope string) (*http.Request, error) {
form := url.Values{
"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
"assertion": {assertion},
"resource": {resource},
"scope": {scope},
}
req, err := http.NewRequest(http.MethodPost, tokenEndpoint, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}

Enterprise-Managed Authorization: ID-JAG のトークン交換​

クライアントが IdP のトークンエンドポイントへ送る、トークン交換のリクエストです (公式仕様の例)。audience は MCP の認可サーバーの issuer、resource は MCP サーバーのリソース識別子です。

POST /oauth2/token HTTP/1.1
Host: acme.idp.example
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&audience=https://auth.chat.example/
&resource=https://mcp.chat.example/
&scope=chat.read+chat.history
&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&client_id=2ec954a1d60620116d36d9ceb7
&client_secret=...

IdP が発行する ID-JAG は、次のようなクレームを持つ JWT です (ヘッダーの typ は oauth-id-jag+jwt)。

{
"jti": "9e43f81b64a33f20116179",
"iss": "https://acme.idp.example",
"sub": "U019488227",
"email": "[email protected]",
"aud": "https://auth.chat.example/",
"resource": "https://mcp.chat.example/",
"client_id": "f53f191f9311af35",
"exp": 1311281970,
"iat": 1311280970,
"scope": "chat.read chat.history"
}

クライアントは、この ID-JAG を、MCP の認可サーバーへ次のように提示します。

POST /oauth2/token HTTP/1.1
Host: auth.chat.example
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJIUzI1NiIsI...
&client_id=https://client.example.com/client.json

Enterprise-Managed Authorization: 認可サーバーで ID-JAG を検証する (Go)​

信頼する IdP の iss だけを allowlist で受け付け、その IdP の鍵で署名を検証し、typ、aud、有効期限、resource を確認します。発行するアクセストークンの audience は、resource で特定される MCP サーバーに限定します。

package idjag

import (
"errors"
"fmt"
"slices"

"github.com/golang-jwt/jwt/v5"
)

type Claims struct {
jwt.RegisteredClaims
Email string `json:"email,omitempty"`
Resource string `json:"resource,omitempty"`
ClientID string `json:"client_id,omitempty"`
Scope string `json:"scope,omitempty"`
}

type Validator struct {
Issuer string // この認可サーバーの issuer。ID-JAG の aud と一致させる
TrustedIdP map[string]jwt.Keyfunc // 信頼する IdP の iss から、その鍵を返す関数への対応 (JWKS)
Resources []string // この認可サーバーがトークンを発行する MCP サーバーのリソース識別子
}

func (v *Validator) Validate(raw string) (*Claims, error) {
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"RS256", "ES256"}),
jwt.WithAudience(v.Issuer),
jwt.WithExpirationRequired(),
)
claims := &Claims{}
_, err := parser.ParseWithClaims(raw, claims, func(t *jwt.Token) (any, error) {
if typ, _ := t.Header["typ"].(string); typ != "oauth-id-jag+jwt" {
return nil, fmt.Errorf("unexpected typ %q", typ)
}
c, ok := t.Claims.(*Claims)
if !ok {
return nil, errors.New("unexpected claims")
}
// 署名の検証に使う鍵は、allowlist にある IdP のものだけ。
keyfunc, ok := v.TrustedIdP[c.Issuer]
if !ok {
return nil, fmt.Errorf("untrusted issuer %q", c.Issuer)
}
return keyfunc(t)
})
if err != nil {
return nil, err
}
if claims.Subject == "" {
return nil, errors.New("missing sub")
}
// アクセストークンの audience を、resource で特定される MCP サーバーに限定する。
if claims.Resource == "" || !slices.Contains(v.Resources, claims.Resource) {
return nil, fmt.Errorf("unknown resource %q", claims.Resource)
}
return claims, nil
}

検証に成功したら、利用者は iss と sub の組で識別し、既存のアカウントとのリンクは、email を補助にします。付与するスコープは、ID-JAG の scope を上限に、この認可サーバー側のポリシーで絞ります。

ツールごとの必要スコープをサーバーで強制する (Go)​

ツール名から必要なスコープへの対応表を持ち、tools/call のたびに検証します。足りなければ、403 の insufficient_scope を、現在の操作に必要なスコープをまとめて返します。未登録のツールは拒否します。

package toolauthz

import (
"fmt"
"net/http"
"slices"
"strings"
)

// 運用者が審査して宣言する。未登録のツールは呼べない (deny by default)。
var requiredScopes = map[string][]string{
"tickets_search": {"tickets:read"},
"tickets_update": {"tickets:read", "tickets:write"},
"tickets_delete": {"tickets:read", "tickets:write", "tickets:delete"},
}

// Check は、トークンのスコープ (空白区切り) が、ツールに十分かを確認する。
// 足りなければ、challenge に WWW-Authenticate の値を返す。
func Check(tool, tokenScope, metadataURL string) (status int, challenge string) {
need, ok := requiredScopes[tool]
if !ok {
return http.StatusForbidden, `Bearer error="insufficient_scope"`
}
have := strings.Fields(tokenScope)
for _, s := range need {
if !slices.Contains(have, s) {
return http.StatusForbidden, fmt.Sprintf(
`Bearer error="insufficient_scope", scope=%q, resource_metadata=%q`,
strings.Join(need, " "), metadataURL)
}
}
return http.StatusOK, ""
}

Workers OAuth Provider: 認可サーバーと MCP サーバーを分ける​

公式のチェンジログにある、v1 の分割 API の例です (要点のみ)。認可サーバーの Worker がトークンを発行し、MCP サーバーの Worker が Service Binding 経由で検証します。書き込みには、insufficientScope() でステップアップを要求します。

import {
OAuthAuthorizationServer,
OAuthResourceServer,
insufficientScope,
} from "@cloudflare/workers-oauth-provider";
import { WorkerEntrypoint } from "cloudflare:workers";

// 認可サーバーの Worker: 利用者のサインインとトークンの発行
const authorizationServer = new OAuthAuthorizationServer<Env>({
issuer: "https://auth.example.com",
resources: ["https://calendar.example.com/mcp"],
scopesSupported: ["calendar:read", "calendar:write", "offline_access"],
clientIdMetadataDocumentEnabled: true,
});

export class AuthServer extends WorkerEntrypoint<Env> {
fetch(request: Request) {
if (new URL(request.url).pathname === "/authorize") {
return showConsent(request, this.env); // 自前の同意画面
}
return authorizationServer.fetch(request, this.env, this.ctx);
}

validateToken(resource: string, token: string) {
return authorizationServer.validateToken(resource, token, this.env);
}
}

// MCP サーバーの Worker: Service Binding で、認可サーバーにトークンを検証させる
export const calendar = new OAuthResourceServer<Env, AuthProps>({
resourceMetadata: {
resource: "https://calendar.example.com/mcp",
authorization_servers: ["https://auth.example.com"],
},
requiredScopes: ["calendar:read"],
validateToken: (env) => env.AUTH_SERVER.validateToken,
handler: {
fetch(request, env, ctx) {
if (request.method === "POST" && !ctx.auth.scope.includes("calendar:write")) {
return insufficientScope(ctx.auth, ["calendar:read", "calendar:write"]);
}
return handleMcp(request, ctx.props);
},
},
});

MCP Server Portals: 許可リストで公開ツールを絞る​

ポータルの API で、サーバーの既定を無効にし (default_disabled)、公開するツールだけを updated_tools で有効にします。ポータルの更新で servers を渡すと、配列がサーバーとポータルの対応の全体を置き換えるため、接続しているすべてのサーバーと、既存の設定を含めます。

{
"servers": [
{
"server_id": "example-server",
"default_disabled": true,
"updated_tools": [
{ "name": "search_documents", "enabled": true },
{ "name": "list_projects", "enabled": true }
]
}
]
}

監査ログを構造化して出す (Go)​

引数は、allowlist の項目だけを値で、他は項目名とハッシュだけを記録します。トークンは記録しません。

package audit

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"log/slog"
"sort"
"time"
)

// 値をそのまま記録してよい、機微でない引数の項目。
var loggableArgs = map[string]bool{"status": true, "limit": true, "project": true}

type Event struct {
RequestID string
Subject string // トークンの sub。M2M ならクライアント自身
ClientID string
Delegated bool // true なら利用者の委任、false なら client credentials
Tool string
Scope string
Args map[string]any
Outcome string // ok / denied / error
Duration time.Duration
}

func Log(l *slog.Logger, e Event) {
args := make([]any, 0, len(e.Args))
keys := make([]string, 0, len(e.Args))
for k := range e.Args {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if loggableArgs[k] {
args = append(args, slog.Any(k, e.Args[k]))
continue
}
sum := sha256.Sum256([]byte(k + "=" + toString(e.Args[k])))
args = append(args, slog.String(k+"_sha256", hex.EncodeToString(sum[:8])))
}
l.Info("mcp.tool_call",
slog.String("request_id", e.RequestID),
slog.String("sub", e.Subject),
slog.String("client_id", e.ClientID),
slog.Bool("delegated", e.Delegated),
slog.String("tool", e.Tool),
slog.String("scope", e.Scope),
slog.Group("args", args...),
slog.String("outcome", e.Outcome),
slog.Duration("duration", e.Duration),
)
}

func toString(v any) string {
b, _ := json.Marshal(v)
return string(b)
}

5. まとめ​

  • 認可拡張は、コアの認可仕様に加えられる、オプトイン・加算的・組み合わせ可能・独立してバージョン管理される仕組み。クライアントが _meta で宣言し、サーバーが server/discover で示す。既定では有効にならず、クライアントの対応状況はさまざま
  • Client Credentials は、利用者のいない M2M 用で、トークンの主体はクライアント自身。JWT Bearer Assertion (RFC 7523) を推奨し、クライアントシークレットは長期の資格情報としてシークレットマネージャーで管理し、ローテーションする
  • Enterprise-Managed Authorization は、企業の IdP が、ID-JAG を通じてアクセスを制御する。クライアントは IdP でのトークン交換で ID-JAG を得て、MCP の認可サーバーのトークンエンドポイントに提示する。IdP が関与するのはトークンの発行までで、通信の内容は見えない
  • 認可サーバーは、既存の IdP か、ライブラリで構築する。PKCE、resource の audience 化、CIMD、iss、DCR の制御、トークンの有効期間を確認する。Workers OAuth Provider v1 は、認可サーバーと MCP サーバーを分離できる
  • プロキシは、クライアントごとの同意を取り、MCP 用と下流用のトークンを分ける。ツール単位のスコープは、読み取り、書き込み、破壊的な操作で分け、tools/call のたびに検証する
  • ゲートウェイや MCP Server Portals は、認証、ポリシー、監査を集約し、Shadow MCP を防ぐ。ただし、各サーバーのトークン検証は残し、ポータルの「ユーザーの認証が必要」は有効にする
  • DPoP や mTLS は、今回確認した MCP の認可仕様には規定がない。短命のトークン、audience の限定、最小スコープを優先する
  • 監査ログには、誰が、どのツールを、どの引数で、どのスコープで実行したかを、相関 ID とともに記録する。トークンと機微な引数は記録しない

関連ドキュメント​

参考資料​