MCPのOAuth認可を理解する|Protected Resource Metadataと401応答

MCP OAuthの401と403。認証と認可の違い、resource_metadataを示すアイキャッチ MCP・認証認可
カテゴリー
MCP・認証認可
公開日
2026.09.26

はじめに

保護されたHTTP型MCPサーバーは、アクセストークンを検証するリソースサーバーです。ユーザーを認証しトークンを発行する認可サーバーとは役割が異なります。

MCPクライアントが認可サーバーを発見する入口がOAuth 2.0 Protected Resource Metadata(RFC 9728)です。この記事は、HTTPとJavaScriptの基礎を知っていて、MCPの認可設計を初めて学ぶ人向けです。読後には、認可サーバーの発見方法と、トークン不正・権限不足への応答を説明できることを目指します。

掲載するHTTP・JSON・Expressコードは設計を理解するための抜粋です。認可サーバーの構築やログインまで動く完成アプリではありません。example.com配下のURLも説明用で、実際の接続先ではありません。

用語を先に整理すると、issuerはトークンの発行元、audienceは利用先、scopeは許可された操作の範囲です。PKCEは、認可コードを横取りされても、それだけではトークンへ交換できないようにする仕組みです。

全体の流れ

  1. クライアントがMCP endpointへアクセス
  2. 未認証ならサーバーが401 Unauthorized
  3. WWW-Authenticateまたはwell-known URIからResource Metadataを取得
  4. そこに書かれた認可サーバーのmetadataを取得
  5. Authorization Code + PKCEなどでアクセストークンを得る
  6. Authorization: Bearer ...でMCPへ再要求
  7. MCPサーバーがトークンの有効性、発行元、宛先、期限、操作権限を検証

STDIO transportへこのHTTPフローをそのまま適用しません。STDIOでは環境から資格情報を取得する設計が推奨されています。

401応答

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read"
Cache-Control: no-store

resource_metadataは認可画面ではなく、保護対象の説明文書を指します。クライアントはヘッダーを正しくパースし、なければ仕様のwell-known URIを試します。

Protected Resource Metadata

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["files:read", "files:write"],
  "bearer_methods_supported": ["header"]
}

resourceはトークンの宛先となるMCPサーバーの標準URIです。クライアントは認可要求とトークン要求へ同じresourceパラメーターを含めます。パラメーターを付けるだけでは不十分で、MCPサーバー側でもトークンの宛先が自分と一致するか検証して、別サービス用トークンの誤用を防ぎます。

サーバー側の最小な入口

以下は認可フロー全体ではなく、401を返すExpress middlewareの例です。

import express from "express";

const app = express();
const RESOURCE = "https://mcp.example.com/mcp";
const METADATA =
  "https://mcp.example.com/.well-known/oauth-protected-resource";

app.get("/.well-known/oauth-protected-resource", (_req, res) => {
  res.json({
    resource: RESOURCE,
    authorization_servers: ["https://auth.example.com"],
    scopes_supported: ["files:read"],
    bearer_methods_supported: ["header"],
  });
});

app.use("/mcp", async (req, res, next) => {
  const match = /^Bearer (.+)$/i.exec(req.get("authorization") ?? "");
  if (!match) {
    res
      .status(401)
      .set("WWW-Authenticate", `Bearer resource_metadata="${METADATA}", scope="files:read"`)
      .end();
    return;
  }

  let auth;
  try {
    auth = await verifyAccessToken(match[1], {
      audience: RESOURCE,
    });
  } catch {
    res.status(401)
      .set("WWW-Authenticate", `Bearer error="invalid_token", resource_metadata="${METADATA}"`)
      .end();
    return;
  }

  // 検証済みの scope を、疑似関数が文字列配列へ正規化して返す契約
  if (!auth.scopes.includes("files:read")) {
    res.status(403)
      .set("WWW-Authenticate", `Bearer error="insufficient_scope", scope="files:read", resource_metadata="${METADATA}"`)
      .end();
    return;
  }
  req.auth = auth;
  next();
});

verifyAccessTokenは未実装の疑似関数で、Expressの機能ではありません。有効性を検証し、検証済み権限をscopes配列で返す契約を想定しています。無効なトークンは401、有効でもfiles:readがなければ403として処理を分け、どちらの応答にもResource MetadataのURLを示します。実運用では通信障害や内部エラーも無効トークンと区別して扱い、いずれの場合も保護対象へ処理を通しません。MCPハンドラー、待受処理、認可サーバー設定はこの抜粋に含みません。

アクセストークンがJWTの場合は、対応ライブラリで許可するアルゴリズム、公開鍵(JWKS)、issuer、audience、期限を検証します。トークンの中身をdecodeしただけでは検証になりません。JWTではない不透明なトークンの場合は、認可サーバーが提供する検証手段を使います。OAuthだから必ずJWTというわけではありません。

やってはいけないこと

  • アクセストークンをURLのqueryへ入れる
  • 別API向けトークンを受け入れる
  • MCPが受け取ったトークンを下流APIへそのまま渡す
  • ログへBearer tokenを出す
  • public clientへ固定client secretを埋め込む
  • audienceやresourceを確認しない
  • scopeをUI表示だけで判定し、各操作で認可しない

下流APIへアクセスするMCPサーバーは、下流用の別トークンを正規のフローで取得します。トークンのパススルーは権限境界を壊します。

401と403

資格情報がない、無効、期限切れなら401が基本です。トークンは有効だが操作権限が不足する場合は、OAuthのエラー応答とアプリの情報漏えい方針を考慮して403を使います。存在確認そのものが秘密の場合は、詳細を返しすぎません。

動作確認

  • tokenなしで401とResource Metadata URLを返す
  • metadataのresourceが実際のMCP URIと一致する
  • 期限切れ、署名不正、issuer不正、audience不正を拒否する
  • 読取scopeで書込Toolを実行できない
  • tokenがアクセスログ、例外、分析基盤へ残らない
  • 認可サーバー停止時に保護を解除しない
  • HTTPSで提供し、localhost以外の平文HTTPを許可しない

トラブル対処

  • 認可サーバーを発見できない:401のresource_metadataとwell-known URIを確認します。
  • tokenが401:署名だけでなくissuer、audience、期限、対象resourceを個別に確認します。
  • 403が続く:tokenをログへ出さず、要求scopeとTool権限の対応表を確認します。

まとめ

MCP OAuthの出発点はログイン画面ではなく、リソースサーバーと認可サーバーの役割分離です。RFC 9728 metadataで発行元を発見し、resourceで宛先を固定し、MCPサーバーでトークンと各操作の権限を検証します。

参考リンク

タイトルとURLをコピーしました