はじめに
プリザンター 1.5.2.0 から、AI クライアントがレコードやビューを操作できる MCP サーバー(/mcp)が使えるようになりました。Claude Desktop や VS Code のように、API キーをヘッダーに設定できるクライアントなら、そのままつなげます。
ところが、Claude の組織コネクタや ChatGPT のカスタム MCP のように、OAuth(Authorization Code + PKCE)で接続する AI アプリには、プリザンターの MCP をそのまま登録できません。API キーをユーザーごとに配って設定してもらう運用も現実的ではありません。
そこで、プリザンターの前に置く認証ラッパー VehicleVision.PleasanterTools.McpOAuthWrapper を作りました。プリザンター本体には手を入れず、OAuth の認可サーバーと MCP の中継だけを受け持ちます。AGPL v3 で GitHub に公開しています。
v0.2.0 では、ログイン画面を API キーだけの入力に変更しました。初期版 v0.1.0 にあった固定ログイン ID 欄はなくなり、ApiKeyLoginId 設定も使いません。
何をするものか
AI アプリには「OAuth の認可サーバー」として見え、認可が済んだ通信だけをプリザンターの /mcp へ中継します。
ユーザーから見ると、AI アプリでコネクタの「接続」を押し、ラッパーのログイン画面に自分の API キーを入れて、接続を許可するだけです。
設計のポイント
認証の根拠は API キー
プリザンターのログインパスワードは使いません。プリザンターで発行済みの API キーを持っていることを認証の根拠にします。LDAP・パスキー・二段階認証を使っている環境でも、プリザンター側の設定を変えずに使えます(その代わり、ラッパーがそれらのログインを代行するわけではありません)。
API キーは保存しない
API キーはラッパーのデータベース・ファイル・トークンのどこにも保存しません。MCP 通信のたびに、プリザンターの Users テーブルから読み取り、その通信の間だけメモリに持ちます。認可とトークンには、キーの再発行を検出するための SHA-256 フィンガープリントだけを入れています。
このため、プリザンターでキーを再発行・削除すれば、古いトークンは次の通信で拒否されます。 アカウントの無効化やロックアウトも同様です。
プリザンターの DB は読み取り専用で参照します。読むのは ApiKey・Disabled・Lockout など 9 列だけで、書き込みはしません。
個人のアカウントと共通アカウント
同意画面で、自分のアカウントの権限で操作するか、管理者が用意した共通アカウントの権限で操作するかを選べます。個人のキーでログインした人が共通アカウントを使う場合も、OAuth の主体はログインした本人のままです。
MCP の中身は解釈しない
GET・POST・DELETE と SSE をそのまま中継するだけで、ツール定義には依存しません。プリザンターのバージョンが上がって MCP のツールが増えても、ラッパー側の変更は要りません。
実装の中身
構成は OpenIddict(Authorization Code + PKCE S256 の認可サーバー)と YARP(HTTP 中継) です。MCP のメッセージは解釈しないので、「OAuth で認証して、通ったものを転送する」ことだけに絞っています。
ログインで何を確認しているか
ログイン画面の API キー欄に入力されたキーを、次の流れで検証します。
- 入力されたキーで、固定した
TenantIdの Users を検索する(SQL はパラメーター化) - キーの照合が大文字小文字を区別しない DB でも誤認しないよう、取得した値を固定時間比較でもう一度確認する
- キーが重複していたら拒否する
-
Disabled・Lockout・利用期限(LoginExpirationLimitなど)を確認する
プリザンターの Password や TOTP の登録状態は見ません。ここで見ているのは「そのキーを持っているか」だけです。ログイン試行は接続元 IP ごとに 15 分で 5 回までで、成功した 1 回分だけを戻します(失敗の履歴は消しません)。
トークンとキーの再発行
OAuth のトークンには、ユーザー ID・キー所有者 ID・OAuth クライアントのほかに、認証に使ったキーと接続に使うキーの SHA-256 フィンガープリントが入ります。キー自体は入りません。
- MCP 通信のたびに、ラッパーは Users から最新のキーを取り直し、保存しておいたフィンガープリントと照合します
- キーを再発行・削除すると、フィンガープリントが合わなくなり、次の利用時に同意チケット・認可コード・アクセストークン・リフレッシュトークンがすべて拒否されます
- 有効期間は、アクセストークン 15 分、認可コード 2 分、リフレッシュトークン 1 日
アクセストークンの有効性は、トークンのデータベースだけでなく Users の最新状態でも判定します。ただし、開始済みの長い通信(SSE)の途中で DB を再照会することはしないので、変更が反映されるのは次の HTTP リクエストからです。
中継のときにやっていること
- 受け取った
Authorization・X-API-Key・Cookie・転送元のヘッダーはすべて取り除き、Users から読んだキーをX-API-Keyにして上流のプリザンターへ渡します - MCP のセッション ID は、ユーザー・キー所有者・OAuth クライアントに結び付けて暗号化します。別の主体のセッション ID を使い回しても拒否されます
- MCP のリクエストは Origin を検証します(既定で同一 Origin と Origin なしを許可)
- 認可・同意の POST は CSRF トークンで守り、同意チケットには認可リクエストを結び付けて、有効期限と一度きりの使用を検証します
- リダイレクト先 URI は完全一致、PKCE、
resource/audience、スコープ、クライアントとの対応を検証します
動的クライアント登録(DCR)の扱い
Claude や ChatGPT は DCR で自分を登録します。登録エンドポイントは認証なしで受け付ける必要があるので、そのままだと誰でもクライアントを作れてしまいます。そこで次の制限を付けています。
- 既定は無効。有効にしても、管理者が許可したリダイレクト URI の一覧にあるものだけ受け付ける
- 同じ名前とリダイレクト URI の再登録は、同じ
client_idを返す(増殖しない) - 登録できる総数は
MaxDynamicClients(既定 100)まで。超えると 503
大規模・複数台のときは KVS を使う
既定の OAuth 状態の保存先は SQLite で、単一インスタンス専用です。ログイン試行の制限もプロセスの中だけでロックしているので、台数を増やすと制限が台ごとに別になってしまいます。ユーザーが多い環境や、冗長化したい環境では、保存先を Redis/Valkey 互換の KVS に切り替えられます。
{
"StateStore": "Redis",
"KvsKeyPrefix": "pleasanter-mcp-oauth"
}
接続文字列は環境変数 MCP_GENERAL_KvsConnectionString で渡します。KVS には、OAuth クライアント、認可・トークンの状態、同意の識別子、ログイン試行回数、Data Protection のキーを置きます。API キーは置きません。
実装で気を付けたのは、複数台から同時に触られても壊れないことです。
- 認可コードと同意の二重使用は、KVS の中でアトミックに拒否する
- 状態の更新は、バージョン比較付きの Lua スクリプトで行い、同時更新で上書きされない
- 一度失効したトークンの復活も、アトミックに拒否する
- ログイン試行は 15 分、同意は 5 分の TTL。失効・期限切れの状態は 5 分ごとに整理する
- クライアント ID とトークンの参照 ID は索引で引き、Lua の更新対象のキーは同じ Redis Cluster のスロットに置く
複数台にするときは、Issuer・テナント・クライアントなどの設定、KvsKeyPrefix、署名・暗号化用の証明書を全インスタンスで揃えます。KVS はキーが追い出されない noeviction にし、必要な可用性と永続化を用意してください。KVS の状態を失うと全員が再接続になります。障害時に SQLite へ切り替える機能や、SQLite と KVS の間で状態を移す機能はありません。
Redis 8.2 と Valkey 9.1.2 では、認可・更新・失効・MCP 中継、再起動後の利用、別インスタンスとの競合、API キーが保存されないことを確認しています。
使い方
導入は、配布 ZIP を展開して設定ファイルを書くだけです。
- ASP.NET Core Runtime 10 を入れ、GitHub の Release から
McpOAuthWrapper.zipを展開する - Users テーブルだけを読める読み取り専用の DB ユーザーを作り、
Rds.jsonに書く -
General.jsonに公開 URL(Issuer)、プリザンターの URL、証明書などを書き、Enabledをtrueにする - Claude や ChatGPT に、
https://<ラッパーのホスト>/mcpをコネクタとして登録する
設定項目の意味、IIS・Azure App Service への配置、AI アプリごとの登録手順は、技術マニュアルサイトにまとめました。
v0.1.0 から更新する場合
ラッパーを停止して新しい配布 ZIP に入れ替えます。既存の General.json・Rds.json、署名・暗号化用の証明書、OAuth の状態保存先は保持してください。General.json の ApiKeyLoginId と環境変数 MCP_GENERAL_ApiKeyLoginId は削除します。API キーだけの入力になっても、共通アカウントの選択や接続への同意、API キーを保存しない仕組みは変わりません。
v0.2.0 の公開時には、合成 Users を使う通常テスト 64 件と、Redis・Valkey モードのテスト各 69 件が CI で成功しています。プリザンター実機・AI アプリ経由の再検証は今回の変更後には実施していないので、更新後は利用する環境で接続を確認してください。
おわりに
プリザンターの MCP を、社内の AI アプリから個人の権限で安全に使えるようにするための土台として作りました。Claude の組織コネクタと ChatGPT での接続は実アカウントで確認できました。Azure App Service と IIS での実機確認も済んでいます。
同じ VehicleVision.PleasanterTools のシリーズとして、プリザンターをバックエンドにした Web アンケートアプリも公開しています。取扱説明書はあわせて技術マニュアルサイトにあります。