mappy_mcp_oauth_* (DB OAuth 2.1 Authz Server cho MCP)
| Mục | Nội dung |
|---|---|
| Trạng thái | 🟡 Đang thiết kế |
| Case liên quan | #13 MCP Server (Thiết lập mới) |
| Tài liệu cha | MCP Server (Tổng thể) |
| Design liên quan | Auth & Tenancy / DB (audit log) |
ER diagram
Tổng quan
Quản lý dữ liệu persistent cần thiết cho OAuth 2.1 Authz Server của MCP.
OAuth hiện có của Mappy không tái sử dụng được
Theo kết quả check DB, Mappy không có bảng Laravel Passport nên không tái sử dụng được. Xây mới nhóm bảng này.
Phương châm thiết kế
| Phương châm | Nội dung |
|---|---|
| Tách JWT + Opaque | access_token là JWT (không cần DB, chỉ quản lý revoke qua jti), refresh_token là Opaque (bắt buộc DB) |
| Refresh token rotate | Để chống tái sử dụng, mỗi lần refresh thì rotate sang token mới |
| family_id | Theo dõi chain rotate. Khi phát hiện trộm thì revoke toàn family |
| Xác thực client | Public client (Claude/ChatGPT) chỉ PKCE, secret là optional |
| Dynamic client registration | Support auto đăng ký theo RFC 7591 |
| Quản lý đồng ý | Ghi user consent theo scope, có thể hủy từ UI |
| Tối thiểu hóa quyền | Chỉ scope được đồng ý rõ ràng mới gắn vào access_token |
Định nghĩa bảng
1. mappy_mcp_oauth_clients (OAuth client)
| No | Tên cột (logical) | Tên cột (physical) | Kiểu | NULL | Key | Mô tả |
|---|---|---|---|---|---|---|
| 1 | ID | id | bigint unsigned | NO | PK | Auto increment |
| 2 | Client ID | client_id | varchar(64) | NO | UK | Identifier có thể public (UUIDv4 v.v.) |
| 3 | Hash client secret | client_secret_hash | varchar(255) | YES | - | Hash bcrypt. Public client = NULL |
| 4 | Tên client | client_name | varchar(128) | NO | - | "Claude Desktop", "ChatGPT Connector" v.v. |
| 5 | Client URI | client_uri | varchar(255) | YES | - | URL của dịch vụ client |
| 6 | Logo URI | logo_uri | varchar(255) | YES | - | Logo hiển thị trên màn đồng ý |
| 7 | Policy URI | policy_uri | varchar(255) | YES | - | Privacy policy |
| 8 | TOS URI | tos_uri | varchar(255) | YES | - | Điều khoản sử dụng |
| 9 | Danh sách redirect URI | redirect_uris | json | NO | - | Mảng callback URL được phép |
| 10 | grant_types được phép | grant_types | json | NO | - | ["authorization_code", "refresh_token"] |
| 11 | Scope được phép | scopes | json | NO | - | Mảng scope mà client này có thể yêu cầu |
| 12 | Phương thức xác thực token | token_endpoint_auth_method | varchar(64) | NO | - | client_secret_basic / client_secret_post / none |
| 13 | Loại client | client_type | varchar(16) | NO | - | confidential / public |
| 14 | Cách đăng ký | registration_method | varchar(16) | NO | - | manual / dynamic |
| 15 | Registration access token | registration_access_token_hash | varchar(255) | YES | - | Cho RFC 7592, phát khi đăng ký động |
| 16 | Flag hoạt động | is_enabled | tinyint(1) | NO | - | 1: bật / 0: dừng |
| 17 | Mức độ tin cậy | trust_level | tinyint | NO | - | 0: untrusted / 1: trusted (SDK chính thức v.v.) |
| 18 | Thời điểm dùng cuối | last_used_at | timestamp | YES | - | Thời điểm phát token cuối |
| 19 | Created at | created_at | timestamp | NO | - | Thời điểm đăng ký |
| 20 | Updated at | updated_at | timestamp | NO | - | Thời điểm update cuối |
| 21 | Deleted at | deleted_at | timestamp | YES | - | Logical delete |
Index
| Tên | Cột |
|---|---|
uk_client_id | client_id UNIQUE |
idx_clients_enabled | (is_enabled, created_at) |
idx_clients_trust | trust_level |
Dữ liệu khởi tạo (đăng ký thủ công)
Client chính thức đăng ký trước:
| 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"] (cho phép pattern) | 1 |
| ChatGPT Connector | public | ["https://chat.openai.com/oauth/callback"] | 1 |
2. mappy_mcp_oauth_consents (đồng ý user)
Ghi scope mà user cấp cho client. Khi cùng client+user nhưng thay đổi scope, thêm dòng mới và revoke dòng cũ bằng revoked_at.
| No | Tên cột | Kiểu | NULL | Key | Mô tả |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | Auto increment |
| 2 | client_id | varchar(64) | NO | IDX, FK | Client |
| 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 (khi user_kind=admin) |
| 6 | scope_granted | text | NO | - | Chuỗi scope cách bằng space |
| 7 | granted_at | timestamp | NO | - | Thời điểm đồng ý |
| 8 | granted_ip | varchar(45) | YES | - | IP khi đồng ý |
| 9 | revoked_at | timestamp | YES | - | Thời điểm hủy |
| 10 | revoked_reason | varchar(64) | YES | - | user_action / admin_action / token_compromise / expired |
Index
| Tên | Cột |
|---|---|
idx_consents_client_user | (client_id, user_id, revoked_at) |
idx_consents_user_active | (user_id, revoked_at) |
3. mappy_mcp_oauth_authorization_codes (code ngắn hạn)
Authorization code OAuth. Hết hạn 60 giây sau khi phát, tiêu thụ 1 lần. Ưu tiên Redis, nhưng nếu có yêu cầu persist thì dùng cả DB.
| No | Tên cột | Kiểu | NULL | Key | Mô tả |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | Auto increment |
| 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 yêu cầu |
| 7 | redirect_uri | varchar(255) | NO | - | redirect_uri lúc authorize |
| 8 | code_challenge | varchar(255) | NO | - | PKCE |
| 9 | code_challenge_method | varchar(16) | NO | - | "S256" |
| 10 | nonce | varchar(64) | YES | - | Cho OpenID (tương lai) |
| 11 | issued_at | timestamp | NO | - | |
| 12 | expires_at | timestamp | NO | IDX | issued_at + 60 sec |
| 13 | consumed_at | timestamp | YES | - | Thời điểm đổi token |
Lý do ưu tiên Redis
authorization_code ngắn (60 giây) và tần suất ghi cao, nên implement bằng Redis (INCR + TTL) hiệu quả hơn. Ghi persistent cho audit thực hiện ở mappy_mcp_audit_logs. DB hóa chỉ khi "yêu cầu audit cao" hoặc "cần audit persistent".
4. mappy_mcp_oauth_refresh_tokens (refresh token)
Token dài hạn, bắt buộc rotate.
| No | Tên cột | Kiểu | NULL | Key | Mô tả |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | Auto increment |
| 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 | Identifier chain rotate. Cho detection trộm |
| 9 | parent_token_hash | varchar(255) | YES | - | Hash của token ngay trước (chain) |
| 10 | issued_at | timestamp | NO | - | |
| 11 | expires_at | timestamp | NO | IDX | issued_at + 30 ngày |
| 12 | last_used_at | timestamp | YES | - | Update khi refresh |
| 13 | rotated_at | timestamp | YES | - | Thời điểm revoke do rotate |
| 14 | revoked_at | timestamp | YES | - | Thời điểm revoke |
| 15 | revoke_reason | varchar(64) | YES | - | user_revoke / rotation / family_compromise / expiry |
| 16 | request_ip | varchar(45) | YES | - | IP khi phát |
| 17 | user_agent | varchar(255) | YES | - | |
| 18 | created_at | timestamp | NO | - |
Index
| Tên | Cột |
|---|---|
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) |
Detection trộm bằng family_id
- Parent token đã rotate nhưng vẫn được dùng → phát hiện token reuse
- Revoke toàn bộ token thuộc family_id đó
- Thông báo user + alert
Trung tâm best practice OAuth 2.1.
5. mappy_mcp_oauth_access_token_revocations (danh sách access token đã revoke)
access_token là JWT (self-contained) nên thường không cần check DB, nhưng lưu jti tạm thời cho trường hợp revoke.
| No | Tên cột | Kiểu | NULL | Key | Mô tả |
|---|---|---|---|---|---|
| 1 | id | bigint unsigned | NO | PK | Auto increment |
| 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 | - | Thời điểm revoke |
| 6 | revoke_reason | varchar(64) | NO | - | |
| 7 | expires_at | timestamp | NO | IDX | exp của JWT gốc. Qua thời điểm này có thể xóa khỏi DB |
| 8 | created_at | timestamp | NO | - |
Vận hành
- Khi verify access_token, trước hết check JWT signature + exp → OK thì check jti có trong bảng này không
- Hàng
expires_at < NOW()DELETE hàng ngày - Cache Redis cho performance (tối ưu negative lookup bằng Bloom Filter)
Index
| Tên | Cột |
|---|---|
uk_jti | jti UNIQUE |
idx_revocations_expires | expires_at |
Vòng đời token (re-flow)
Pattern đăng ký client
A) Đăng ký thủ công (client chính thức)
- INSERT trực tiếp vào DB (migration / seeder)
trust_level=1registration_method='manual'
B) Đăng ký động (RFC 7591)
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"]
}Response:
{
"client_id": "01HVABC...",
"client_secret": null,
"registration_access_token": "regtok_...",
"registration_client_uri": "https://mcp.mappy.example.com/oauth/register/01HVABC..."
}Giới hạn đăng ký động
Cho phép đăng ký động vì tương thích ChatGPT, nhưng xử lý như untrusted (trust_level=0):
- Rate limit chặt hơn (60 request/giờ)
- Hiển thị cảnh báo "Third party chưa xác minh" trên màn đồng ý
- Giới hạn số client có thể đăng ký đồng thời / user (tối đa 5)
Ước lượng dung lượng
| Bảng | Số dòng giả định (1 năm) |
|---|---|
| mappy_mcp_oauth_clients | 〜200 dòng |
| mappy_mcp_oauth_consents | 〜10,000 dòng |
| mappy_mcp_oauth_refresh_tokens | 〜200,000 dòng (kể cả rotate) |
| mappy_mcp_oauth_access_token_revocations | 〜100,000 dòng (auto DELETE) |
→ Tổng cũng < 100 MB. Không lo dung lượng.
DDL (tham khảo)
-- ① OAuth client
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 client';
-- ② User consent
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 user consent';
-- ③ Refresh token
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 refresh token';
-- ④ Danh sách revoke access token
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 access token revocations';Maintenance job
| Job | Tần suất | Nội dung |
|---|---|---|
| Xóa refresh_token hết hạn | Hàng ngày | Physical delete WHERE expires_at < NOW() - INTERVAL 30 DAY |
| Xóa revocations đã quá expires_at | Hàng ngày | Physical delete WHERE expires_at < NOW() |
| Cảnh báo client 30 ngày không dùng | Hàng tháng | List last_used_at < NOW() - INTERVAL 30 DAY |
| Tổng hợp log detection family_compromise | Hàng ngày | Tổng hợp revoke_reason='family_compromise', thông báo management |
Quản lý key (key ký JWT)
| Mục | Nội dung |
|---|---|
| Algorithm | RS256 (đề xuất) hoặc HS256 |
| Độ dài key | RS256: ≥ 2048bit |
| Chu kỳ rotation | 90 ngày |
| Lưu trữ | AWS KMS / Secrets Manager |
kid (key id) | Đặt trong JWT header, có thể vận hành nhiều key song song |
| Công khai public key | Phân phối qua /.well-known/jwks.json (cho resource server) |
Điểm chưa chốt (quyết định tại review design)
| # | Mục | Phương án |
|---|---|---|
| 1 | Algorithm ký JWT | RS256 (đề xuất) / HS256 |
| 2 | TTL refresh_token | 30 ngày / 14 ngày |
| 3 | Lưu authorization_code | Bắt buộc Redis / dùng cả DB |
| 4 | TTL access_token | 60 phút / 30 phút |
| 5 | Cách thông báo user khi family_compromise | Email / Slack / Admin panel |
| 6 | Cho phép đăng ký động | Cho phép tất / Allow list giới hạn |
| 7 | Chủ thể quyết định trust_level | Auto / Admin duyệt |
| 8 | Hành vi khi dừng client | Revoke ngay / Token hiện có vẫn dùng đến expires_at |
Tài liệu liên quan
- MCP Server (Tổng thể)
- Auth & Tenancy — Chi tiết OAuth flow
- DB (audit log) — Ghi phát・revoke token