MCP サーバー 認証・テナンシ設計
| 項目 | 内容 |
|---|---|
| ステータス | 🟡 設計中 |
| 関連案件 | #13 Mappy MCP サーバー新設 |
| 親ドキュメント | MCP サーバー 全体像 |
| 関連 DB 設計 | OAuth クライアント DB |
1. 本ドキュメントの目的
MCP サーバーの**認証(誰か)と認可・テナンシ(何を操作できるか)**を確定する。
- OAuth 2.1 認可サーバーの仕様(フロー・エンドポイント・トークン)
- scope の体系と Mappy 既存権限へのマッピング
- テナンシ判定アルゴリズム(ユーザー階層・店舗境界)
- Admin 代理操作の扱い
- セキュリティ要件
実装スタック(Python / TypeScript)の差分はここでは記述しない(言語非依存)。
2. 前提条件
2.1 既存 Mappy 認証の現状(DB 確認結果)
| 観点 | 結果 |
|---|---|
| Laravel Passport テーブル | 存在しない(oauth_* テーブル 0 件) |
| Laravel Sanctum テーブル | 存在しない(personal_access_tokens 無し) |
| Laravel セッションテーブル | 存在しない(sessions 無し → file セッション) |
config/auth.php | passport ガード 3 つ定義済みだが、テーブル未作成のため実質未稼働 |
| 実運用 | Web セッション認証のみ |
OAuth 2.1 は完全新設
既存の Passport 依存は composer.json には残っているが migrate されていないため流用不可。MCP 用に OAuth 2.1 認可サーバーを新規構築する。
2.2 MCP 仕様の制約
- OAuth 2.1 必須(OAuth 2.0 でも MCP 仕様としては許容されているが、新規導入なら 2.1)
- PKCE 必須(
code_challenge_method=S256) - Implicit Flow 廃止
- Resource Owner Password Credentials Grant 廃止
- Dynamic Client Registration (RFC 7591) 対応(ChatGPT Connectors 互換のため)
- OAuth Metadata (RFC 8414) 対応(
/.well-known/oauth-authorization-server)
3. OAuth 2.1 フロー詳細
3.1 認可コード + PKCE フロー(メイン)
3.2 トークン更新フロー
3.3 動的クライアント登録(RFC 7591)
ChatGPT Connectors はクライアント登録を動的に要求するため対応必須。
公開クライアントとの違い
Claude Desktop / ChatGPT は実質的にパブリッククライアント(client_secret を秘匿できない)。 PKCE で代用するため、token_endpoint_auth_method=none を許可する。
3.4 トークン取り消し(RFC 7009)
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=...&token_type_hint=access_tokenユーザーが Mappy 管理画面から「連携解除」を選んだ場合にも、対応する access_token / refresh_token を失効させる。
4. エンドポイント仕様
| パス | メソッド | 認証 | 用途 |
|---|---|---|---|
/.well-known/oauth-authorization-server | GET | 不要 | OAuth メタデータ |
/.well-known/oauth-protected-resource | GET | 不要 | リソースサーバーメタデータ |
/oauth/register | POST | 不要 | 動的クライアント登録 |
/oauth/authorize | GET | Mappy セッション | 認可エンドポイント |
/oauth/authorize/consent | POST | Mappy セッション | 同意処理 |
/oauth/token | POST | client_id (+ secret) | トークン発行・更新 |
/oauth/revoke | POST | client_id (+ secret) | トークン失効 |
/oauth/introspect | POST | client_id + secret | トークン検証(内部用) |
4.1 OAuth メタデータ例
{
"issuer": "https://mcp.mappy.example.com",
"authorization_endpoint": "https://mcp.mappy.example.com/oauth/authorize",
"token_endpoint": "https://mcp.mappy.example.com/oauth/token",
"revocation_endpoint": "https://mcp.mappy.example.com/oauth/revoke",
"registration_endpoint": "https://mcp.mappy.example.com/oauth/register",
"scopes_supported": [
"mappy:location:read", "mappy:location:write",
"mappy:posts:read", "mappy:posts:write",
"mappy:reviews:read", "mappy:reviews:write",
"mappy:ranking:read", "mappy:insight:read",
"mappy:report:read", "mappy:report:write"
],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"]
}5. スコープ設計
5.1 スコープ命名規則
mappy:<domain>:<action>| 部分 | 例 |
|---|---|
mappy | 固定プレフィックス(将来 admin: 等を分ける場合に備える) |
<domain> | location / posts / reviews / ranking / insight / report / keyword / group / media / sms / questionnaire / sns / notification |
<action> | read / write / delete(必要に応じて) |
5.2 スコープ一覧(Phase 別)
Phase 1 (MVP) — 読取系
| scope | 説明 | 対応 tool 例 |
|---|---|---|
mappy:location:read | ロケーション一覧・詳細の取得 | location.list, location.get |
mappy:reviews:read | 口コミ一覧・詳細の取得 | review.list, review.get |
mappy:ranking:read | 検索順位の取得 | ranking.list |
mappy:insight:read | インサイトの取得 | insight.summary |
mappy:keyword:read | キーワード一覧・流入の取得 | keyword.list |
mappy:group:read | グループ・所属店舗の取得 | group.list |
mappy:posts:read | 投稿一覧・詳細の取得 | post.list |
mappy:media:read | メディア一覧の取得 | media.list |
mappy:report:read | レポート一覧・取得 | report.list, report.get |
mappy:questionnaire:read | アンケート一覧・回答取得 | questionnaire.list |
mappy:sms:read | SMS ログ取得 | sms.list |
Phase 2 — 書込系
| scope | 説明 |
|---|---|
mappy:posts:write | 投稿の作成・予約 |
mappy:reviews:write | 口コミ返信・テンプレート登録 |
mappy:media:write | メディアのアップロード |
mappy:media:delete | メディアの削除 |
mappy:location:write | ロケーション属性の更新 |
Phase 3 — レポート・分析
| scope | 説明 |
|---|---|
mappy:report:write | レポート生成リクエスト |
mappy:keyword:write | キーワード登録(将来) |
mappy:group:write | グループ管理(将来) |
5.3 Mappy 既存権限との対応
Mappy ユーザーには既存のアクセスレベルカラムがあり、scope はこれらに従属する。
| MCP scope | 必要な Mappy 側条件 |
|---|---|
mappy:location:read | mappy_users.is_enabled = 1 |
mappy:location:write | 上記 + gbp_connection_settings_access_level >= 2 |
mappy:posts:read | is_enabled = 1 + 該当ロケーションが mappy_user_available_gbp_locations にある |
mappy:posts:write | 上記 + 該当 GBP の mappy_business_accounts.permission_level が書込可 |
mappy:reviews:write | is_enabled = 1 + 該当ロケーション利用可 |
mappy:ranking:read | search_ranking_enabled = 1 |
mappy:insight:read | is_enabled = 1 |
mappy:keyword:read | search_ranking_enabled = 1 |
mappy:report:read | is_enabled = 1 + (MAIN_USER 向け)レポート権限フラグ |
mappy:report:write | 上記 + 書込権限フラグ |
二重制御の意図
scope = MCP 経由で許可された操作種別、Mappy 側カラム = ユーザー固有の機能 ON/OFF。 顧客 AI が mappy:ranking:read を持っていても、対象ユーザーが search_ranking_enabled = 0 なら拒否する(403 を返す)。
6. テナンシ判定アルゴリズム
6.1 用語
| 用語 | 意味 |
|---|---|
| 認証ユーザー | OAuth で同意し、トークンの主体となった mappy_users.id(または admins.id) |
| 対象ユーザー | tool が操作する対象の mappy_users.id(多くは認証ユーザーと同じ) |
| 対象店舗 | tool が操作する mappy_gbp_locations.id |
6.2 認証ユーザーの種類別の操作範囲
6.3 店舗境界の判定
対象ユーザーが決まったら、操作可能な店舗を以下で絞り込む:
visible_locations(user_id) =
SELECT gbp_location_id
FROM mappy_user_available_gbp_locations
WHERE user_id = :user_id
∪
SELECT gl.gbp_location_id
FROM mappy_groups g
JOIN mappy_group_location gl ON gl.group_id = g.id
WHERE g.user_id = :user_idテーブル名の例外
mappy_group_locationは 単数形(複数形ではない)adminsはmappy_プレフィックス無し
DB 確認結果より確定済み。
6.4 group_id を指定された場合
tool 入力で group_id が指定された場合は、追加で以下のチェック:
SELECT 1
FROM mappy_groups
WHERE id = :group_id
AND user_id = :resolved_user_id
LIMIT 1;該当無しなら 403。
6.5 判定関数の擬似コード
def resolve_scope(token: AccessToken, input: dict) -> ResolvedContext:
# 1. 認証ユーザーを確定
auth = token.subject # {kind: 'user'|'admin', id: ..., is_supervisor: ...}
# 2. 対象ユーザーを確定
if auth.kind == 'admin':
if auth.is_supervisor:
target_user_id = input.get('user_id') # 明示指定必須
if target_user_id is None:
raise InvalidInput("user_id required for supervisor admin")
else:
target_user_id = token.preview_user_id # 担当範囲
else: # mappy user
target_user_id = input.get('user_id', auth.id)
# 階層チェック
if not is_in_hierarchy(auth.id, target_user_id):
raise Forbidden("target_user_id out of hierarchy")
# 3. 機能フラグチェック
user = fetch_user(target_user_id)
if not is_enabled_for_scope(user, token.scopes):
raise Forbidden("scope feature disabled for target user")
# 4. 店舗境界チェック
if input.get('location_id') is not None:
if input['location_id'] not in visible_locations(target_user_id):
raise Forbidden("location not accessible")
return ResolvedContext(
auth=auth,
target_user_id=target_user_id,
accessible_locations=visible_locations(target_user_id),
)7. Admin 代理操作の扱い
7.1 既存挙動
- Mappy には Admin が
preview_user_idCookie 経由で特定ユーザーになりすまし操作する機構あり - セッション認証 + Cookie で動作
7.2 MCP での扱い(Phase 別)
| Phase | 扱い |
|---|---|
| Phase 1(MVP) | Admin 自身としてのアクセスのみ。代理操作は許可しない(実装複雑化を回避) |
| Phase 2 | is_supervisor=1 の Admin に限り、user_id を tool 入力で明示することで代理操作可能 |
| Phase 3 | 一般 Admin も担当ユーザーへの代理を許可(OAuth 認可時に対象ユーザーを選択) |
7.3 Phase 2 以降のフロー(暫定)
監査の重要性
代理操作は監査上のリスクが高い。必ず Audit Log に「誰が誰の代理で何をしたか」を全件記録する。 詳細は DB 設計(監査ログ) を参照。
8. トークンライフサイクル
| トークン | 有効期限 | rotate | 保存先 |
|---|---|---|---|
authorization_code | 60 秒 | 1 回使用で消費 | Redis(メモリのみ) |
access_token | 60 分 | リフレッシュで新発行 | クライアント側のみ(サーバーは jti のみ保持) |
refresh_token | 30 日(rolling) | リフレッシュで rotate(旧失効) | DB |
id_token(将来) | 60 分 | — | クライアント側 |
8.1 JWT vs Opaque
- access_token: JWT(HS256 / RS256)— 検証コストを下げる
- refresh_token: Opaque(DB 検索が必須、ローテーション安全)
8.2 JWT クレーム例
{
"iss": "https://mcp.mappy.example.com",
"sub": "user:1435",
"aud": "mappy-mcp",
"exp": 1717200000,
"iat": 1717196400,
"jti": "01HVABCDEFG...",
"scope": "mappy:location:read mappy:reviews:read",
"mcp": {
"user_kind": "mappy",
"user_type": 1,
"is_supervisor": false,
"preview_user_id": null
}
}8.3 失効戦略
| 事象 | 動作 |
|---|---|
| ユーザーが Mappy を退会 | 該当 sub の全 access_token / refresh_token を失効 |
| ユーザーが「連携解除」 | 該当クライアントの全トークンを失効 |
クライアントが /oauth/revoke 呼び出し | 該当トークンを失効 |
| パスワード変更 | 同上 |
| 不審な動作(レート制限 N 回連続ヒット等) | 自動失効 + アラート |
9. セキュリティ要件
9.1 必須対応
| 項目 | 対応 |
|---|---|
| PKCE | code_challenge_method=S256 必須、plain は禁止 |
| state | CSRF 対策、必須 |
| redirect_uri | 完全一致(部分マッチ禁止) |
| code 再使用 | 1 回で消費、再使用検知時は関連トークン全失効 |
| refresh_token rotate | 必須、検知時は family 全失効 |
| token 漏洩検知 | jti + IP の組み合わせで監視 |
| HTTPS 必須 | HTTP 受付不可、HSTS 有効 |
| クッキーセキュア | Secure, HttpOnly, SameSite=Lax |
9.2 redirect_uri ホワイトリスト
Claude / ChatGPT の公式 redirect_uri をクライアント登録時に検証:
| クライアント | 既知の redirect_uri パターン |
|---|---|
| Claude Desktop | https://claude.ai/api/mcp/auth_callback |
| Claude Code | http://localhost:<port>/callback(動的) |
| ChatGPT Connectors | https://chat.openai.com/oauth/callback(仮) |
localhost の扱い
Claude Code 等は localhost の動的ポートを使うため、登録時に http://localhost:*/callback パターンを許可する(明示的に opt-in したクライアントのみ)。
9.3 セキュリティヘッダー
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Content-Security-Policy: default-src 'self'; ...9.4 ログ・監査
- password / token / code を含むペイロードはログ出力禁止
- ログには
jti(JWT ID)のみ記録、token 本体は記録しない - 認可失敗・トークン拒否は CloudWatch Alarm で監視
10. エラーレスポンス
10.1 OAuth エラー(OAuth 2.1 標準)
| code | HTTP | 例 |
|---|---|---|
invalid_request | 400 | 必須パラメータ欠落 |
invalid_client | 401 | client_id 不正 |
invalid_grant | 400 | code 期限切れ・code_verifier 不一致 |
unauthorized_client | 400 | クライアントが該当 grant_type 使用不可 |
unsupported_grant_type | 400 | サポート外の grant_type |
invalid_scope | 400 | 未知の scope |
access_denied | 403 | ユーザーが同意拒否 |
server_error | 500 | サーバー内部エラー |
10.2 MCP リクエスト時のエラー(JSON-RPC)
| code | 意味 |
|---|---|
| -32000 | scope 不足 |
| -32001 | テナンシエラー(対象範囲外) |
| -32002 | 機能フラグ無効(対象ユーザーが該当機能 OFF) |
| -32003 | レート制限 |
| -32602 | 入力 schema 不正 |
| -32603 | 内部エラー |
11. レビュー時の論点(未確定)
| # | 論点 | 候補 |
|---|---|---|
| 1 | access_token 有効期限 | 60 分(標準)/ 30 分(厳しめ) |
| 2 | refresh_token rolling 期間 | 30 日 / 14 日 |
| 3 | JWT 署名アルゴリズム | HS256(共有鍵)/ RS256(公開鍵) |
| 4 | Admin 代理操作の Phase 1 対応有無 | 提案:Phase 1 では非対応 |
| 5 | scope 同意画面の粒度 | scope 単位 / グループ単位 |
| 6 | 連携解除画面の置き場所 | Mappy 管理画面に追加 |
| 7 | 既存 Web セッションとの連携 | OAuth 認可エンドポイントで Web セッションを流用する設計の是非 |
| 8 | client_secret の保存方式 | DB に bcrypt ハッシュ / KMS 暗号化 |
12. 関連ドキュメント
- MCP サーバー 全体像
- ツールカタログ — scope の具体的 tool への適用
- 書込安全装置 — 書込系の追加チェック
- DB 設計(OAuth クライアント) — DB スキーマ
- DB 設計(監査ログ) — 認可成功/失敗の記録