Nullstead

MCP

MCPサーバー認証を「動く仕様」にする:OAuth 2.1時代の実装ランブック(Cloudflare Workers実践)

MCPサーバーをOAuth 2.1前提で安全運用するための実装ガイドです。RFC 9728/9700の要点、PKCE、スコープ設計、Cloudflare Workersでの実装パターンを個人開発向けに整理します。

約10分

MCPサーバー認証を「動く仕様」にする:OAuth 2.1時代の実装ランブック(Cloudflare Workers実践)

AIエージェントを作っていると、だいたい同じ壁にぶつかります。
「動く。便利。……でも、このMCPサーバー誰でも叩けない?」という、心臓にやや悪い壁です。

最近のMCP(Model Context Protocol)仕様では、認証・認可まわりがかなり整理されてきました。特に、MCPサーバーを OAuth 2.1 のリソースサーバーとして扱う前提が明確になり、WWW-Authenticate ヘッダーや /.well-known のメタデータ公開を前提にした発見フローが実務レベルに落ちてきています。

本記事では、私が個人開発で使っている「最小だけど事故りにくい」設計を、Cloudflare Workersを例にまとめます。
トーンは軽めですが、中身はちゃんと堅めです。冗談は入れますが、トークンは漏らしません。


なぜ今、MCP認証を見直すのか

理由はシンプルで、MCPサーバーが「ローカルのお試し」から「外部接続される運用対象」へ一気に進んだからです。

MCPの認可仕様(draft)では、以下が実質の前提になっています。

  • MCPサーバーは OAuth 2.1 の保護リソースとして振る舞う
  • 保護リソースメタデータ(RFC 9728)を提供する
  • クライアントは WWW-Authenticate と /.well-known/oauth-protected-resource を使って認可サーバーを発見する

※OAuth 2.1: OAuth 2.0の実運用で推奨される手法を統合・整理したフレームワーク案です。Implicit Grantのような危険運用を避け、PKCE前提を強める方向です。
※PKCE(Proof Key for Code Exchange): 認可コードの横取り対策。公開クライアントでは必須級です。

「とりあえずAPIキー1本で」は立ち上げ時には速いのですが、利用者が増えた瞬間に権限管理と監査ログが崩れます。
エージェントが社内Wikiを読み、Issueを作り、DBを引く世界では、誰の代理で何をしたかが追えない設計は長生きしません。


仕様の要点を3分で押さえる(MCP × OAuth 2.1)

実装で重要なのは、仕様書の全暗記ではなく「どこが必須か」を間違えないことです。

  1. MCPサーバーはリソースサーバー
    • トークン発行主体(認可サーバー)と責務を分離できる
  2. 認可サーバー発見が標準化
    • 401応答時の WWW-Authenticate から resource_metadata を示す
    • もしくは /.well-known/oauth-protected-resource を提供
  3. クライアント側は複数AS対応を前提にする
    • authorization_servers が複数返る可能性がある
    • ASごとにクライアント登録・トークン状態を分離する

ここでハマりやすいのが、「1つのclient_idを全ASで共通利用できると思い込む」問題です。
これはかなり危険です。ASごとに識別子の意味が違うため、資格情報の使い回しは事故の入口になります。

「同じ見た目のエンドポイントだから同じだろう」は、開発者の楽観としては満点ですが、セキュリティとしては赤点です。


OAuth 2.0 BCP(RFC 9700)から逆算する設計原則

RFC 9700(OAuth 2.0 Security Best Current Practice)は、平たく言うと「昔のやり方、そのままだと危ないです」を体系化した文書です。MCP文脈でも効くポイントは次の3つです。

1) 認可コード + PKCEを標準路線にする

公開クライアント(CLI、デスクトップアプリ、ローカルエージェント)では、PKCEなし運用を許す理由はほぼありません。
「開発中だから平文challenge許可で」は短期的には楽ですが、本番に残りがちです。

2) トークンの権限を絞る

  • スコープを雑に * へ寄せない
  • ツール単位で最低権限に分割する
  • トークンTTLを長くしすぎない

※TTL(Time To Live): 有効期限です。長いほど運用は楽ですが、漏えい時の被害半径が広がります。

3) 監査可能性を設計に含める

「成功ログだけある」は監査では不合格です。以下を最低限残します。

  • 誰が(主体)
  • どのクライアントで(client_id / app)
  • どのスコープで(権限)
  • 何を実行したか(tool/action)
  • いつ失敗したか(拒否・期限切れ・不足権限)

監査ログは、問題が起きた後に読むものです。
つまり、平時に「未来の自分が読める日本語」で書いておくのが最適解です。英語でももちろん良いですが、深夜2時に自分が読める言語を選ぶと生存率が上がります。


Cloudflare Workersで作る最小実装(サンプル)

Cloudflareの workers-oauth-provider は、MCPサーバーをOAuthで保護する土台として使いやすいです。以下は概念を掴むための最小例です(実環境での検証・調整は必須)。

// 動作未検証サンプル(設計説明用)
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
import { WorkerEntrypoint } from "cloudflare:workers";

class ApiHandler extends WorkerEntrypoint {
  async fetch(req: Request): Promise<Response> {
    // 実際には user / scope 情報を取り、ツール権限を判定
    return new Response(JSON.stringify({ ok: true }), {
      headers: { "content-type": "application/json" },
    });
  }
}

export default new OAuthProvider({
  apiRoute: "/mcp",
  apiHandler: ApiHandler,
  defaultHandler: {
    async fetch() {
      return new Response("MCP OAuth gateway", { status: 200 });
    },
  },
  authorizeEndpoint: "/authorize",
  tokenEndpoint: "/oauth/token",
  clientRegistrationEndpoint: "/oauth/register",

  // スコープはツール単位に寄せる
  scopesSupported: ["task.read", "task.write", "admin.audit.read"],

  // 互換性より安全性を優先
  allowImplicitFlow: false,
  allowPlainPKCE: false,

  // トークン寿命は短めから開始
  accessTokenTTL: 3600,
  refreshTokenTTL: 1209600,
});

ポイントは、便利なデフォルトをそのまま本番に持ち込まないことです。
特に allowPlainPKCE は段階的にでも false へ寄せる価値があります。


実運用で効く「権限設計」の切り方

MCPサーバーの認可設計は、技術よりモデリングが9割です。私は次の順で決めると失敗が減りました。

手順A: ツールを「読み取り」と「変更」に分離

  • doc.read
  • doc.write
  • repo.issue.create
  • repo.issue.close

この分離だけで、誤操作時の被害がだいぶ小さくなります。

手順B: 危険操作に追加ガード

  • 本番反映、削除、課金操作は別スコープ
  • 可能なら二段階承認(人間確認)を入れる

手順C: スコープ不足を“親切に”返す

401/403時のエラーメッセージが雑だと、クライアント実装者が迷子になります。

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "insufficient_scope",
  "required": ["repo.issue.create"],
  "granted": ["repo.read"]
}

この「必要権限の見える化」は、UX改善だけでなく運用問い合わせの削減にも効きます。
サポート担当の睡眠時間は、仕様準拠の副産物として守られます。


よくある地雷と回避策

ここはかなり実務的な話です。私が実際に何度か踏んだ(そして足跡がまだある)地雷を列挙します。

地雷1: Redirect URIの管理が甘い

  • 回避策: 完全一致で検証する
  • ワイルドカードや曖昧一致を避ける

地雷2: スコープ設計が雑で肥大化

  • 回避策: ツール境界ごとに分割し、利用実績で棚卸しする
  • 使われないスコープは削除候補にする

地雷3: 静的トークンを長期運用

  • 回避策: 短命アクセストークン + リフレッシュトークン + 失効導線
  • 漏えいを前提に「被害最小化」で設計

地雷4: 監査ログが“読めない”

  • 回避策: JSON構造を統一し、actor, client_id, scope, tool, result を固定
  • 検索しやすいキー名に統一

地雷5: 仕様ドラフト依存のハードコード

  • 回避策: 仕様差分を吸収できる設定レイヤーを作る
  • .well-known やヘッダー解析を関数化して差し替え可能にする

ドラフト仕様を使う時は、「未来の変更は来る」という前提で実装しておくのが平和です。
「仕様が変わるなんて聞いてない」は、だいたい自分が昨日の自分に言っています。


導入チェックリスト(個人開発向け)

最後に、今日から使える最低限チェックです。

  • MCPエンドポイントが401時に WWW-Authenticate を返す
  • /.well-known/oauth-protected-resource を公開している
  • 認可コードフロー + PKCE(S256)を採用している
  • allowImplicitFlow を無効化している
  • スコープがツール単位で分離されている
  • 期限切れ・拒否・権限不足のログが残る
  • 認可サーバーごとにclient登録状態を分離している
  • トークンTTLと失効導線を定義している

全部いきなり完璧でなくても大丈夫です。
まずは「発見フロー準拠」と「PKCE前提化」から始めるだけで、事故率はかなり下がります。


まとめ

MCPの認証・認可は、面倒に見えて実は「後から効く保険」です。
エージェントが賢くなるほど、権限事故の破壊力も上がります。だからこそ、OAuth 2.1系の実装原則とMCP仕様の発見フローを、最初に押さえておく価値があります。

私の結論は次の一文です。

「エージェントを速くする前に、代理実行の責任境界を速く定義する」

速度はキャッシュで買えますが、信頼は設計でしか買えません。
そして設計は、だいたい締切の前日に突然は生えてきません。残念ながら。

参考リンク

Tags #MCP #OAuth 2.1 #Cloudflare Workers #セキュリティ #個人開発