MCPの認可仕様 (6/18) に従ったMCPサーバーを作る。認可サーバーにはAuth0を使ってみたい。

6/18 の MCPの認可仕様
参考になりそうな記事1

まずは仕様を読んでみて重要そうなポイントをメモしてみる
標準準拠
この認証メカニズムは、以下にリストされている確立された仕様に基づいていますが、シンプルさを維持しながらセキュリティと相互運用性を確保するために、選択された機能のサブセットを実装しています。
- OAuth 2.1 IETF ドラフト ( draft-ietf-oauth-v2-1-12 )
- OAuth 2.0 認可サーバーメタデータ ( RFC8414 )
- OAuth 2.0 動的クライアント登録プロトコル ( RFC7591 )
- OAuth 2.0 保護リソースメタデータ ( RFC9728 )

保護されたMCP サーバーは、アクセス トークンを使用して保護されたリソース要求を受け入れ、応答できるOAuth 2.1 リソース サーバーとして機能します。
MCPサーバーはリソースサーバーなので、RFC9728の OAuth 2.0 Protected Resource Metadata の仕様を参考にする必要がありそう
MCPクライアントはOAuth 2.1 クライアントとして機能し、リソース所有者に代わって保護されたリソース要求を行います。
MCPクライアントはこのスクラップの主題ではないので後で必要があれば見ていく。
Inspector や cursor, vscodeが対応していると嬉しい。
(おそらくだがvscodeは 6/18 のバージョンの仕様に従っているはず。 GitHub Remote MCPのリリースノート から読み取る限りではあるが。(仕様のリンクの参照先がdraftとなっていて、当時のdraft = 6/18 アップデートの内容のはず)

Protected Resource Metadata(PRM)は以下の重要な役割を持つ。
- 認証サーバーの場所情報提供(authorization_serversフィールド)
- サーバー自身のリソース識別子の明示(resourceフィールド)
- サポートされるスコープの公開(scopes_supportedフィールド)
MCPクライアント側も、PRM文書に基づいて認証サーバーを発見することが必須となった。

MCP servers MUST implement the OAuth 2.0 Protected Resource Metadata (RFC9728) specification to indicate the locations of authorization servers. The Protected Resource Metadata document returned by the MCP server MUST include the authorization_servers field containing at least one authorization server.
MCPサーバーが OAuth 2.0 Protected Resource Metadata を実装するのは必須
URLは GET /.well-known/oauth-protected-resource

MCP servers MUST use the HTTP header WWW-Authenticate when returning a 401 Unauthorized to indicate the location of the resource server metadata URL as described in RFC9728 Section 5.1 “WWW-Authenticate Response”.
フローの最初で 401 のステータスとともに WWW-Authenticate ヘッダ に resource server metadata URL を乗せるのが必須
例
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://resource.example.com/.well-known/oauth-protected-resource"

MCPクライアント、認可サーバー間の DCRは必須ではないが推奨
認可サーバーは通常クライアントを適宜登録する手間が必要だが、DCRに対応することでこれが自動で行われる

図を見る限り、MCPサーバーの認可フローで実装しなければならない箇所は3つ

MCP servers, acting in their role as an OAuth 2.1 resource server, MUST validate access tokens as described in OAuth 2.1 Section 5.2. MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2. If validation fails, servers MUST respond according to OAuth 2.1 Section 5.3 error handling requirements. Invalid or expired tokens MUST receive a HTTP 401 response.
MCP servers MUST NOT accept or transit any other tokens.
アクセストークンのハンドリング(上図の最後の部分)についてさらに細かく見ると4つポイントがある
-
OAuth 2.1 リソースサーバーとして機能する MCP サーバーは、OAuth 2.1 セクション 5.2に記載されているようにアクセストークンを検証する 必要があります。
アクセス トークンを受け取った後、リソース サーバーは、アクセス トークンの有効期限が切れていないこと、要求されたリソースへのアクセスが承認されていること、適切なスコープで発行されていること、保護されたリソースにアクセスするためのリソース サーバーのその他のポリシー要件を満たしていることを確認する必要があります。
アクセストークンは一般的に、参照トークンと自己エンコードトークンの2つのカテゴリに分類されます。参照トークンは、認可サーバーにクエリを実行するか、トークンデータベースでトークンを検索することで検証できます。一方、自己エンコードトークンには、リソースサーバーが抽出できる暗号化および/または署名された文字列に認可情報が含まれています。
アクセストークンの有効性を確認するために認可サーバーに問い合わせる標準化された方法は、トークンイントロスペクション[ RFC7662 ]で定義されています。
トークン文字列内の情報をエンコードする標準化された方法は、アクセストークンのJWTプロファイル[ RFC9068 ]で定義されています。-
(不確実) Auth0は opaque トークン をデフォルトで発行するが、インストロペクションエンドポイントを持っていない -
(不確実)Auth0は JWTアクセストークン(self‑encoded token)を発行しており 自己検証できる - アクセス トークンの作成と検証に関する追加の考慮事項については、セクション 7.1を参照してください。
- セキュリティ等に関する留意事項が書かれていた
- ある程度知っている内容なので割愛
3. 気になったのは https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-7.1.4 dが一旦メモに止める
-
-
また、 RFC 8707 セクション 2に従い、 MCP サーバーはアクセストークンが対象ユーザーとして自身に発行されたことを検証する必要があります
-
検証に失敗した場合、サーバーはOAuth 2.1 セクション 5.3 の エラー処理要件に従って応答する 必要があります。無効または期限切れのトークンは、HTTP 401 レスポンスを受け取る必要があります
- MCP サーバーは他のトークンを受け入れたり転送したりしてはなりません。

To mitigate this, MCP clients MUST implement PKCE according to OAuth 2.1 Section 7.5.2. PKCE helps prevent authorization code interception and injection attacks by requiring clients to create a secret verifier-challenge pair, ensuring that only the original requestor can exchange an authorization code for tokens.
vscodeはGitHubリモートMCPのリリースノート公開時点でまだPKCE対応してないって言っていた記憶
今後対応するって書いてあったので一時的に仕様から準拠できていない状態でリリースしてるのか
準拠すべきではあるけど小さくリリースするという点で参考になる

MCP クライアントは、認可サーバーに登録されたリダイレクト URI を持っている必要があります。
認可サーバーは、リダイレクト攻撃を防ぐために、事前に登録された値に対して正確なリダイレクト URI を検証する必要があります。
MCP クライアントは、認可コード フローで状態パラメータを使用して検証し、元の状態が含まれていない、または元の状態と一致しない結果を破棄する必要があります。
これ気になるなー
Auth0 使うならリダイレクトURI はMCPクライアントが動くlocalhostになる?
Auth0ってローカルホストをリダイレクトURIとして指定できたっけ?
なんかここに関しては理解できていない可能性が高い

攻撃者は、サードパーティAPIへの仲介役として機能するMCPサーバーを悪用し、Confused Deputy脆弱性を悪用する可能性があります。盗んだ認可コードを使用することで、ユーザーの同意なしにアクセストークンを取得できます。
静的クライアント ID を使用する MCP プロキシ サーバーは、サードパーティの認証サーバーに転送する前に、動的に登録されたクライアントごとにユーザーの同意を取得する必要があります(追加の同意が必要になる場合があります)。
これもMUST とされてるけど、一旦実装しなくていい気がするな

オーディエンス検証はしようねという話
(もっと詳しく書いてあるが、上述のトークン検証の部分で回収されそうな留意事項なので、メモは割愛)

https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization のページは読み切った
ここからは他の参考資料だったりを漁っていく

リソースインジケータがあると、悪意のあるMCPサーバーによってアクセストークンが使われるのを防げるっぽい?
ちょっと理解できていない。
悪意のあるMCPサーバーにトークン検証するかしないかが委ねられるなら検証せずに使われるのでは?
(理解できていないので後でもっと調べる)

の実装が 仕様変更に既に追従されているのかを調べていく
未マージ/マージ済みのPRを 眺めてたが、仕様変更に追従してそうでもあり、してなさそうでもある
一個一個みていくときりがないのでやめた

A server that implements the Streamable HTTP transport (protocol version 2025-03-26).
まだ 3/26対応の実装っっぽい
sdkのmiddlewareは既に 6/18版のコードに対応していそう

expressじゃなくてhonoで実装したいので、ミドルウェアは ↑を書き写して書いてもいいかも?
hono使いたいのはただの趣味
一旦expressでも全然よいが

実装のためのタスクを整理していく
- アクセストークンの確認、ない場合の401, WWW-Authenticateの対応 (https://zenn.dev/link/comments/79db411fd5da6e )
- https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/server/auth/middleware/bearerAuth.ts を参考にする
- RFC9728の OAuth 2.0 Protected Resource Metadata のエンドポイント実装
4. routerの参考: https://github.com/modelcontextprotocol/typescript-sdk/blob/0506addf35f422650658c5e665ea184e3115a184/src/server/auth/router.ts#L160-L211
5. レスポンスの型の参考:
- https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/shared/auth.ts#L143
- https://github.com/modelcontextprotocol/typescript-sdk/blob/0506addf35f422650658c5e665ea184e3115a184/src/shared/auth.ts#L3-L23
7. ハンドラーの引数の参考
- https://github.com/modelcontextprotocol/typescript-sdk/blob/0506addf35f422650658c5e665ea184e3115a184/src/examples/server/simpleStreamableHttp.ts#L436
- https://github.com/modelcontextprotocol/typescript-sdk/blob/0506addf35f422650658c5e665ea184e3115a184/src/examples/server/demoInMemoryOAuthProvider.ts#L209-L215
- https://github.com/modelcontextprotocol/typescript-sdk/blob/0506addf35f422650658c5e665ea184e3115a184/src/server/auth/router.ts#L64-L102
- これの中身をAuth0にすればよさそう - アクセストークンの検証
3. https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/server/auth/middleware/bearerAuth.ts

メモ
一旦のTODO
-
Auth0を認可サーバーとしたときの各種設定を調べる
- 実際に実装していくときは Auth0での実装も考えていかなくてはならない
-
テスト用のMCP Clientが最新仕様に追従できているものがあるか調べる
- inspectorはまだ対応指定なさそう?と思ったが触ってみないとわからない

AIに入力してリファインメントするために、実装するときの前提を書き出していく
- 認可サーバーはAuth0を使う
- MCPサーバーのデプロイ先はcloudflareを使う
- タスクの進め方(今の想定)
- シンプルなwebサーバーをcloudflareにデプロイする
- ブラウザでアクセスできるかテスト
- シンプルなMCPサーバーをcloudflareにデプロイする
- MCP Inspectorやcursorで接続してテスト
- MCPサーバーに 認可フローの機構を実装する
- MCP Inspectorやcursorで接続してテスト

まずはシンプルなMCPサーバーの作成
この状態で動いた
pnpm run deploy

認可フローを実装していく

認証フロー実装中だけど、時間切れ
まだauth0との連携は全くかけていない。
inspectorでの接続も当然動かない所まで来た。

現状の知見
- cloudflare workersで動かそうとするとMCPが使えない
- honoに書き換えるために、書き写すといったことをある程度行っている