mappy_mcp_oauth_*(MCP OAuth 2.1 認可サーバー DB)
| 項目 | 内容 |
|---|---|
| ステータス | 🟡 設計中 |
| 関連案件 | #13 Mappy MCP サーバー新設 |
| 親ドキュメント | MCP サーバー 全体像 |
| 関連設計 | 認証・テナンシ / DB 設計(監査ログ) |
ER 図
概要
MCP 用の OAuth 2.1 認可サーバー で必要となる永続データを管理する。
Mappy 既存 OAuth は流用不可
DB 確認結果より、Mappy には Laravel Passport のテーブルが存在しないため流用不可。本テーブル群を新規構築する。
設計方針
| 方針 | 内容 |
|---|---|
| JWT + Opaque の使い分け | access_token は JWT(DB 不要、jti のみ失効管理)、refresh_token は Opaque(DB 必須) |
| refresh_token rotate | 使い回しを防ぐため、リフレッシュごとに新トークンへローテーション |
| family_id | rotate のチェーンを追跡。盗難検知時に family 全体を一括失効 |
| クライアント認証 | パブリッククライアント(Claude/ChatGPT)は PKCE のみ、シークレットは optional |
| 動的クライアント登録 | RFC 7591 対応のため、自動登録もサポート |
| 同意管理 | scope ごとのユーザー同意を記録、UI から取り消し可能 |
| 権限の最小化 | scope は明示同意のもののみ access_token に乗せる |
テーブル定義
1. mappy_mcp_oauth_clients(OAuth クライアント)
| No | カラム名(論理) | カラム名(物理) | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|---|
| 1 | ID | id | bigint unsigned | NO | PK | 自動採番 |
| 2 | クライアント ID | client_id | varchar(64) | NO | UK | 公開可能な識別子(UUIDv4 等) |
| 3 | クライアントシークレットハッシュ | client_secret_hash | varchar(255) | YES | - | bcrypt ハッシュ。パブリッククライアントは NULL |
| 4 | クライアント名 | client_name | varchar(128) | NO | - | "Claude Desktop", "ChatGPT Connector" 等 |
| 5 | クライアント URI | client_uri | varchar(255) | YES | - | クライアントサービスの URL |
| 6 | ロゴ URI | logo_uri | varchar(255) | YES | - | 同意画面に表示するロゴ |
| 7 | ポリシー URI | policy_uri | varchar(255) | YES | - | プライバシーポリシー |
| 8 | 利用規約 URI | tos_uri | varchar(255) | YES | - | 利用規約 |
| 9 | リダイレクト URI 一覧 | redirect_uris | json | NO | - | 許可された callback URL の配列 |
| 10 | 許可 grant_types | grant_types | json | NO | - | ["authorization_code", "refresh_token"] |
| 11 | 許可スコープ | scopes | json | NO | - | このクライアントが要求可能な scope 配列 |
| 12 | トークン認証方法 | token_endpoint_auth_method | varchar(64) | NO | - | client_secret_basic / client_secret_post / none |
| 13 | クライアント種別 | client_type | varchar(16) | NO | - | confidential / public |
| 14 | 登録方法 | registration_method | varchar(16) | NO | - | manual / dynamic |
| 15 | 登録アクセストークン | registration_access_token_hash | varchar(255) | YES | - | RFC 7592 用、動的登録時に発行 |
| 16 | 有効フラグ | is_enabled | tinyint(1) | NO | - | 1: 有効 / 0: 停止 |
| 17 | 信頼レベル | trust_level | tinyint | NO | - | 0: untrusted / 1: trusted(公式 SDK 等) |
| 18 | 最終使用日時 | last_used_at | timestamp | YES | - | 最終トークン発行日時 |
| 19 | 作成日時 | created_at | timestamp | NO | - | 登録日時 |
| 20 | 更新日時 | updated_at | timestamp | NO | - | 最終更新日時 |
| 21 | 削除日時 | deleted_at | timestamp | YES | - | 論理削除 |
インデックス
| 名前 | カラム |
|---|---|
uk_client_id | client_id UNIQUE |
idx_clients_enabled | (is_enabled, created_at) |
idx_clients_trust | trust_level |
初期データ(マニュアル登録分)
公式クライアントは事前登録する:
| client_name | client_type | redirect_uris | trust_level |
|---|---|---|---|
| Claude Desktop | public | ["https://claude.ai/api/mcp/auth_callback"] | 1 |
| Claude Code | public | ["http://localhost:*/callback"](パターン許可) | 1 |
| ChatGPT Connector | public | ["https://chat.openai.com/oauth/callback"] | 1 |
2. mappy_mcp_oauth_consents(ユーザー同意)
ユーザーがクライアントに与えた scope の記録。同じクライアント+ユーザーで scope を変更した場合は新規行を追加し、旧行を revoked_at で失効。
| No | カラム名 | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | 自動採番 |
| 2 | client_id | varchar(64) | NO | IDX, FK | クライアント |
| 3 | user_id | bigint unsigned | NO | IDX, FK | mappy_users.id |
| 4 | user_kind | varchar(16) | NO | - | mappy / admin |
| 5 | admin_id | bigint unsigned | YES | FK | admins.id(user_kind=admin の場合) |
| 6 | scope_granted | text | NO | - | スペース区切り scope 文字列 |
| 7 | granted_at | timestamp | NO | - | 同意日時 |
| 8 | granted_ip | varchar(45) | YES | - | 同意時の IP |
| 9 | revoked_at | timestamp | YES | - | 取り消し日時 |
| 10 | revoked_reason | varchar(64) | YES | - | user_action / admin_action / token_compromise / expired |
インデックス
| 名前 | カラム |
|---|---|
idx_consents_client_user | (client_id, user_id, revoked_at) |
idx_consents_user_active | (user_id, revoked_at) |
3. mappy_mcp_oauth_authorization_codes(短期コード)
OAuth 認可コード。発行から 60 秒で失効、1 回使用で消費。 Redis 優先だが、永続化要件があれば DB も併用。
| No | カラム名 | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | 自動採番 |
| 2 | code_hash | varchar(255) | NO | UK | SHA-256(code) |
| 3 | client_id | varchar(64) | NO | IDX, FK | |
| 4 | user_id | bigint unsigned | NO | FK | |
| 5 | user_kind | varchar(16) | NO | - | |
| 6 | scope | text | NO | - | 要求 scope |
| 7 | redirect_uri | varchar(255) | NO | - | 認可時の redirect_uri |
| 8 | code_challenge | varchar(255) | NO | - | PKCE |
| 9 | code_challenge_method | varchar(16) | NO | - | "S256" |
| 10 | nonce | varchar(64) | YES | - | OpenID 用(将来) |
| 11 | issued_at | timestamp | NO | - | |
| 12 | expires_at | timestamp | NO | IDX | issued_at + 60 sec |
| 13 | consumed_at | timestamp | YES | - | トークン交換時刻 |
Redis 優先理由
authorization_code は短命(60 秒)かつ書込頻度が高いため、Redis(INCR + TTL)で実装する方が効率的。 監査用の永続記録は mappy_mcp_audit_logs 側で行う。 DB 化は「監査要件が高い」「永続化監査が必要」な場合のみ。
4. mappy_mcp_oauth_refresh_tokens(リフレッシュトークン)
長期トークン、rotate 必須。
| No | カラム名 | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | 自動採番 |
| 2 | token_hash | varchar(255) | NO | UK | SHA-256(token) |
| 3 | client_id | varchar(64) | NO | IDX, FK | |
| 4 | user_id | bigint unsigned | NO | IDX, FK | |
| 5 | user_kind | varchar(16) | NO | - | |
| 6 | admin_id | bigint unsigned | YES | FK | |
| 7 | scope | text | NO | - | |
| 8 | family_id | varchar(64) | NO | IDX | rotate チェーン識別子。盗難検知用 |
| 9 | parent_token_hash | varchar(255) | YES | - | 直前トークンの hash(チェーン記録) |
| 10 | issued_at | timestamp | NO | - | |
| 11 | expires_at | timestamp | NO | IDX | issued_at + 30 days |
| 12 | last_used_at | timestamp | YES | - | リフレッシュ時に更新 |
| 13 | rotated_at | timestamp | YES | - | rotate で失効した時刻 |
| 14 | revoked_at | timestamp | YES | - | 失効日時 |
| 15 | revoke_reason | varchar(64) | YES | - | user_revoke / rotation / family_compromise / expiry |
| 16 | request_ip | varchar(45) | YES | - | 発行時 IP |
| 17 | user_agent | varchar(255) | YES | - | |
| 18 | created_at | timestamp | NO | - |
インデックス
| 名前 | カラム |
|---|---|
uk_token_hash | token_hash UNIQUE |
idx_refresh_client_user | (client_id, user_id) |
idx_refresh_family | family_id |
idx_refresh_expires | expires_at |
idx_refresh_active | (user_id, revoked_at, expires_at) |
family_id による盗難検知
- 親トークンが既に rotate 済みなのに使われた → token reuse 検出
- 該当 family_id の全トークンを失効
- ユーザーに通知 + アラート
OAuth 2.1 ベストプラクティスの中核。
5. mappy_mcp_oauth_access_token_revocations(アクセストークン失効リスト)
access_token は JWT(自己完結型)のため通常 DB チェック不要だが、失効時のために jti を一時保存する。
| No | カラム名 | データ型 | NULL | キー | 説明 |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | 自動採番 |
| 2 | jti | varchar(64) | NO | UK | JWT ID |
| 3 | client_id | varchar(64) | NO | IDX | |
| 4 | user_id | bigint unsigned | NO | IDX | |
| 5 | revoked_at | timestamp | NO | - | 失効日時 |
| 6 | revoke_reason | varchar(64) | NO | - | |
| 7 | expires_at | timestamp | NO | IDX | 元 JWT の exp。これを過ぎたら DB から削除可 |
| 8 | created_at | timestamp | NO | - |
運用
- access_token 検証時、まず JWT 署名 + exp チェック → OK なら jti が本テーブルに無いか確認
expires_at < NOW()の行は日次 DELETE- パフォーマンスのため Redis にもキャッシュ(Bloom Filter で否定検索を最適化)
インデックス
| 名前 | カラム |
|---|---|
uk_jti | jti UNIQUE |
idx_revocations_expires | expires_at |
トークンライフサイクル(フロー再掲)
クライアント登録パターン
A) マニュアル登録(公式クライアント)
- DB に直接 INSERT(migration / seeder)
trust_level=1registration_method='manual'
B) 動的登録(RFC 7591)
http
POST /oauth/register
Content-Type: application/json
{
"client_name": "Custom MCP Client",
"redirect_uris": ["https://example.com/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"]
}レスポンス:
json
{
"client_id": "01HVABC...",
"client_secret": null,
"registration_access_token": "regtok_...",
"registration_client_uri": "https://mcp.mappy.example.com/oauth/register/01HVABC..."
}動的登録の制限
ChatGPT 互換のため動的登録は許可するが、untrusted(trust_level=0)として扱う:
- レート制限を厳しくする(1 時間 60 リクエスト)
- 同意画面で「未確認のサードパーティ」と警告表示
- 1 ユーザーあたり同時に登録できるクライアント数を制限(5 個まで)
容量見積もり
| テーブル | 想定行数(1 年) |
|---|---|
| mappy_mcp_oauth_clients | 〜200 行 |
| mappy_mcp_oauth_consents | 〜10,000 行 |
| mappy_mcp_oauth_refresh_tokens | 〜200,000 行(rotate 込み) |
| mappy_mcp_oauth_access_token_revocations | 〜100,000 行(自動 DELETE) |
→ 合計でも 100 MB 未満。容量懸念なし。
DDL(参考)
sql
-- ① OAuth クライアント
CREATE TABLE mappy_mcp_oauth_clients (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
client_id VARCHAR(64) NOT NULL,
client_secret_hash VARCHAR(255) DEFAULT NULL,
client_name VARCHAR(128) NOT NULL,
client_uri VARCHAR(255) DEFAULT NULL,
logo_uri VARCHAR(255) DEFAULT NULL,
policy_uri VARCHAR(255) DEFAULT NULL,
tos_uri VARCHAR(255) DEFAULT NULL,
redirect_uris JSON NOT NULL,
grant_types JSON NOT NULL,
scopes JSON NOT NULL,
token_endpoint_auth_method VARCHAR(64) NOT NULL DEFAULT 'none',
client_type VARCHAR(16) NOT NULL DEFAULT 'public',
registration_method VARCHAR(16) NOT NULL DEFAULT 'manual',
registration_access_token_hash VARCHAR(255) DEFAULT NULL,
is_enabled TINYINT(1) NOT NULL DEFAULT 1,
trust_level TINYINT NOT NULL DEFAULT 0,
last_used_at TIMESTAMP NULL DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_client_id (client_id),
KEY idx_clients_enabled (is_enabled, created_at),
KEY idx_clients_trust (trust_level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MCP OAuth クライアント';
-- ② ユーザー同意
CREATE TABLE mappy_mcp_oauth_consents (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
client_id VARCHAR(64) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
user_kind VARCHAR(16) NOT NULL,
admin_id BIGINT UNSIGNED DEFAULT NULL,
scope_granted TEXT NOT NULL,
granted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
granted_ip VARCHAR(45) DEFAULT NULL,
revoked_at TIMESTAMP NULL DEFAULT NULL,
revoked_reason VARCHAR(64) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_consents_client_user (client_id, user_id, revoked_at),
KEY idx_consents_user_active (user_id, revoked_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MCP OAuth ユーザー同意';
-- ③ リフレッシュトークン
CREATE TABLE mappy_mcp_oauth_refresh_tokens (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
token_hash VARCHAR(255) NOT NULL,
client_id VARCHAR(64) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
user_kind VARCHAR(16) NOT NULL,
admin_id BIGINT UNSIGNED DEFAULT NULL,
scope TEXT NOT NULL,
family_id VARCHAR(64) NOT NULL,
parent_token_hash VARCHAR(255) DEFAULT NULL,
issued_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP NULL DEFAULT NULL,
rotated_at TIMESTAMP NULL DEFAULT NULL,
revoked_at TIMESTAMP NULL DEFAULT NULL,
revoke_reason VARCHAR(64) DEFAULT NULL,
request_ip VARCHAR(45) DEFAULT NULL,
user_agent VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_token_hash (token_hash),
KEY idx_refresh_client_user (client_id, user_id),
KEY idx_refresh_family (family_id),
KEY idx_refresh_expires (expires_at),
KEY idx_refresh_active (user_id, revoked_at, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MCP OAuth リフレッシュトークン';
-- ④ アクセストークン失効リスト
CREATE TABLE mappy_mcp_oauth_access_token_revocations (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
jti VARCHAR(64) NOT NULL,
client_id VARCHAR(64) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
revoked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
revoke_reason VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_jti (jti),
KEY idx_revocations_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MCP OAuth アクセストークン失効リスト';メンテナンスジョブ
| ジョブ | 頻度 | 内容 |
|---|---|---|
| 期限切れ refresh_token 削除 | 日次 | WHERE expires_at < NOW() - INTERVAL 30 DAY を物理削除 |
| revocations の expires_at 経過削除 | 日次 | WHERE expires_at < NOW() を物理削除 |
| 30 日未使用 client の警告 | 月次 | last_used_at < NOW() - INTERVAL 30 DAY をリスト化 |
| family_compromise の検知ログ集計 | 日次 | revoke_reason='family_compromise' を集計し管理通知 |
鍵管理(JWT 署名鍵)
| 項目 | 内容 |
|---|---|
| アルゴリズム | RS256(推奨)または HS256 |
| 鍵長 | RS256: 2048bit 以上 |
| ローテーション周期 | 90 日 |
| 保管 | AWS KMS / Secrets Manager |
kid(key id) | JWT ヘッダーに含め、複数鍵を並行運用可能に |
| 公開鍵公開 | /.well-known/jwks.json で配布(リソースサーバー用) |
未確定事項(設計レビューで決定)
| # | 項目 | 候補 |
|---|---|---|
| 1 | JWT 署名アルゴリズム | RS256(推奨)/ HS256 |
| 2 | refresh_token TTL | 30 日 / 14 日 |
| 3 | authorization_code 保管 | Redis 必須 / DB 併用 |
| 4 | access_token TTL | 60 分 / 30 分 |
| 5 | family_compromise 時のユーザー通知方法 | メール / Slack / 管理画面 |
| 6 | 動的登録の許可 | 全許可 / Allow list で制限 |
| 7 | trust_level の決定主体 | 自動 / 管理者承認 |
| 8 | クライアント停止時の挙動 | 即時失効 / 既存トークンは expires_at まで有効 |
関連ドキュメント
- MCP サーバー 全体像
- 認証・テナンシ — OAuth フローの詳細
- DB 設計(監査ログ) — トークン発行・失効の記録