Skip to content

MCP サーバー 認証・テナンシ設計(v2)

概要

項目内容
ステータス🟡 設計中
親ドキュメント案件提案書 / アーキテクチャ全体像
関連設計ツール一覧 / インフラ・実装計画

本ドキュメントは、MCP サーバーの 認証(誰か)認可・テナンシ(何を操作できるか) を確定する。

1. 前提条件

1.1 既存システムの状況

  • Mappy(Laravel)には OAuth 2.x 認可サーバーが存在しない(Passport / Sanctum 未稼働、env 確認済み)
  • 各製品の認証は Web セッション(HTTP cookie)ベース
  • users テーブルは 4 製品で同一スキーマ(同じ Laravel コードベース、DB_DATABASE=gmac で統一)

→ MCP の OAuth 2.1 認可サーバーは 完全新規構築。既存 Laravel のセッション認証とは独立に動作する。

1.2 MCP プロトコル仕様の制約

MCP 2025-03-26 仕様で認証に関する必須事項:

項目必須 / 任意
OAuth 2.1必須(OAuth 2.0 は MCP 仕様上も非推奨)
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必須

2. OAuth 2.1 フロー詳細

2.1 認可コード + PKCE フロー(メイン)

2.2 トークン更新フロー(refresh_token rotation)

family_id による盗難検知

  1. 親トークンが既に rotate 済みなのに使われた → トークン漏洩の可能性
  2. 該当 family_id全トークンを失効
  3. ユーザーに通知 + アラート

OAuth 2.1 ベストプラクティスの中核。

2.3 動的クライアント登録(RFC 7591)

ChatGPT Connectors は事前登録不要で動的にクライアント登録するため対応必須。

公開クライアント(Claude Desktop / ChatGPT)は client_secret を保持しない ため token_endpoint_auth_method=none を許可し、PKCE で認証する。

3. エンドポイント仕様

パスメソッド認証用途
/.well-known/oauth-authorization-serverGET不要OAuth メタデータ
/oauth/registerPOST不要動的クライアント登録
/oauth/authorizeGETログイン後認可エンドポイント
/oauth/loginPOST不要ログイン処理(users テーブル照合)
/oauth/authorize/consentPOSTログイン中scope 同意処理
/oauth/tokenPOSTclient_idトークン発行・更新
/oauth/revokePOSTclient_idトークン失効

3.1 OAuth メタデータ例

json
{
  "issuer": "https://mcp.h2t-products.com",
  "authorization_endpoint": "https://mcp.h2t-products.com/oauth/authorize",
  "token_endpoint": "https://mcp.h2t-products.com/oauth/token",
  "revocation_endpoint": "https://mcp.h2t-products.com/oauth/revoke",
  "registration_endpoint": "https://mcp.h2t-products.com/oauth/register",
  "scopes_supported": [
    "mcp:location:read",
    "mcp:review:read",
    "mcp:ranking:read",
    "mcp:insight:read"
  ],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic"]
}

4. scope 設計

4.1 命名規則

mcp:<domain>:<action>
部分
mcp固定プレフィックス
<domain>location / review / ranking / insight
<action>read / write(Phase 2+)

4.2 Phase 1 のスコープ一覧

scope対応 tool必要な Mappy 側条件(実カラム)
mcp:location:readlocation.listmappy_users.is_enabled = 1
mcp:review:readreview.listmappy_users.is_enabled = 1
mcp:ranking:readranking.listmappy_users.is_enabled = 1 AND search_ranking_enabled = 1
mcp:insight:readinsight.summarymappy_users.is_enabled = 1

二重制御の意図

scope = MCP 経由で許可された操作種別Mappy 側カラム = ユーザー固有の機能 ON/OFF。顧客 AI が mcp:ranking:read を持っていても、対象ユーザーの順位閲覧フラグが OFF なら **拒否(403)**する。

4.3 デフォルト scope セット

ユーザーが OAuth 同意画面で scope を細かく選ばなくても済むよう、デフォルトの「読取セット」を用意:

  • mcp:read = mcp:location:read mcp:review:read mcp:ranking:read mcp:insight:read

これにより、初心者ユーザーでも「全部読取可」を 1 クリックで同意できる。Phase 2 で書込が加わったら mcp:write も提供。

5. ユーザー基盤の統合方針

5.1 4 製品の users テーブル

4 製品は同一 Laravel コードベースを使っているため mappy_users テーブルのスキーマは同一(GMAC Aurora で実調査済み)。ただし データは製品別に独立(GMAC の mappy_users と GCOR の mappy_users は別レコード)。

GMAC Aurora (gmac DB)
  ├ mappy_users (1,442 rows)
  │   ├ id, login_id, password, email, ...
  │   ├ user_type (1=MAIN_USER, 2=GROUP_USER, 3=MASTER_USER)
  │   ├ parent_user_id (階層構造)
  │   ├ is_enabled, search_ranking_enabled
  │   ├ search_ranking_access_level
  │   ├ gbp_connection_settings_access_level
  │   ├ anti_tamper_screen_access_level
  │   ├ is_smart_meo, show_kuchikomi_settings_link
  │   └ (GMAC を契約している顧客のみ)
GCOR Aurora (gmac DB)
  ├ mappy_users (同じスキーマ、GCOR 顧客のみ)
...

同じ顧客(同じメールアドレス)が複数製品を使う場合、それぞれの製品の mappy_users に別レコードが存在する可能性がある。

認証時のメインキーは login_id(メールアドレスではない)。email カラムはあるが、ログイン用 ID は別管理。

5.2 ログイン時の product 判定方式

方式 A(採用): クライアント側が product を指定

OAuth 認可リクエスト時にクライアントが product クエリパラメータを付ける:

GET /oauth/authorize
  ?response_type=code
  &client_id=...
  &product=gmac        ← クライアントが指定
  &redirect_uri=...
  &code_challenge=...

→ MCP サーバーはそのプロダクトの Aurora の mappy_users テーブルで認証する。

利点:

  • 顧客 AI 側(Claude Desktop の MCP 設定など)で 1 回設定すれば以後固定
  • 1 顧客 = 1 製品 = 1 OAuth クライアントになり、シンプル
  • 認証 Aurora の振り分けが認可開始時点で決まるので実装単純

方式 B(不採用): メールアドレスで全製品の users を横断検索

ログイン画面でメール入力 → 4 製品全 users テーブルを順次検索 → ヒットした製品で認証。

不採用理由:

  • 同じメールアドレスが複数製品に存在する場合、どちらを選ぶかユーザーに聞く UI が必要
  • DB 接続が 4 倍になり、遅い
  • 認証エラー時のレスポンス時間が長い(4 製品全部見てから「該当なし」を返す)

Phase 1 は方式 A で確定。将来「1 接続で複数製品横断」要件が出たら Phase 3 以降で再検討。

5.3 認証時のフロー

6. テナンシ判定アルゴリズム

6.1 用語

用語意味
認証ユーザーOAuth トークンの主体(JWT の sub クレームの user_id)
対象店舗tool で操作対象とする gbp_locations.id

6.2 店舗境界の判定(Phase 1)

sql
visible_locations(user_id) =
    SELECT gbp_location_id
    FROM mappy_user_available_gbp_locations
    WHERE user_id = :user_id

→ Phase 1 では「ユーザーが直接見られる店舗」のみで判定(mappy_user_available_gbp_locations の単純照合)。mappy_groups / mappy_group_location 経由の集約は Phase 2 以降。

→ tool の入力で location_id が指定された場合、上記の集合に含まれているか検証。含まれていなければ FORBIDDEN_LOCATION で拒否。

6.3 機能フラグチェック

scope だけでなく、対象ユーザーのアプリケーション側フラグも確認する(実カラム名は本番調査で確定):

scope必要なフラグ
mcp:location:readmappy_users.is_enabled = 1
mcp:review:readmappy_users.is_enabled = 1
mcp:ranking:readmappy_users.is_enabled = 1 AND mappy_users.search_ranking_enabled = 1
mcp:insight:readmappy_users.is_enabled = 1

トークンに該当 scope があっても、ユーザーのフラグが OFF なら拒否(403 FORBIDDEN_FEATURE)。

Phase 2 以降での書込系 scope に対応する access_level カラム(将来用、Phase 1 では使わない):

将来 scope必要なフラグ
mcp:location:write(将来)mappy_users.gbp_connection_settings_access_level >= 2
mcp:antitamper:read(将来)mappy_users.anti_tamper_screen_access_level >= 1
mcp:smartmeo:*(将来)mappy_users.is_smart_meo = 1
mcp:kuchikomi:settings(将来)mappy_users.show_kuchikomi_settings_link = 1

6.4 判定関数の擬似コード

python
def authorize_request(token: AccessToken, tool_name: str, input: dict) -> ResolvedContext:
    # 1. tool 必須 scope のチェック
    required_scope = TOOL_SCOPE_MAP[tool_name]
    if required_scope not in token.scopes:
        raise Forbidden(f"missing scope: {required_scope}")

    # 2. ユーザー情報取得 (対象 product の Aurora の mappy_users から)
    # SELECT id, user_type, parent_user_id, is_enabled, search_ranking_enabled,
    #        search_ranking_access_level, gbp_connection_settings_access_level,
    #        anti_tamper_screen_access_level, is_smart_meo
    # FROM mappy_users WHERE id = :user_id
    user = fetch_mappy_user(token.product, token.user_id)
    if not user.is_enabled:
        raise Forbidden("user disabled")

    # 3. 機能フラグチェック
    if required_scope == 'mcp:ranking:read' and not user.search_ranking_enabled:
        raise Forbidden("ranking feature disabled for this user")

    # 4. 店舗境界チェック
    if 'location_id' in input:
        if input['location_id'] not in visible_locations(token.product, token.user_id):
            raise Forbidden("location not accessible")

    return ResolvedContext(
        product=token.product,
        user_id=token.user_id,
        user=user,
    )

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

トークン形式有効期限rotate保存先
authorization_codeOpaque(32 文字ランダム)60 秒1 回使用で消費DynamoDB(TTL)
access_tokenJWT (RS256)60 分リフレッシュで新発行クライアント側のみ(サーバーは jti 失効リスト管理)
refresh_tokenOpaque(64 文字ランダム)30 日(rolling)リフレッシュで rotate(旧失効)DynamoDB
id_token(将来)JWT(Phase 1 では未実装)

7.1 JWT クレーム設計

json
{
  "iss": "https://mcp.h2t-products.com",
  "sub": "1435",
  "aud": "mcp",
  "exp": 1735689600,
  "iat": 1735686000,
  "jti": "01HVABCDEFG...",
  "scope": "mcp:location:read mcp:review:read",
  "product": "gmac",
  "user_type": 1,
  "client_id": "01HV..."
}
クレーム意味
submappy_users.id
aud"mcp" 固定
exp発行から 60 分後
iat発行時刻
jtiJWT 一意 ID(失効リスト用)
scopeスペース区切りの scope 列
product"gmac" / "gcor" / "pipit" / "kuchikomi_one"
user_typemappy_users.user_type(1=MAIN_USER, 2=GROUP_USER, 3=MASTER_USER)
client_idOAuth クライアント ID

7.2 失効戦略

事象動作
ユーザーがログアウト・連携解除該当 sub の access_token jti を失効リストに追加、refresh_token を revoked_at セット
該当製品の users.is_enabled = 0 になった同上、ただし機能フラグチェックで実質的に拒否されるので即時失効は不要
/oauth/revoke 呼び出し該当トークンを失効
refresh_token 再使用検出family_id 配下を一括失効 + ユーザー通知
パスワード変更refresh_token 全失効

7.3 JWT 署名鍵

項目内容
アルゴリズムRS256(推奨)
鍵長2048bit 以上
ローテーション周期90 日
保管AWS Secrets Manager(mcp/jwt-signing-key
kid(key id)JWT ヘッダーに含め、複数鍵を並行運用可能
公開鍵公開/.well-known/jwks.json で配布

8. セキュリティ要件

8.1 必須対応

項目対応
PKCEcode_challenge_method=S256 必須、plain は禁止
stateCSRF 対策、必須
redirect_uri完全一致(部分マッチ禁止)
code 再使用1 回で消費、再使用検知時は関連トークン全失効
refresh_token rotate必須、family_id で再使用検出
HTTPS 必須HTTP 受付不可、HSTS 有効
クッキー(同意画面用)Secure, HttpOnly, SameSite=Lax

8.2 redirect_uri ホワイトリスト

公式 MCP クライアントの redirect_uri を OAuth クライアント登録時に検証:

クライアントredirect_uri パターン
Claude Desktophttps://claude.ai/api/mcp/auth_callback
Claude Codehttp://localhost:*/callback(動的ポート)
ChatGPT Connectorshttps://chat.openai.com/oauth/callback(要検証)

localhost の動的ポートは Claude Code 等で必要なため、明示的に opt-in したクライアントのみ許可。

8.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'

8.4 ログ・監査

  • password / token / code 本体はログ出力禁止
  • ログには jti(JWT ID)のみ記録
  • 認可失敗・トークン拒否は CloudWatch Logs + DynamoDB 監査ログに記録
  • メトリクスとして「認証失敗率」「refresh 再使用検出回数」を CloudWatch で監視

9. DynamoDB スキーマ

OAuth 認可サーバーで必要な永続データを DynamoDB に格納する。VPC 不要、IAM 経由でアクセス。

9.1 テーブル一覧

テーブル名主キーTTL用途
mcp_oauth_clientsclient_id (PK)なしOAuth クライアント情報
mcp_oauth_codescode_hash (PK)expires_at(60s)認可コード
mcp_oauth_refresh_tokenstoken_hash (PK), user_id (GSI)expires_at(30d)リフレッシュトークン
mcp_oauth_consentsuser_id (PK), client_id (SK)なしユーザー同意
mcp_jwt_revocationsjti (PK)expires_at(≦ 60 min)アクセストークン失効リスト
mcp_audit_logsrequest_id (PK), user_id+created_at (GSI)1 年監査ログ

9.2 主要テーブルの属性

mcp_oauth_clients

属性説明
client_idString (PK)UUID v4
client_nameString"Claude Desktop" 等
redirect_urisList許可された callback URL の配列
grant_typesList["authorization_code", "refresh_token"]
scopes_allowedListこのクライアントが要求可能な scope
token_endpoint_auth_methodString"none" / "client_secret_basic"
client_typeString"public" / "confidential"
registration_methodString"manual" / "dynamic"
is_enabledBool有効/無効
created_at / updated_atString (ISO)

mcp_oauth_refresh_tokens

属性説明
token_hashString (PK)SHA-256(token)
user_idNumber (GSI PK)users.id
productString"gmac" 等
client_idString
scopeStringスペース区切り
family_idStringrotate チェーン識別子
parent_token_hashString直前トークンの hash
issued_atString (ISO)
expires_atNumber (Unix epoch)TTL 用
revoked_atString (ISO)nullable
revoke_reasonStringrotation / user_revoke / family_compromise 等

mcp_oauth_codes

属性説明
code_hashString (PK)SHA-256(code)
client_idString
user_idNumber
productString
scopeString
redirect_uriString
code_challengeStringPKCE
code_challenge_methodString"S256"
expires_atNumber (Unix epoch)TTL(60s)
consumed_atString (ISO)消費時刻

9.3 オンデマンドキャパシティで運用

DynamoDB は on-demand 課金 とし、書込/読込量に応じた自動スケール。初期の低トラフィック時にコスト最小化。

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

#論点候補
1access_token 有効期限60 分(標準)/ 30 分(厳しめ)
2refresh_token rolling 期間30 日 / 14 日
3JWT 署名アルゴリズムRS256(推奨)/ HS256
4ログイン画面の UI / 製品ブランディング1 画面共通か、製品ごとにテーマか
5scope 同意画面の粒度個別 scope / グループ単位(mcp:read ひとまとめ)
6既存 Mappy ログイン情報との同期パスワード変更時の MCP 失効連携
7動的クライアント登録の制限全許可 / 1 ユーザーあたり同時 N 個まで
8カスタムドメインの確定mcp.h2t-products.com 等、未決定

11. 関連ドキュメント