Skip to content

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.phppassport ガード 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)

http
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-serverGET不要OAuth メタデータ
/.well-known/oauth-protected-resourceGET不要リソースサーバーメタデータ
/oauth/registerPOST不要動的クライアント登録
/oauth/authorizeGETMappy セッション認可エンドポイント
/oauth/authorize/consentPOSTMappy セッション同意処理
/oauth/tokenPOSTclient_id (+ secret)トークン発行・更新
/oauth/revokePOSTclient_id (+ secret)トークン失効
/oauth/introspectPOSTclient_id + secretトークン検証(内部用)

4.1 OAuth メタデータ例

json
{
  "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:readSMS ログ取得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:readmappy_users.is_enabled = 1
mappy:location:write上記 + gbp_connection_settings_access_level >= 2
mappy:posts:readis_enabled = 1 + 該当ロケーションが mappy_user_available_gbp_locations にある
mappy:posts:write上記 + 該当 GBP の mappy_business_accounts.permission_level が書込可
mappy:reviews:writeis_enabled = 1 + 該当ロケーション利用可
mappy:ranking:readsearch_ranking_enabled = 1
mappy:insight:readis_enabled = 1
mappy:keyword:readsearch_ranking_enabled = 1
mappy:report:readis_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単数形(複数形ではない)
  • adminsmappy_ プレフィックス無し

DB 確認結果より確定済み。

6.4 group_id を指定された場合

tool 入力で group_id が指定された場合は、追加で以下のチェック:

sql
SELECT 1
FROM mappy_groups
WHERE id = :group_id
  AND user_id = :resolved_user_id
LIMIT 1;

該当無しなら 403。

6.5 判定関数の擬似コード

python
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_id Cookie 経由で特定ユーザーになりすまし操作する機構あり
  • セッション認証 + Cookie で動作

7.2 MCP での扱い(Phase 別)

Phase扱い
Phase 1(MVP)Admin 自身としてのアクセスのみ。代理操作は許可しない(実装複雑化を回避)
Phase 2is_supervisor=1 の Admin に限り、user_id を tool 入力で明示することで代理操作可能
Phase 3一般 Admin も担当ユーザーへの代理を許可(OAuth 認可時に対象ユーザーを選択)

7.3 Phase 2 以降のフロー(暫定)

監査の重要性

代理操作は監査上のリスクが高い。必ず Audit Log に「誰が誰の代理で何をしたか」を全件記録する。 詳細は DB 設計(監査ログ) を参照。

8. トークンライフサイクル

トークン有効期限rotate保存先
authorization_code60 秒1 回使用で消費Redis(メモリのみ)
access_token60 分リフレッシュで新発行クライアント側のみ(サーバーは jti のみ保持)
refresh_token30 日(rolling)リフレッシュで rotate(旧失効)DB
id_token(将来)60 分クライアント側

8.1 JWT vs Opaque

  • access_token: JWT(HS256 / RS256)— 検証コストを下げる
  • refresh_token: Opaque(DB 検索が必須、ローテーション安全)

8.2 JWT クレーム例

json
{
  "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 必須対応

項目対応
PKCEcode_challenge_method=S256 必須、plain は禁止
stateCSRF 対策、必須
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 Desktophttps://claude.ai/api/mcp/auth_callback
Claude Codehttp://localhost:<port>/callback(動的)
ChatGPT Connectorshttps://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 標準)

codeHTTP
invalid_request400必須パラメータ欠落
invalid_client401client_id 不正
invalid_grant400code 期限切れ・code_verifier 不一致
unauthorized_client400クライアントが該当 grant_type 使用不可
unsupported_grant_type400サポート外の grant_type
invalid_scope400未知の scope
access_denied403ユーザーが同意拒否
server_error500サーバー内部エラー

10.2 MCP リクエスト時のエラー(JSON-RPC)

code意味
-32000scope 不足
-32001テナンシエラー(対象範囲外)
-32002機能フラグ無効(対象ユーザーが該当機能 OFF)
-32003レート制限
-32602入力 schema 不正
-32603内部エラー

11. レビュー時の論点(未確定)

#論点候補
1access_token 有効期限60 分(標準)/ 30 分(厳しめ)
2refresh_token rolling 期間30 日 / 14 日
3JWT 署名アルゴリズムHS256(共有鍵)/ RS256(公開鍵)
4Admin 代理操作の Phase 1 対応有無提案:Phase 1 では非対応
5scope 同意画面の粒度scope 単位 / グループ単位
6連携解除画面の置き場所Mappy 管理画面に追加
7既存 Web セッションとの連携OAuth 認可エンドポイントで Web セッションを流用する設計の是非
8client_secret の保存方式DB に bcrypt ハッシュ / KMS 暗号化

12. 関連ドキュメント