コンテンツにスキップ

認可

この話が当てはまるのは HTTP 版だけである。stdio 版はトークンを1本持ち、どのツールも それを使う(2つのサーバを参照)。

このページは認証が終わったところから始まる。呼び出し側の身元は 既に決まっていて、問うのは「それで何をしてよいか」である。

全体の形

sequenceDiagram
    participant C as MCP クライアント
    participant M as invenio-mcp
    participant K as Keycloak
    participant I as InvenioRDM
    C->>M: tools/call create_record(トークン無し)
    M-->>C: 401 + WWW-Authenticate(resource_metadata・scope)
    C->>M: GET /.well-known/oauth-protected-resource/mcp
    M-->>C: authorization_servers・scopes_supported
    C->>K: OAuth 2.1 + PKCE、resource=<canonical URI>
    K-->>C: アクセストークン(aud = 本サーバ)
    C->>M: tools/call create_record(Bearer)
    M->>K: RFC 8693 で交換 → aud=invenio-api
    M->>I: 本人として REST 呼び出し
    I-->>M: 結果
    M-->>C: 結果

3つの scope

scope 対象 なぜ分けるか
(不要) 検索・取得・エクスポート・バージョン一覧・ファイル一覧・ダウンロード・語彙・コミュニティの参照 リポジトリは誰にでも公開するものであり、REST API 自体がそうなっている
mcp:read whoamimy_recordslist_revisionslist_requestsget_request 誰であるかによって答えが変わるもの
mcp:write 作成・更新・公開・破棄・新バージョン・ファイル操作・コミュニティへの投稿・コメント 変更するもの
mcp:curate delete_recordrestore_recordrequest_actioncreate_community InvenioRDM 側で admin 権限が要る。下書きの破棄とは重さが違う

mcp:curate は権限の名ではなく、渡す力の名で付けてある。 mcp:admin では言い過ぎで、 実際に渡すのは公開レコードの取り下げと復元、査読の受理、コミュニティの作成であって、 管理者一般ではない。delete では言い足りない。復元は delete ではないし、delete_draft は write 側にある。

取り下げと復元は分けていない。InvenioRDM 側でどちらも同じ admin 権限が要るので、 scope だけ割っても実際の権限分離にはならず、取り下げたものを戻せないクライアントが 生まれるだけになる。

読取を未認証で通すのは意図的である

未認証のクライアントでも公開レコードを検索・取得できる。これは抜けではない。

  • リポジトリが既にそうしている。GET /api/records にトークンは要らない。
  • トークンがあれば同じツールがその人として動くので、下書きも見える。
  • 何も設定していない段階からリポジトリについて答えられる。人がモデルに最初に頼むのは、 たいていそれである。

認可の要るツールをトークン無しで呼ぶと 401 が返り、WWW-Authenticateresource_metadata の URL と scope が載る。これが発見フローの入口であって、 回避すべきエラーではない。

step-up は 401 ではなく 403

認証は通るが scope が足りないときは、MCP の認可仕様どおり 403 insufficient_scope に 必要な scope を載せて返す。理解できるクライアントは、最初からやり直さずに広い scope へ 認可し直せる。

既製のクライアントはたいていこれを扱えない

MCP SDK は 401 でしか再認可せず、403 を扱わない。/mcp-auth が 最小集合ではなく mcp:read mcp:write mcp:curate を広告しているのはそのためで、 そちらに来たクライアントは、扱えない 403 を踏む代わりに、必要なものを一度に得る。

同じ資源への入口が2つある

/mcp は未認証のクライアントの接続を通す。/mcp-authinitialize を含め、 最初の要求から 401 を返す。

後者が在るのは、実在するクライアントのふるまいのためである。最初の接続が失敗しないと 認可の準備をしない作りのものがある。mcp-remote 0.1.37 はコールバックの待ち受けを UnauthorizedError の経路でしか作らないので、/mcp に対しては接続が成功してしまい、 あとからツール単位で 401 が返っても認可コードの受け取り先が無い。

/mcp-auth は同じ資源の別の扉ではなく、独立した保護リソースである。RFC 9728 は メタデータの resource が接続先 URI と一致することを求め、クライアントはそこを検証する ——mcp-remote"Protected resource … does not match expected …" と言って接続を 拒む。したがって専用の canonical URI と専用のメタデータを持たせ、Keycloak の realm 設定は トークンに両方の audience を載せている。

トークンは交換する。転送しない

keycloak モードで提示されるトークンは本サーバ宛である。それをそのまま InvenioRDM へ 送るのは仕様が禁じる confused deputy にあたるので、RFC 8693 で aud=invenio-api の 別トークンに交換して使う。交換後のトークンも本人の身元を持つので、InvenioRDM 自身の 権限判定はそのまま効く。

受け取り側では aud が本サーバの canonical URI であることを検証するので、他のサービス 向けに発行されたトークンを持ち込むことはできない。

PAT モードに交換は無い。受け取るトークンがそもそも InvenioRDM のトークンなので、 交換する先が無いからである。引き換えは明示してある——audience による分離が無い。 それが認可サーバを要らなくすることの代価であり、keycloak モードが在る理由でもある。

判定が実際に起きる場所

scope が決めるのはクライアントが要求してよいかである。実際に行えるかを決めるのは InvenioRDM である。mcp:curate を持つ利用者でも、アカウントに admin ロールが無ければ InvenioRDM から 403 が返る。それが正しい。権限の模型を2か所に持つことが、両者をずらす。

確かめ方

http/conformance/mcp_client.py が一連の流れを headless で走らせ、未認証呼び出しの 401、 resource_metadata からの発見、resource 付き PKCE(S256)(RFC 8707)、iss の検証 (RFC 9207)、403 からの step-up、別 audience 宛トークンの拒否を PASS/FAIL で報告する。

MCP_RESOURCE=https://<mcp>/mcp MCP_TEST_USER=... MCP_TEST_PASSWORD=... \
  python3 http/conformance/mcp_client.py