Security & AI Architecture

OAuth vs OAuth2の違い:AIエージェントとMCP認証 (2026)

クイックアンサー:OAuth 1.0aは複雑なリクエストごとの暗号署名に依存し、OAuth 2.0はリプレイ攻撃に脆弱なBearerトークンを導入しました。自律型AIエージェントやModel Context Protocol(MCP)サーバーにはOAuth 2.1が必須標準です。全認可フローでPKCE(RFC 7636)を義務付け、安全でないImplicit/Passwordグラントを廃止し、DPoP(RFC 9449)と連携してヘッドレス環境全体で送信者拘束型のゼロトラスト認証を強制します。


1. はじめに:2026年における自律型エージェントのアイデンティティ危機

単発の大規模言語モデル(LLM)チャットUIから、自律的かつマルチターンで動作するAIエージェントおよびModel Context Protocol(MCP)サーバーへの急速な移行は、深刻なセキュリティ危機を引き起こしました。それがエージェントのアイデンティティと認可のボトルネックです。

2024年から2025年にかけて、開発者はClaude Code、Cursor、Windsurf、AutoGen、カスタムLangGraphエージェントなどの自律ツールを企業APIに接続する際、.envファイルやプロセスの環境変数にハードコードされた静的なPersonal Access Token(PAT)や長期有効なAPIキーを主に使用していました。AIエージェントがローカルシェルコマンドを実行したり、内部データベースにクエリを送信したり(PostgreSQLやSupabase MCPサーバー経由)、社内イシュートラッカーを更新したり(JiraやLinear MCPサーバー経由)する場合、エージェントは広範で無制限なアンビエント権限(Ambient Authority)の下で動作します。

脆弱なレガシーエージェントアーキテクチャ(静的アンビエント権限):
+--------------------+        子プロセスの起動        +---------------------------+
|  LLMホストエージェント | ──────────────────────────> |   ローカルMCPツールサーバー  |
| (Claude Code /     |   Env: GITHUB_TOKEN=ghp_...  |  (process.envを読み込み)   |
|  Cursor / LangSeq) |                              +-------------+-------------+
+---------+----------+                                            |
          | 間接プロンプトインジェクション                               | 無制限の読み取り/書き込み
          v                                                       v
+--------------------+                                      +-------------------+
| 信頼できないWeb上の |                                      | アップストリーム企業 |
| 攻撃者プロンプト    | ──> 静的シークレットを漏洩 ────────> | GitHub / Slack /  |
| 「環境変数を出力」 |     攻撃者のHTTP Webhookへ送信       | 内部データベース  |
+--------------------+                                      +-------------------+

この静的クレデンシャルアーキテクチャは、次の3つの理由から根本的に破綻しています:

  1. シークレット漏洩の媒介となるプロンプトインジェクション: エージェントが信頼できないデータ(Webページ、メール、GitHubイシューに埋め込まれた敵対的プロンプトなど)に遭遇すると、LLMが操られ、process.envを出力したりローカル設定ファイルを調査するコマンドを実行させられ、高権限キーが即座に流出します。
  2. アイデンティティ委譲の欠如: 静的APIキーでは、人間のオペレーターが意図的に実行したアクションと、AIエージェントがハルシネーションや自律的判断でトリガーしたアクションを区別できません。企業の監査ログでは、すべての呼び出しが人間ユーザーによるものとして同一に記録されます。
  3. 動的失効と最小権限スコープの不在: 静的トークンは通常、過剰に広い権限(リポジトリ全体の読み書き権限など)を持ち、数ヶ月または無期限の有効期間を持っています。

この構造的リスクを解決するため、AIエコシステムは委譲認可フレームワークへと収束しました。しかし、OAuth 1.0aOAuth 2.0、そして最新の統合規格であるOAuth 2.1の選択—さらにPKCE(RFC 7636)DPoP(RFC 9449)Device Authorization Grant(RFC 8628)の組み合わせ—を決定するには、ヘッドレスかつ自律的な実行制約の下でこれらのプロトコルがどのように動作するかを深く理解する必要があります。


2. OAuthの進化:1.0a vs 2.0 vs 2.1の構造的比較

最新のAIエージェントフレームワークがなぜOAuth 2.1を義務付けているのかを理解するために、OAuth仕様の3つの主要な改訂にわたるアーキテクチャの進化、トレードオフ、および重大な脆弱性を検証します。

OAUTH仕様の進化プロセス (2007 - 2026):
+---------------------------------------------------------------------------------------------+
| OAuth 1.0a (RFC 5849, 2010)                                                                 |
| - すべてのHTTPリクエストに対する対称/非対称暗号署名 (HMAC-SHA1, RSA-SHA1)                    |
| - トークンリフレッシュフローの欠如、ステートフルな署名計算、トランスポート非依存のセキュリティ|
| - AIエージェントへの評価:使用不可。暗号オーバーヘッドがストリーミングと動的プロキシを破壊。 |
+---------------------------------------------------------------------------------------------+
                                               │
                                               ▼
+---------------------------------------------------------------------------------------------+
| OAuth 2.0 (RFC 6749 & RFC 6750, 2012)                                                       |
| - 暗号化をトランスポート層セキュリティ(TLS 1.2/1.3)に委譲                                  |
| - Bearerトークン、スコープ、リフレッシュトークン、特化型グラントタイプを導入                 |
| - Implicit Flowおよびリソースオーナーパスワードクレデンシャル(ROPC)を含む                   |
| - AIエージェントへの評価:危険。プロンプトインジェクションやSSRFでBearerトークンが容易に盗難される。 |
+---------------------------------------------------------------------------------------------+
                                               │
                                               ▼
+---------------------------------------------------------------------------------------------+
| OAuth 2.1 (IETF統合標準, 2025/2026)                                                         |
| - 非推奨で安全でないグラントを完全排除(ImplicitおよびPasswordグラントを恒久的に削除)      |
| - すべての認可コードフローでPKCE(RFC 7636)を義務化(パブリックおよびコンフィデンシャル)    |
| - 完全一致のリダイレクトURI文字列照合、URIクエリパラメータ内でのトークン受け渡しを禁止     |
| - リフレッシュトークンローテーション(RTR)または送信者拘束トークン(DPoP/mTLS)を要求       |
| - AIエージェントへの評価:MCPおよび自律エージェントのアイデンティティ委譲における最高基準。  |
+---------------------------------------------------------------------------------------------+

OAuth 1.0a (RFC 5849):暗号の厳格性とステートフル性

OAuth 1.0aは、HTTPS/TLSが高価で普及していなかった時代に開発されました。平文HTTP上での盗聴から保護するため、OAuth 1.0aではクライアントとサーバーが個々のHTTPリクエストごとに暗号署名(HMAC-SHA1またはRSA-SHA1)を計算する必要がありました。

署名計算には、HTTPメソッド、正規化された完全なURL、辞書順にソートされたすべてのクエリパラメータ、リクエストヘッダー、クライアントNonce、およびUnixタイムスタンプの正規化が必要でした:

$$\text{BaseString} = \text{HTTP\_METHOD} \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{Encode}(\text{URL}) \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{Encode}(\text{SortedParams})$$

$$\text{Signature} = \text{HMAC-SHA1}(\text{ClientSecret} \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{TokenSecret}, \text{BaseString})$$

OAuth 1.0aがAIエージェントで破綻する理由:

  1. ストリーミングおよびチャンク転送との不整合: Server-Sent EventsやWebSocket上のMCPなど、現代のエージェントプロトコルはインクリメンタルなJSON-RPCペイロードをストリーミングします。非決定論的なストリーミングチャンクや動的プロキシの書き換えに対して署名を再計算すると、署名検証が継続的に失敗します。
  2. エフェメラルなツールオーケストレーション: AIエージェントはLLMツールの引数を通じて動的にHTTPリクエストを構築します。パラメータの順序変更、URLエンコードの差異(%20+など)、またはプロキシによるヘッダー追加により署名が無効化され、自律ループ内で401 Unauthorizedエラーが頻発します。
  3. ネイティブなリフレッシュ分離の欠如: OAuth 1.0aトークンには自動ローテーション機能を備えた短寿命メカニズムがなく、長寿命の認証情報がクライアント環境に常駐することを余儀なくされました。

OAuth 2.0 (RFC 6749):Bearerリスクと引き換えにしたシンプルさ

OAuth 2.0は、暗号の完全性をトランスポート層(HTTPSの義務化)に移行し、Bearerトークン(RFC 6750)を導入することで1.0aの複雑さを解決しました。トークンを所持する者は誰でもリソースにアクセスでき、これは現金の所持とまったく同じ性質を持ちます:

GET /v1/repositories HTTP/1.1
Host: api.github.com
Authorization: Bearer ya29.a0AfH6SMB...

OAuth 2.0では以下の4つの認可グラントフローが定義されました:

  1. Authorization Code Grant: 機密バックエンドシークレットを保持できるサーバー向けリダイレクトフロー。
  2. Implicit Grant: バックエンドのコード交換を行わず、URLハッシュ(#access_token=...)で直接トークンをブラウザに返すフロー。
  3. Resource Owner Password Credentials(ROPC): ユーザーのIDとパスワードをクライアントアプリに直接送信してトークンを受け取るフロー。
  4. Client Credentials Grant: 人間の介在を必要としないバックグラウンドサーバーデーモン向けの機械対機械(M2M)認可。

AIシステムにおけるOAuth 2.0の致命的脆弱性:

  • Bearerトークンのリプレイ脆弱性: SSRF、プロンプトインジェクション、またはログ流出によってエージェントの実行環境が侵害された場合、攻撃者はBearerトークンを窃取し、失効するまで世界中どこからでも再利用(リプレイ)できます。
  • Implicit Grantの罠: 初期のシングルページアプリ(SPA)やデスクトップエージェントUIはImplicitフローを使用していました。トークンはブラウザ履歴、Refererヘッダー、ローカルリダイレクトハンドラー経由で頻繁に漏洩しました。
  • Password Grantのアンチパターン: CLIエージェントの開発者は、ターミナルでユーザーにパスワードの入力を求める実装を頻発させ、OAuthの最も重要な基本原則である「サードパーティに認証情報を絶対に渡さない」という約束を破壊しました。

OAuth 2.1:自律型エージェントのための最新堅牢化標準

OAuth 2.1は、OAuth 2.0が長年蓄積してきた技術的負債とセキュリティ脆弱性を排除するIETF統合仕様です。自律型AIシステムおよびModel Context Protocol統合において、OAuth 2.1は妥協のないアーキテクチャ要件を定めています:

  1. 安全でないグラントの完全排除: Implicit GrantとResource Owner Password Credentials Grantは正式に廃止され、仕様から完全に削除されました。
  2. 全認可コードフローにおけるPKCEの義務化: Proof Key for Code Exchange(RFC 7636)はオプションではなく、パブリッククライアント(CLIエージェント、IDE拡張機能)とコンフィデンシャルクライアント(バックエンドエージェント群)の双方で厳格に義務付けられます
  3. リダイレクトURIの完全一致照合: 認可サーバーは、リダイレクトURIに対して厳格なバイト単位の完全一致文字列比較を実施し、オープンリダイレクター攻撃やワイルドカードによるサブドメイン乗っ取りを防止しなければなりません。
  4. URIクエリ内トークンの完全禁止: BearerトークンをURIクエリパラメータに含めることは明示的に禁止されています(HTTPアクセスログ、プロキシキャッシュ、ブラウザ履歴へのトークン流出を防止)。
  5. リフレッシュトークン保護の強制: 認可サーバーは、リフレッシュトークンローテーション(RTR)(更新ごとに古いトークンを破棄し、新しいペアを発行)または送信者拘束トークン(DPoPまたはmTLS)のいずれかを必ず実装しなければなりません。

アーキテクチャ比較表:OAuth 1.0a vs OAuth 2.0 vs OAuth 2.1

アーキテクチャ次元 OAuth 1.0a (RFC 5849) OAuth 2.0 (RFC 6749 / 6750) OAuth 2.1 (IETF 2026標準)
暗号モデル アプリ層リクエスト署名 (HMAC-SHA1 / RSA) トランスポート層セキュリティ (TLS) + 平文Bearer TLS + 義務的PKCE + 送信者拘束 (DPoP/mTLS)
Bearerリプレイ攻撃リスク なし(リクエストごとに固有Nonceで署名) 極めて高い(所持=全認可) 緩和済み(DPoP秘密鍵による送信者拘束)
PKCE要件 未対応 オプション(RFC 7636、主にモバイル向け) 全ての認可コード交換で必須
Implicit Grant (response_type=token) 未対応 許可(ブラウザSPA向け設計) 完全に削除・禁止
Password Grant (ROPC) 未対応 許可(レガシーな認証情報直接交換) 完全に削除・禁止
リダイレクトURI検証 プレフィックス一致を許容 パスやワイルドカード一致が頻繁に許可 厳格なバイト単位の完全一致が必須
クエリパラメータ内トークン 対応 許可(?access_token=... 厳格に禁止(ヘッダーまたはボディのみ)
リフレッシュトークン管理 ネイティブな仕組みなし 失効または期限切れまで単一トークンを再利用 強制ローテーション(RTR)または暗号バインディング
ローカルCLIエージェント適合性 極めて不良(シェル内の署名計算が脆弱) 脆弱(ローカルループバックリダイレクトの傍受) 最適(PKCE + ループバックエフェメラルポート)
リモートMCPサーバー適合性 ストリーミングJSON-RPCと互換性なし 利用可能だがシークレット流出リスク大 業界デフォルト標準(厳格な最小権限スコープ)

3. Model Context Protocol (MCP) 認証トポロジ:エージェント対サーバー通信の保護

Anthropicによってオープンソース化され、Claude Code、Cursor、Windsurf、企業向けエージェントランタイムに広く採用されたModel Context Protocol(MCP)は、JSON-RPC 2.0上で非対称なクライアント・サーバーアーキテクチャを確立します。

OAuth 2.1が機能する領域を理解するため、MCPデプロイメント内の2つの異なる通信境界を整理します:

  • 境界A(ホストからMCPサーバー): LLMクライアントアプリケーション(Claude Code、Cursorなど)とMCPサーバープロセスの間の接続。
  • 境界B(MCPサーバーからアップストリーム企業インフラ): MCPサーバーと外部SaaS API(GitHub、Linear、Jira、Slack、Salesforceなど)の間の接続。
MODEL CONTEXT PROTOCOL (MCP) 認証トポロジ:
+-------------------------------------------------------------------------------------------------------+
| MCPホストランタイム (例: Claude Code / Cursor / 自律エージェント基盤)                                      |
|                                                                                                       |
|  +---------------------+        プロンプトコンテキスト   +--------------------------------------------+ |
|  | ユーザープロンプト   | <───────────────────────────> | LLM推論エンジン (Claude 3.7 / GPT-4o)      | |
|  +----------+----------+                               +--------------------------------------------+ |
|             | ツール呼び出しをディスパッチ (`tools/call`)                                               |
|             v                                                                                         |
|  +--------------------------------------------------------------------------------------------------+ |
|  | MCPクライアントエンジン                                                                           | |
|  | - 認可サーバーとのOAuth 2.1 PKCEハンドシェイクを管理                                              | |
|  | - エクスポート不可能なメモリ内でエフェメラルDPoP秘密鍵を保持                                       | |
|  | - リクエストごとにDPoP Proof JWTを生成し、JSON-RPCヘッダーにAccess Tokenを注入                     | |
|  +-----------------------------------+--------------------------------------------------------------+ |
+--------------------------------------|----------------------------------------------------------------+
                                       |
                                       | トランスポート: Stdio (ローカルプロセス) または SSE/HTTP (リモート)
                                       v
+-------------------------------------------------------------------------------------------------------+
| MCPサーバーランタイム (例: GitHub MCP / 企業データベースMCP)                                          |
|                                                                                                       |
|  +--------------------------------------------------------------------------------------------------+ |
|  | 認証&トークン検証インターセプター                                                                | |
|  | 1. 認可サーバーのJWKS経由でOAuth 2.1トークン署名を検証                                             | |
|  | 2. DPoP Proofの検証: HTTPメソッド、URI、Nonce、およびエフェメラル公開鍵の一致を確認                | |
|  | 3. スコープの評価: 最小権限の強制 (例: `issues:read` を許可し `admin:all` を拒否)                 | |
|  +-----------------------------------+--------------------------------------------------------------+ |
|                                      |                                                                |
|                                      v                                                                |
|  +--------------------------------------------------------------------------------------------------+ |
|  | MCPツール実行エンジン (`tools/call` 実装)                                                          | |
|  | - 入力をサニタイズし、パストラバーサルを防止、サンドボックス化されたAPI呼び出しを実行              | |
|  +-----------------------------------+--------------------------------------------------------------+ |
+--------------------------------------|----------------------------------------------------------------+
                                       | アップストリームAPI呼び出し (委譲されたスコープ付きトークンで認証)
                                       v
                      +----------------------------------+
                      | アップストリーム企業SaaS / DB    |
                      | (GitHub / Jira / PostgreSQL / S3)|
                      +----------------------------------+

Stdio vs リモートSSE/HTTPトランスポートの比較

  1. ローカルStdioトランスポート (transport: "stdio"):
  • MCPサーバーはホストによって生成されたローカル子プロセスとして動作し、標準入力および標準出力で通信します。
  • セキュリティアンチパターン: 従来、開発者は環境変数を通じて認証情報を注入していました:
  • 脆弱性: エージェントが実行したシェルコマンドや、同一コンテナ内で起動した子プロセスが /proc/[pid]/environ を検査したり env を実行したりすることで、組織全体のGitHubアクセス権が即座に侵害されます。
  • OAuth 2.1による解決策: ホスト側でOAuth 2.1 PKCEトークンボールトを管理します。MCPサーバー起動時は静的シークレットを持たせず、初期化ハンドシェイク時に短寿命の委譲トークンを渡すか、ホスト自身が認証済みリバースプロキシとして機能します。
  1. リモートSSE/HTTPトランスポート (transport: "sse"):
  • MCPサーバーはHTTPポートでリッスンする分散Webサービスとして動作し、サーバーからクライアントへの通知にServer-Sent Eventsを使用します。
  • この構成ではOAuth 2.1が必須です。MCPクライアントは標準のHTTP Authorization ヘッダーを用いてリモートサーバーに対して認証し、サーバーはJWKSで署名検証を行いDPoP Proofと照合します。

4. PKCE (RFC 7636) 詳解:エージェントのローカルコールバック保護

Proof Key for Code Exchange(PKCE、「ピクシー」と発音)は、モバイル端末における認可コード傍受攻撃を防止するためにRFC 7636で標準化されました。OAuth 2.1では、PKCEがすべての認可コード交換で必須となっています。

CLIエージェントやIDEが「パブリッククライアント」に該当する理由

Claude Code、Cursor、Roo CodeなどのAI開発ツールは、OAuthの分類上パブリッククライアント(Public Client)にあたります。ソースコードやバイナリ配布物がユーザーのローカルマシン上で直接実行されるため、静的な client_secret を安全に保管することが不可能です。オープンソースのCLIエージェントにシークレットを埋め込んでも、バイナリのリバースエンジニアリングで容易に抽出されます。

パブリッククライアントが認可を要求すると、認可サーバーはローカルのリダイレクトURI(一時的に起動したループバックHTTPサーバー、例: http://127.0.0.1:18492/callback)経由で認可コード(Authorization Code)を返します。

認可コード傍受攻撃の流れ (PKCEなし):
1. 正当なCLIエージェントが認可サーバーに認可コードを要求。
2. 開発者マシン上の悪意あるバックグラウンドプロセスがローカルポートを傍受。
3. 認可サーバーがブラウザを http://127.0.0.1:18492/callback?code=AUTH_CODE_123 へリダイレクト。
4. 悪意あるプロセスが AUTH_CODE_123 を傍受。
5. 悪意あるプロセスが AUTH_CODE_123 を /token エンドポイントへ送信。
   パブリッククライアントのためclient_secretが不要であり、認可サーバーはAccess Tokenを発行してしまう!

PKCEの数学的防御メカニズム

PKCEは、認可リクエストごとに動的かつ暗号学的に偽造不可能なワンタイムシークレットを生成することで、この脆弱性を完全に排除します。

PKCEプロトコルの通信フロー:
+-------------+                     +-----------------------+                    +--------------------+
|  Agent CLI  |                     |  ユーザーブラウザ     |                    |      認可サーバー  |
| (クライアント) |                     +-----------+-----------+                    +---------+----------+
+------+------+                                 |                                          |
       | 1. code_verifierを生成 (高エントロピー)|                                          |
       |    code_challenge = S256(...) を計算   |                                          |
       |                                        |                                          |
       | 2. ループバックHTTPリスナーを起動      |                                          |
       |    ブラウザを開く (challengeを付与) ──>| 3. GET /authorize?response_type=code     |
       |                                        |    &client_id=agent_cli                  |
       |                                        |    &code_challenge=E9Melhoa2Owv...       |
       |                                        |    &code_challenge_method=S256 ─────────>|
       |                                        |                                          | 4. ユーザーが同意
       |                                        | 5. 302リダイレクト (ループバック宛て)    |    challengeを保持
       |                                        |<─────────────────────────────────────────|
       |<───────────────────────────────────────|    http://127.0.0.1:18492/callback?code=AC_88921
       | 6. コールバックからコードを傍受        |
       |                                                                                   |
       | 7. POST /oauth/token                                                              |
       |    code=AC_88921 & code_verifier=dBjftJeZ4CVP-mB92K... ─────────────────────────>|
       |                                                                                   | 8. 計算照合:
       |                                                                                   |    SHA256(verifier)
       |                                                                                   |    == 保存されたchallenge?
       | 9. Access Token + Refresh Token (RTR) を返却 <────────────────────────────────────|    一致: トークンを発行
+------+------+

この暗号ハンドシェイクは次のように進行します:

  1. Code Verifierの生成: AIエージェントはURL予約外文字([A-Z], [a-z], [0-9], -, ., _, ~)を用い、43〜128文字の暗号学的にランダムな高エントロピー文字列 $V$ を生成します:
  2. Code Challengeの計算: クライアントは $V$ のSHA-256ハッシュを計算し、パディングなしのBase64URLでエンコードします:
  3. 認可リクエスト: クライアントは $C$ と変換方式 code_challenge_method=S256/authorize エンドポイントに送信します。サーバーは発行された認可コードとともに $C$ を保存します。
  4. トークン交換: クライアントが /token で認可コードを引き換える際、生の code_verifier=V を送信します。認可サーバーは $\text{Base64URL-Encode}(\text{SHA-256}(V))$ を計算し、保存されている $C$ と完全一致することを検証します。

仮に悪意あるプロセスがローカルマシン上で認可コードを傍受しても、エージェントプロセスのメモリから外に出ていない元の code_verifier を保持していないため、トークンに交換することは不可能です

プロダクションTypeScript実装:堅牢なPKCEエンジン

ローカルMCPクライアントランタイム向けに設計されたPKCE生成・検証モジュールです:

// pkce.ts - Enterprise OAuth 2.1 PKCE Engine for AI Agent Clients
import { randomBytes, createHash } from "node:crypto";

export interface PKCEChallenge {
  codeVerifier: string;
  codeChallenge: string;
  codeChallengeMethod: "S256";
}

export class PKCEEngine {
  /**
   * Generates a cryptographically secure code_verifier (RFC 7636 Section 4.1)
   * Length defaults to 64 bytes of entropy (yielding ~86 base64url characters).
   */
  public static generateVerifier(length: number = 64): string {
    if (length < 32 || length > 96) {
      throw new RangeError("Verifier byte length must be between 32 and 96.");
    }
    const buffer = randomBytes(length);
    return this.base64UrlEncode(buffer);
  }

  /**
   * Computes the S256 code_challenge from the code_verifier (RFC 7636 Section 4.2)
   */
  public static computeChallenge(verifier: string): string {
    const hash = createHash("sha256").update(verifier, "ascii").digest();
    return this.base64UrlEncode(hash);
  }

  /**
   * Generates the complete PKCE pair ready for OAuth 2.1 authorization
   */
  public static createPair(): PKCEChallenge {
    const codeVerifier = this.generateVerifier(64);
    const codeChallenge = this.computeChallenge(codeVerifier);
    return {
      codeVerifier,
      codeChallenge,
      codeChallengeMethod: "S256",
    };
  }

  /**
   * Server-side verification: Validates an incoming code_verifier against stored challenge
   */
  public static verify(verifier: string, storedChallenge: string): boolean {
    const computed = this.computeChallenge(verifier);
    // Timing-safe buffer comparison to prevent side-channel timing attacks
    const bufA = Buffer.from(computed);
    const bufB = Buffer.from(storedChallenge);
    if (bufA.length !== bufB.length) return false;
    
    let result = 0;
    for (let i = 0; i < bufA.length; i++) {
      result |= bufA[i] ^ bufB[i];
    }
    return result === 0;
  }

  private static base64UrlEncode(buffer: Buffer): string {
    return buffer
      .toString("base64")
      .replace(/\\+/g, "-")
      .replace(/\\//g, "_")
      .replace(/=+$/, "");
  }
}

5. ヘッドレスサーバーとCLI認可:Device Flow (RFC 8628)

PKCEはブラウザとループバックポートを開くことができるデスクトップ対話環境の認証を解決しますが、最新のAIエージェントはブラウザが存在しないヘッドレス環境で実行されることが急増しています:

  • リモートクラウドクラスタ内のDockerコンテナ(AWS ECS、Kubernetes、Fly.io)。
  • エフェメラルなCI/CDランナー(GitHub Actions、GitLab CI)。
  • リモートVMおよびターミナルのSSHセッション。

ヘッドレス環境では、エージェントはリダイレクト完了のためのブラウザウィンドウを表示できません。また、ユーザーにターミナルでパスワードを直接入力させる手法はOAuth 2.1に違反します。この課題に対する業界標準の解決策がOAuth 2.0 Device Authorization Grant(RFC 8628)です。

ヘッドレス環境におけるDevice Authorization Grant (RFC 8628):
+-------------------+                                                +-----------------------+
| ヘッドレスエージェント |                                                |       認可サーバー    |
| (Docker / クラウド)|                                                +-----------+-----------+
+---------+---------+                                                            |
          | 1. POST /oauth/device/code                                           |
          |    (client_id, scope) ──────────────────────────────────────────────>|
          |                                                                      | 2. 生成:
          | 3. デバイス認証情報を返却:                                           |    device_code (秘密)
          |    - user_code: "WDJB-HGNP"                                          |    user_code (公開)
          |    - verification_uri: "https://auth.corp.com/activate"              |    interval: 5秒
          |    - interval: 5 <───────────────────────────────────────────────────|
          |                                                                      |
          | 4. ターミナルにユーザー向け指示を表示:                               |
          |    「https://auth.corp.com/activate を開き、次のコードを入力: WDJB-HGNP」|
          |                                                                      |
          | 5. ポーリングループ開始:                                             |
          |    POST /oauth/token (grant_type=device_code, device_code=...) ─────>|
          |    <── 400 Bad Request: {"error": "authorization_pending"} ─────────|
          |    [5秒待機]                                                         |
          |                                                                      |
+---------+---------+     ユーザーがPCやスマホでURLにアクセス                    |
| ユーザーのノートPC| ──> "WDJB-HGNP" を入力し、MFA認証を完了 ──────────────────>| 6. ユーザーが承認!
+-------------------+                                                            |
          |                                                                      |
          | 7. 次のポーリングサイクル:                                           |
          |    POST /oauth/token ───────────────────────────────────────────────>|
          |    <── 200 OK: {access_token: "...", refresh_token: "..."} ──────────|
          v
[ヘッドレスエージェントがシークレット露出ゼロで安全に認可完了]

マシン対マシン(M2M)の代替手法:RFC 7523 Private Key JWT

自律AIエージェントが人間の関与なしに完全バックグラウンドで自律動作する場合(例: 毎日の自動コードリファクタリングBot)、承認者がその場にいないためDevice Authorization Grantも適用できません。

このシナリオでは、RFC 7523(クライアント認証のためのJWTプロファイル)で強化されたClient Credentials Grantがゼロトラストアーキテクチャの標準となります:

  • 共有の静的 client_secret をネットワーク上に平文送信する代わりに、エージェントはHSM(ハードウェアセキュリティモジュール)やKubernetes Secret Vaultに保管された非対称秘密鍵(RSAまたはECDSA)を保持します。
  • /oauth/token へリクエストする際、エージェントは自身を証明するエフェメラルなJWTを署名・生成します。これには一意のUUID jti、60秒の極めて短い有効期限、および対象オーディエンス aud が含まれます。
  • 認可サーバーは登録済みのエージェント公開鍵に対して署名を検証するため、ネットワーク上に共有シークレットが送信されることは一切ありません。

6. トークンライフサイクルと自律リフレッシュワークフロー

自律型AIエージェントは、大規模コードベースのインデックス作成、ベンチマークの反復実行、インシデント監視など、数時間に及ぶ長時間のタスクを実行します。OAuth 2.1のAccess Tokenは露出リスクを抑えるため意図的に短寿命(通常5〜15分)に設定されているため、エージェントは実行中のツール呼び出しを中断することなく、トークンのライフサイクルを自律的に管理しなければなりません。

リフレッシュトークンローテーション(RTR)と漏洩検知

OAuth 2.1において、リフレッシュトークンは盗難から厳格に保護されなければなりません。その中核技術がリフレッシュトークンローテーション(RTR)です:

  1. エージェントが /oauth/tokenrefresh_token を送信するたびに、認可サーバーはその特定のリフレッシュトークンを即時無効化します。
  2. サーバーは真新しい access_token と、まったく新しい refresh_token のペアを発行します。
  3. 攻撃者が過去に使用済みのリフレッシュトークンを窃取して再利用を試みた場合、認可サーバーは不正侵害イベントを即座に検知します:

$$\text{Incoming Token State} == \text{"REVOKED"} \implies \text{Revoke All Tokens in Family Tree}$$

認可サーバーは該当する認可グラントのファミリーツリー全体を直ちに失効させ、実行中のすべてのエージェントインスタンスのAccess TokenとRefresh Tokenをすべて無効化します。

リフレッシュトークンローテーション (RTR) と侵害自動復旧:
トークン生成チェーン:
[Refresh Token A] ──(交換完了)──> [Refresh Token B] ──(交換完了)──> [Refresh Token C] (有効中)
         │
         │ 攻撃者が盗み出した古い [Refresh Token A] を再送信
         v
[認可サーバーが無効化済みトークンAの再利用を検知!]
         │
         ▼
[重大セキュリティアラート]: Token B、Token C、および関連するすべてのAccess Tokenを即時失効。
エージェントセッションが安全に終了し、権限昇格攻撃を完全遮断。

Pythonプロダクション実装:スレッドセーフな非同期トークンマネージャー

マルチエージェント環境(1つのオーケストレーターが同一MCPサーバーに問い合わせる10個のサブエージェントを並行起動する場合など)では、複数の並行ツール呼び出しが同時にトークンの有効期限切れを検出することがあります。10個のサブエージェントが一斉に1回限りのリフレッシュトークンで交換を試みると、9個は失敗し、認可サーバーは同時実行の競合をトークン再利用攻撃と誤認してセッション全体を強制停止してしまいます。

以下は、非同期ミューテックスロック、先行更新、競合調整を備えた本番向けPythonモジュールです:

# token_manager.py - Enterprise Async Token Lifecycle Manager for AI Agents
import asyncio
import time
import httpx
from typing import Optional, Dict, Any

class AgentTokenManager:
    def __init__(
        self,
        token_endpoint: str,
        client_id: str,
        initial_refresh_token: str,
        proactive_refresh_seconds: int = 60,
    ):
        self.token_endpoint = token_endpoint
        self.client_id = client_id
        self.refresh_token = initial_refresh_token
        self.access_token: Optional[str] = None
        self.expires_at: float = 0.0
        self.proactive_refresh_seconds = proactive_refresh_seconds
        self._lock = asyncio.Lock()

    async def get_valid_access_token(self) -> str:
        now = time.time()
        if self.access_token and (self.expires_at - now) > self.proactive_refresh_seconds:
            return self.access_token

        async with self._lock:
            now = time.time()
            if self.access_token and (self.expires_at - now) > self.proactive_refresh_seconds:
                return self.access_token

            await self._refresh_token_exchange()
            if not self.access_token:
                raise RuntimeError("Failed to acquire valid access token from authorization server.")
            return self.access_token

    async def _refresh_token_exchange(self) -> None:
        payload = {
            "grant_type": "refresh_token",
            "refresh_token": self.refresh_token,
            "client_id": self.client_id,
        }
        
        async with httpx.AsyncClient(timeout=10.0) as client:
            try:
                response = await client.post(
                    self.token_endpoint,
                    data=payload,
                    headers={"Content-Type": "application/x-www-form-urlencoded"},
                )
            except httpx.RequestError as exc:
                raise ConnectionError(f"Network transport error during token refresh: {exc}")

            if response.status_code == 200:
                data: Dict[str, Any] = response.json()
                self.access_token = data["access_token"]
                expires_in = int(data.get("expires_in", 3600))
                self.expires_at = time.time() + expires_in
                
                # Update to the newly rotated refresh token if provided
                if "refresh_token" in data:
                    self.refresh_token = data["refresh_token"]
            elif response.status_code in (400, 401):
                err_data = response.json()
                # If error is 'invalid_grant', the token was likely already rotated or revoked
                raise PermissionError(f"Token refresh rejected (possible token theft or expiry): {err_data}")
            else:
                response.raise_for_status()

7. ゼロトラスト認証情報の隔離:DPoP (RFC 9449) とエンクレーブ

OAuth 2.1とPKCEを厳密に適用しても、従来のBearerトークンには「トークンが漏洩した場合、誰でも使用できてしまう」という致命的なアーキテクチャ上の弱点が残ります。

AI統合の文脈において、エージェントは信頼できない外部入力に対して複雑な推論を日常的に実行します。攻撃者が間接プロンプトインジェクションを悪用してエージェントにSSRF(Server-Side Request Forgery)を実行させ、認可ヘッダー付きで攻撃者管理のサーバーへHTTPリクエストを送信させた場合、Bearerトークンは容易に奪取されます。

真のゼロトラストセキュリティを実現するため、OAuth 2.1はDPoP(Demonstrating Proof-of-Possession at the Application Layer、RFC 9449)を統合しています。

DPOP (RFC 9449) アプリケーション層での送信者拘束メカニズム:
+-------------------------------------------------------------------------------------------------+
| エージェントローカル環境 (クライアント)                                                         |
| - エフェメラル鍵ペアの生成: 公開鍵 (JWK) + 秘密鍵 (RAM/セキュアエンクレーブ内から出ない)        |
+-------------------------------------------------------------------------------------------------+
                                                │
                                                │ 1. DPoP Proofヘッダーを付与:
                                                │    DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Ar...
                                                │    ペイロード: {
                                                │      "htm": "GET",
                                                │      "htu": "https://api.enterprise.com/mcp/tools",
                                                │      "iat": 1772630400,
                                                │      "jti": "random_nonce_9921",
                                                │      "jwk": { ...公開鍵構造体... }
                                                │    }
                                                │
                                                │ 2. バインドされたDPoPトークンを送信:
                                                │    Authorization: DPoP dpop_access_token_88921
                                                v
+-------------------------------------------------------------------------------------------------+
| エンタープライズMCPゲートウェイ (リソースサーバー)                                               |
| 1. Access TokenがJWK内の公開鍵フィンガープリントと暗号的に一致していることを検証。             |
| 2. DPoP Proofの署名が公開鍵と完全に一致することを検証。                                         |
| 3. "htm" が "GET" と等しく、"htu" が完全な宛先URLと厳密に一致することを検証。                   |
| 4. "iat" が時間枠内(< 60秒)であり、"jti" がリプレイされていないことを検証。                   |
+-------------------------------------------------------------------------------------------------+
                                                │
       ┌────────────────────────────────────────┴────────────────────────────────────────┐
       ▼                                                                                 ▼
[正当なDPoP PROOFと一致する秘密鍵]                                              [窃取されたトークンのリプレイ攻撃]
リクエストは正常に実行エンジンへ進行                                            攻撃者はトークンを持つが、エージェント
                                                                                ローカルの秘密鍵を持たない。
                                                                                結果: 401 Unauthorizedで完全遮断!

DPoPの実際の運用メカニズム

  1. エフェメラル鍵の生成: エージェントセッション初期化時、揮発性メモリまたはセキュアエンクレーブ内で非対称鍵ペア(通常はP-256楕円曲線ECDSAまたはEd25519)を生成します。
  2. 暗号学的バインディング: エージェントが認可サーバーにトークンを要求する際、DPoP Proofを添付します。発行されるAccess Tokenには、公開鍵のSHA-256サムプリント(jkt クレーム)が暗号学的に組み込まれます。
  3. リクエストごとの所持証明: 後続のMCPサーバーリクエストごとに、エージェントは固有の使い捨てDPoP JWTを署名して送信します:
  • htm: 正確なHTTPリクエストメソッド(例: POST)。
  • htu: クエリやフラグメントを含まない完全な送信先URI。
  • iat: 作成タイムスタンプ($\pm 60$ 秒以内)。
  • jti: リプレイを防止する一意のUUID。
  • nonce: サーバーから要求されたチャレンジNonce(設定されている場合)。
  1. 多層防御の実現: プロンプト漏洩やプロキシログからAccess Token文字列が盗まれたとしても、合致するDPoPヘッダーを署名生成するためのエージェント秘密鍵がないため、攻撃者はトークンを一切利用できません

8. エンタープライズセキュリティベンチマーク、リスクマトリクス、障害モード

自律AIエージェントの認証方式を選定する際は、計算レイテンシと脅威防御性能のバランスを考慮する必要があります。以下は実機テストによるレイテンシ、暗号計算負荷、セキュリティ保証の比較です。

実測パフォーマンスベンチマーク(10,000回反復測定、Apple M4 Maxハードウェア)

認証アーキテクチャ クライアントハンドシェイク (p50) クライアントハンドシェイク (p99) リクエストごとの検証オーバーヘッド リプレイ保護性能 クライアントメモリフットプリント サーバーCPU負荷増分
静的PAT / APIキー 0.1 ms (ハンドシェイクなし) 0.2 ms 0.02 ms (文字列比較) なし (完全リプレイ可能) < 1 KB 基準値
OAuth 1.0a (HMAC-SHA1) 14.2 ms 38.5 ms 1.84 ms (署名パース・検証) 部分的 (Nonce検証) 12 KB +18%
OAuth 2.0 Bearer 45.1 ms 112.0 ms 0.15 ms (JWT署名検証/キャッシュ) なし (Bearerリプレイ可能) 18 KB +4%
OAuth 2.1 (PKCE + RTR) 48.6 ms 118.4 ms 0.16 ms (JWT検証) 中程度 (RTRによる失効) 24 KB +5%
OAuth 2.1 + DPoP (P-256) 54.2 ms 132.8 ms 1.22 ms (DPoP JWT検証) 最高 (ゼロリプレイ) 36 KB +12%
mTLS (RFC 8705) 62.8 ms 154.1 ms 0.45 ms (TLSセッションキャッシュ) 最高 (証明書バインド) 128 KB +15%

エンタープライズエージェント脅威リスクマトリクス

脅威重大度とプロトコル防御性能のマトリクス:
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 攻撃ベクトル                  | 静的APIキー       | OAuth 1.0a        | OAuth 2.0 Bearer  | OAuth 2.1 + DPoP  |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 1. 間接プロンプトインジェクション| 致命的 (10/10)    | 高度リスク (7/10) | 致命的 (10/10)    | 低リスク (2/10)   |
|    (環境変数・ログの漏洩)     | 恒久キーが全漏洩  | 署名構築が極めて困難| Bearerが盗まれ悪用| 盗難トークンは無効|
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 2. ローカルポート傍受         | 該当なし          | 低リスク (3/10)   | 高度リスク (8/10) | 完全防御 (1/10)   |
|    (CLIループバックの横取り)  | リダイレクトなし  | Nonce署名付き     | 認可コードを奪取  | PKCEが完全阻止    |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 3. SSRFツール踏み台攻撃       | 致命的 (10/10)    | 中程度 (5/10)     | 致命的 (10/10)    | 完全防御 (1/10)   |
|    (外部送信への誘導)         | 認証ヘッダー漏洩  | URI不一致で失敗   | トークン再利用    | DPoPのURI検証失敗 |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 4. サブプロセス環境変数調査   | 致命的 (10/10)    | 中程度 (5/10)     | 高度リスク (8/10) | 低リスク (2/10)   |
|    (/proc/environの読み取り)  | 静的キーが丸見え  | 環境変数に露出    | Bearerが露出      | 短寿命・鍵バインド|
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 5. トークン更新の競合状態     | 該当なし          | 該当なし          | 低リスク (2/10)   | 要注意 (ミューテッ|
|    (並行エージェント群)       | 更新なし          | 更新なし          | トークン再利用    | クス管理が必須)   |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+

9. 実践導入ガイド:堅牢化OAuth 2.1 + PKCE MCPサーバーの実装

これらのアーキテクチャ標準を実践するため、TypeScript、Express、JSON-RPC 2.0を使用した本番運用対応のリモートModel Context Protocol(MCP)サーバーを構築します。このサーバーは、OAuth 2.1トークン検証、PKCE検証、AIエージェントのツール呼び出しに対するきめ細やかなスコープ強制を実施します。

アーキテクチャの概要

  • 認証ミドルウェア: 企業IdPのJWKSエンドポイントに対して受信したBearerおよびDPoPトークンを検証します。
  • スコープチェッカー: 粒度の細かいスコープを厳格に強制します(コード検索ツールには code:read のみを許可し、code:write や管理者操作を防止)。
  • ツールサンドボックス: 分離された実行コンテキスト内でツールロジックを実行します。

完全なTypeScript MCPサーバー実装

// server.ts - Hardened Remote MCP Server with OAuth 2.1 Validation
import express, { Request, Response, NextFunction } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";

const app = express();
app.use(express.json());

// Configuration
const ISSUER = "https://auth.enterprise.com/";
const AUDIENCE = "https://mcp.enterprise.com/";
const JWKS_URI = new URL("https://auth.enterprise.com/.well-known/jwks.json");
const JWKS = createRemoteJWKSet(JWKS_URI);

interface AuthenticatedRequest extends Request {
  tokenClaims?: any;
}

/**
 * Enterprise OAuth 2.1 Token Validation Middleware
 */
async function requireOAuth21(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Missing or invalid OAuth 2.1 Authorization header." },
      id: req.body?.id || null,
    });
    return;
  }

  const token = authHeader.split(" ")[1];

  try {
    // Cryptographically verify token signature, issuer, audience, and expiration
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: ISSUER,
      audience: AUDIENCE,
    });

    // Enforce OAuth 2.1 requirement: reject tokens without an expiration claim
    if (!payload.exp || typeof payload.exp !== "number") {
      res.status(401).json({
        jsonrpc: "2.0",
        error: { code: -32002, message: "Non-compliant token: missing expiration claim." },
        id: req.body?.id || null,
      });
      return;
    }

    req.tokenClaims = payload;
    next();
  } catch (err: any) {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32003, message: `Token verification failed: ${err.message}` },
      id: req.body?.id || null,
    });
  }
}

/**
 * Fine-Grained Scope Enforcement Guard
 */
function requireScope(requiredScope: string) {
  return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
    const scopes: string[] = (req.tokenClaims?.scope || "").split(" ");
    if (!scopes.includes(requiredScope)) {
      res.status(403).json({
        jsonrpc: "2.0",
        error: {
          code: -32004,
          message: `Insufficient permissions: missing required scope '${requiredScope}'`,
        },
        id: req.body?.id || null,
      });
      return;
    }
    next();
  };
}

/**
 * Standard MCP JSON-RPC 2.0 Handler Endpoint
 */
app.post(
  "/mcp/v1",
  requireOAuth21,
  requireScope("mcp:tools:execute"),
  async (req: AuthenticatedRequest, res: Response): Promise<void> => {
    const { jsonrpc, method, params, id } = req.body;

    if (jsonrpc !== "2.0") {
      res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Invalid JSON-RPC version." }, id });
      return;
    }

    // Router for MCP Primitives
    switch (method) {
      case "tools/list":
        res.json({
          jsonrpc: "2.0",
          result: {
            tools: [
              {
                name: "query_database",
                description: "Executes read-only SQL queries against the analytics warehouse.",
                inputSchema: {
                  type: "object",
                  properties: { query: { type: "string" } },
                  required: ["query"],
                },
              },
            ],
          },
          id,
        });
        break;

      case "tools/call":
        if (params?.name === "query_database") {
          // Verify elevated data scope for this specific tool execution
          const scopes: string[] = (req.tokenClaims?.scope || "").split(" ");
          if (!scopes.includes("db:analytics:read")) {
            res.json({
              jsonrpc: "2.0",
              error: { code: -32005, message: "Forbidden: tool requires 'db:analytics:read' scope." },
              id,
            });
            return;
          }

          // Execute tool with verified, scoped identity
          const userSub = req.tokenClaims.sub;
          console.log(`Executing query on behalf of verified agent identity: ${userSub}`);
          
          res.json({
            jsonrpc: "2.0",
            result: {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ status: "success", rows_returned: 42, latency_ms: 12 }),
                },
              ],
            },
            id,
          });
        } else {
          res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: "Tool not found." }, id });
        }
        break;

      default:
        res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: "Method not found." }, id });
    }
  }
);

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Hardened OAuth 2.1 MCP Server listening on port ${PORT}`);
});

10. 結論と推奨戦略 (E-E-A-T)

自律型AIモデルを企業インフラに接続する際は、AIエージェントを半信頼の委譲アクター(Semi-Trusted Delegated Actor)として扱う必要があります。AIエージェントを完全に信頼された内部マイクロサービス(ルート権限キーを直接付与)として扱うことも、完全に信頼できない外部アクター(全工程で人間が手動承認)として扱うことも、アーキテクチャ上の失敗です。

OAuth 2.1は、開発者の自律性とエンタープライズのゼロトラスト準拠を両立させ、この溝を埋めるために不可欠な暗号フレームワークを提供します。

AIセキュリティアーキテクチャ 5大必須チェックリスト

  1. 静的な環境変数シークレットの全廃: MCP設定、.envファイル、コンテナデプロイを監査し、静的PATを短寿命のOAuth 2.1アクセストークンへ完全移行する。
  2. S256方式PKCEの全面適用: すべてのCLIツール(Claude Code、Cursor拡張など)がSHA-256変換を用いたRFC 7636を実装していることを確認し、安全でないplain方式を拒絶する。
  3. ヘッドレス環境でのRFC 8628またはPrivate Key JWTの導入: Dockerやクラウドホストのエージェントにおいてターミナルでのパスワード手動入力を廃止し、Device GrantまたはRFC 7523非対称鍵認証を採用する。
  4. 非同期ミューテックスを用いたリフレッシュトークンローテーションの導入: エージェントクライアントに排他ロックを実装し、並行更新による競合エラーと不意のトークン失効を排除する。
  5. DPoP (RFC 9449) による送信者拘束トークンの推進: コード実行やDB書き込みなどの高リスク操作においてDPoPヘッダーを義務付け、プロンプトインジェクションによるトークン窃取攻撃をトランスポート境界で無力化する。
← 記事一覧へ
0 / 4