前回からの続き
C言語で作成したMCPサーバーに認証処理を追加してみたいと思います。
認証処理はどのように動作する?
MCPの仕様と、みなさんがネット上に公開してくれていただいている情報を照らし合わせながら、今回は以下のような方法で認証処理を実現しました。
基本的にはMCPの仕様に従って実装しているつもりです。
認可サーバー
認可サーバーには Auth0 を使用しました。
当初、KeyCloakでも試していたのですが、DCRが上手く動作しない。
かといって、MCP Inspectorは、クライアントIDは指定できても、クライアントシークレットが指定できず、事前にクライアント登録して動作させることもできず。ギブアップです。
KeyCloakでいろいろ試しているときに、MCP Inspectorのソースを眺めていて思ったのですが、公式サイトで配布しているテストプログラムにしては、最新の仕様に追いついていないように思えます。
例えば、"WWW-Authenticate: Bearer resource_metadata=..." や "/.well-known/oauth-protected-resource" でメタデータ取得先を伝えたとしても、素直にそこを見に行かず、とにかくいろんなURLでメタデータの取得をリトライする。
古い仕様のMCPサーバーでも接続できるようにしている工夫かもしれませんが、この仕組みのおかげで公開されている、MCP認証のシーケンスとことなり、だいぶ混乱しました。
で、なんだかんだ試した結果、Auth0でやっと動作できたという感じです。
ただ、Auth0のDCRもすんなりとは動作せず、コツが必要です。
Auth0の設定に関しては、以下の記事が参考になりました。
特に以下の2つのポイントが重要だと思います。
- データベース認証をドメインレベルに設定しておく
- デフォルトオーディエンスに作成したAPIを設定しておく
途中ChatGTPとも相談しながら調査すすめてましたが、MCP Inspectorソースがあるんだから、自分でその中身調べたら?と、手厳しいご指摘いただきました。
まぁ、ごもっともです笑。
おかげで、それなりに理解できたので、MCP Inspector自体を改造してしまえば、KeyCloakでも動作できそうな気はします。
ただ、それはそれで、MCPの仕様に準拠してるの?とか、現在の仕様では、やはりDCR使うのが推奨かなと思います。
認証の仕組み
に仕様が書かれていますが、このシーケンスとAuth0の設定を組み合わせて、どういう風に認証が行われているのか確認しました。
APIの作成
Auth0にAPIを作成。
そのAPIにパーミッションを定義します。
今回は、以下のように読み替えることができるかと思います。
API = MCPサーバー
パーミッション = MCPサーバー上のどのツールに認証をかけるか。
ユーザーの作成
Auto0 にユーザーを作成しパスワードを設定。
作成したユーザーに先ほど作成したAPIのパーミッションを割り当てます。
これでユーザーでログインすることによって、MCPサーバーが使用できるという定義になります。
MCP Inspectorでのログイン処理
以下のようなシーケンスで実際に認証が行われ、MCPサーバーを使用できるユーザーを制限します。
- MCP Inspector が MCPサーバーにinitializeを実行
→ 401エラーで認証が要求される。 - MCP Inspector が 認可サーバー(Auth0)に認証を依頼
→ Auth0のログイン画面が表示される。作成したユーザーでログイン。アクセストークンが取得できる。 - MCP Inspectorは、アクセストークンと共に、再度MCPサーバーとの通信を行う
→ 動作するようになる!
アクセストークンとは何?
アクセストークンはJWTという形式で作成されており、公開鍵が含まれています。
よって、MCPサーバーは、Auth0に対して通信することなく、自分自身で、アクセストークンの内容を確認することができます。
今回は、jwt-cppというライブラリを利用してアクセストークンのデコードを行いました。
また、Auth0は、アクセストークンを作り出す際に、自身が管理している秘密鍵で署名を行います。
なので秘密鍵を持たない他の人が、アクセストークンを勝手に作り出すことができません。
よって、このアクセストークンにMCPサーバーの情報が含まれているか確認することで、認証が実現できるということになります...
と言いながらも、そもそもこれらの情報を知りうる悪意あるクライアントを用意し、勝手に署名したアクセストークンを作り出し、それをMCPサーバーに送り付けてきたらとか考えると、今はテストとしてMCPサーバー自体はhttpで通信させてますが、これをhttpsにするのは当たりまえとしても、現時点のDCRの仕組み自体には、抜け穴があるように思えます。
Auth0の設定をより深く理解し、このあたりの対策を加えるべきなのかと思います。
MCPサーバーへの認証処理の実装
認証機能を追加したことによって、前回のプログラムは以下のようになりました。
#include "McpServer.h"
int main()
{
// MCPサーバー
McpServer server("MCP Test");
+ // 認可サーバーを定義
+ server.SetAuthorization(
+ "\"https://*auth0のテナント名*.us.auth0.com\"",
+ "\"*APIのパーミッション*\""
+ );
// TOOLを定義
std::vector<McpServer::McpProperty> properties = {
{"location", McpServer::PROPERTY_STRING, true}
};
server.AddTool(
"get_channels", // 名称
"Returns a list of available TV channels.", // 説明
properties, // 入力パラメータ
[](const std::map<std::string, std::string>& args) -> // 実処理
std::vector<McpServer::McpContent>{
std::vector<McpServer::McpContent> contents;
contents.push_back({
.type = "text",
.text = "NHK G"
});
contents.push_back({
.type = "text",
.text = "ETV"
});
return contents;
}
);
// MCPサーバーを起動する
server.Run(
+ "http://127.0.0.1:8000/mcp", // URL(Auth0では、このURLをIDとしてAPIを作成)
10 * 60 * 1000 // セッションのタイムアウト時間
);
return 0;
}
これで、MCP Inspectorで http://127.0.0.1:8000/mcp を指定して、Connectを実行すると、Auth0のログイン画面が表示され、ログイン後に、MCPサーバーに対してツール実行が行えるようになります。
おわりに
MCP Inspectorの実装状況からも、このあたりの仕様はまだまだ今後も変化するようなことがありそうに思えますが、認証の仕組みがあることで、MCPサーバーを遠隔から利用することができるようになるかと思います。
まだまだ、内部の実装は見直すべきポイントがある状況ですが、とりあえず目標としていた認証処理を実現することができました。
(ちなみに、今回はアクセストークンの検証として、オーディエンスのみチェックしていて、有効期限やスコープのチェックは行っていません。)
個人的には、OAuthは昔CakePHPで作成したシステムにて、SSOを実現するときに使ったことがあります。
たぶん、10年位前なので、OAuth1.0だったかもしれません。
大分仕組みは変わっているように思えますが、当時の記憶があれば、なんとなく実現できるものですね。