MCP Server — Thiết kế Auth & Tenancy
| 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ể) |
| Thiết kế DB liên quan | OAuth client DB |
1. Mục đích tài liệu
Chốt Authentication (ai) và Authorization・Tenancy (có thể thao tác gì) của MCP Server.
- Spec OAuth 2.1 Authz Server (flow・endpoint・token)
- Hệ thống scope và mapping với quyền hiện có của Mappy
- Thuật toán xác định tenancy (phân tầng user・ranh giới cửa hàng)
- Cách xử lý thao tác thay mặt admin
- Yêu cầu bảo mật
Tài liệu này không mô tả khác biệt stack triển khai (Python / TypeScript) — language-independent.
2. Điều kiện tiên quyết
2.1 Hiện trạng auth của Mappy (kết quả kiểm tra DB)
| Mục | Kết quả |
|---|---|
| Bảng Laravel Passport | Không tồn tại (bảng oauth_* có 0 dòng) |
| Bảng Laravel Sanctum | Không tồn tại (không có personal_access_tokens) |
| Bảng session Laravel | Không tồn tại (không có sessions → file session) |
config/auth.php | Đã định nghĩa 3 guard passport nhưng bảng chưa tạo nên thực tế chưa hoạt động |
| Vận hành thực tế | Chỉ có web session authentication |
OAuth 2.1 sẽ xây mới hoàn toàn
Dependency Passport vẫn còn trong composer.json nhưng chưa migrate nên không tái sử dụng được. Sẽ xây mới OAuth 2.1 Authz Server cho MCP.
2.2 Ràng buộc của spec MCP
- Bắt buộc OAuth 2.1 (spec MCP cũng chấp nhận OAuth 2.0 nhưng vì xây mới nên dùng 2.1)
- Bắt buộc PKCE (
code_challenge_method=S256) - Bỏ Implicit Flow
- Bỏ Resource Owner Password Credentials Grant
- Dynamic Client Registration (RFC 7591) support (để tương thích ChatGPT Connectors)
- OAuth Metadata (RFC 8414) support (
/.well-known/oauth-authorization-server)
3. Chi tiết flow OAuth 2.1
3.1 Flow authorization code + PKCE (chính)
3.2 Flow refresh token
3.3 Dynamic client registration (RFC 7591)
ChatGPT Connectors yêu cầu đăng ký client động nên bắt buộc support.
Khác biệt với public client
Claude Desktop / ChatGPT thực chất là public client (không giấu được client_secret). Thay bằng PKCE, cho phép token_endpoint_auth_method=none.
3.4 Token revocation (RFC 7009)
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=...&token_type_hint=access_tokenKhi user chọn "Hủy liên kết" từ màn admin Mappy, revoke access_token / refresh_token tương ứng.
4. Spec endpoint
| Path | Method | Auth | Mục đích |
|---|---|---|---|
/.well-known/oauth-authorization-server | GET | Không cần | OAuth metadata |
/.well-known/oauth-protected-resource | GET | Không cần | Resource server metadata |
/oauth/register | POST | Không cần | Dynamic client registration |
/oauth/authorize | GET | Mappy session | Endpoint authorize |
/oauth/authorize/consent | POST | Mappy session | Xử lý đồng ý |
/oauth/token | POST | client_id (+ secret) | Phát・refresh token |
/oauth/revoke | POST | client_id (+ secret) | Revoke token |
/oauth/introspect | POST | client_id + secret | Verify token (nội bộ) |
4.1 Ví dụ OAuth metadata
{
"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. Thiết kế scope
5.1 Quy tắc đặt tên scope
mappy:<domain>:<action>| Phần | Ví dụ |
|---|---|
mappy | Prefix cố định (dự phòng cho tương lai có thêm admin: v.v.) |
<domain> | location / posts / reviews / ranking / insight / report / keyword / group / media / sms / questionnaire / sns / notification |
<action> | read / write / delete (tùy nhu cầu) |
5.2 Danh sách scope (theo Phase)
Phase 1 (MVP) — Read
| scope | Mô tả | Ví dụ tool tương ứng |
|---|---|---|
mappy:location:read | Lấy danh sách・chi tiết location | location.list, location.get |
mappy:reviews:read | Lấy danh sách・chi tiết review | review.list, review.get |
mappy:ranking:read | Lấy xếp hạng tìm kiếm | ranking.list |
mappy:insight:read | Lấy insight | insight.summary |
mappy:keyword:read | Lấy từ khóa・lưu lượng | keyword.list |
mappy:group:read | Lấy group・cửa hàng thuộc | group.list |
mappy:posts:read | Lấy danh sách・chi tiết post | post.list |
mappy:media:read | Lấy danh sách media | media.list |
mappy:report:read | Lấy danh sách・report | report.list, report.get |
mappy:questionnaire:read | Lấy danh sách・câu trả lời khảo sát | questionnaire.list |
mappy:sms:read | Lấy log SMS | sms.list |
Phase 2 — Write
| scope | Mô tả |
|---|---|
mappy:posts:write | Tạo・lên lịch post |
mappy:reviews:write | Trả lời review・đăng ký template |
mappy:media:write | Upload media |
mappy:media:delete | Xóa media |
mappy:location:write | Update thuộc tính location |
Phase 3 — Báo cáo・Phân tích
| scope | Mô tả |
|---|---|
mappy:report:write | Yêu cầu tạo report |
mappy:keyword:write | Đăng ký keyword (tương lai) |
mappy:group:write | Quản lý group (tương lai) |
5.3 Tương ứng với quyền hiện có của Mappy
User Mappy có sẵn các cột access level, scope phụ thuộc vào chúng.
| MCP scope | Điều kiện phía Mappy cần |
|---|---|
mappy:location:read | mappy_users.is_enabled = 1 |
mappy:location:write | Trên + gbp_connection_settings_access_level >= 2 |
mappy:posts:read | is_enabled = 1 + location đó có trong mappy_user_available_gbp_locations |
mappy:posts:write | Trên + mappy_business_accounts.permission_level của GBP đó cho phép write |
mappy:reviews:write | is_enabled = 1 + location đó dùng được |
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 + (cho MAIN_USER) flag quyền report |
mappy:report:write | Trên + flag quyền write |
Ý đồ kiểm soát kép
scope = loại thao tác được cho phép qua MCP, cột phía Mappy = bật/tắt tính năng cho từng user. AI khách dù có mappy:ranking:read, nếu user đối tượng có search_ranking_enabled = 0 thì từ chối (trả 403).
6. Thuật toán xác định tenancy
6.1 Thuật ngữ
| Thuật ngữ | Nghĩa |
|---|---|
| User auth | mappy_users.id (hoặc admins.id) đã đồng ý OAuth và là chủ token |
| User đối tượng | mappy_users.id mà tool thao tác (thường giống user auth) |
| Cửa hàng đối tượng | mappy_gbp_locations.id mà tool thao tác |
6.2 Phạm vi thao tác theo loại user auth
6.3 Xác định ranh giới cửa hàng
Khi đã chốt user đối tượng, lọc cửa hàng có thể thao tác:
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_idNgoại lệ tên bảng
mappy_group_locationlà dạng số ít (không phải số nhiều)adminskhông có prefixmappy_
Đã xác nhận qua kết quả check DB.
6.4 Khi chỉ định group_id
Khi input của tool có group_id, thêm kiểm tra:
SELECT 1
FROM mappy_groups
WHERE id = :group_id
AND user_id = :resolved_user_id
LIMIT 1;Không có → 403.
6.5 Pseudo code của hàm phán đoán
def resolve_scope(token: AccessToken, input: dict) -> ResolvedContext:
# 1. Xác định user auth
auth = token.subject # {kind: 'user'|'admin', id: ..., is_supervisor: ...}
# 2. Xác định user đối tượng
if auth.kind == 'admin':
if auth.is_supervisor:
target_user_id = input.get('user_id') # bắt buộc chỉ định rõ
if target_user_id is None:
raise InvalidInput("user_id required for supervisor admin")
else:
target_user_id = token.preview_user_id # phạm vi phụ trách
else: # mappy user
target_user_id = input.get('user_id', auth.id)
# Kiểm tra phân tầng
if not is_in_hierarchy(auth.id, target_user_id):
raise Forbidden("target_user_id out of hierarchy")
# 3. Kiểm tra flag tính năng
user = fetch_user(target_user_id)
if not is_enabled_for_scope(user, token.scopes):
raise Forbidden("scope feature disabled for target user")
# 4. Kiểm tra ranh giới cửa hàng
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. Xử lý thao tác thay mặt admin
7.1 Hành vi hiện có
- Mappy có cơ chế cho phép admin giả danh user qua cookie
preview_user_id - Hoạt động bằng session auth + cookie
7.2 Cách xử lý tại MCP (theo Phase)
| Phase | Cách xử lý |
|---|---|
| Phase 1 (MVP) | Chỉ access với tư cách admin bản thân. Không cho phép thay mặt (tránh phức tạp implement) |
| Phase 2 | Chỉ admin is_supervisor=1 mới được thay mặt khi chỉ định user_id trong input tool |
| Phase 3 | Admin thường cũng có thể thay mặt user phụ trách (chọn user đối tượng khi authorize OAuth) |
7.3 Flow Phase 2 trở đi (tạm)
Tầm quan trọng của audit
Thao tác thay mặt có rủi ro audit cao. Phải ghi toàn bộ "ai thay mặt ai làm gì" vào Audit Log. Chi tiết xem DB (audit log).
8. Vòng đời token
| Token | Hạn | Rotate | Lưu trữ |
|---|---|---|---|
authorization_code | 60 giây | Tiêu thụ 1 lần | Redis (chỉ memory) |
access_token | 60 phút | Refresh sinh mới | Chỉ client (server chỉ giữ jti) |
refresh_token | 30 ngày (rolling) | Refresh rotate (cũ revoke) | DB |
id_token (tương lai) | 60 phút | — | Chỉ client |
8.1 JWT vs Opaque
- access_token: JWT (HS256 / RS256) — giảm chi phí verify
- refresh_token: Opaque (bắt buộc search DB, rotation an toàn)
8.2 Ví dụ JWT claim
{
"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 Chiến lược revoke
| Sự kiện | Hành động |
|---|---|
| User hủy đăng ký Mappy | Revoke toàn bộ access_token / refresh_token của sub đó |
| User chọn "Hủy liên kết" | Revoke toàn bộ token của client đó |
Client gọi /oauth/revoke | Revoke token đó |
| Đổi password | Tương tự |
| Hành vi đáng ngờ (rate limit hit N lần liên tiếp v.v.) | Auto revoke + alert |
9. Yêu cầu bảo mật
9.1 Bắt buộc
| Mục | Đáp ứng |
|---|---|
| PKCE | Bắt buộc code_challenge_method=S256, cấm plain |
| state | Chống CSRF, bắt buộc |
| redirect_uri | Khớp tuyệt đối (cấm partial match) |
| Tái sử dụng code | Tiêu thụ 1 lần, khi phát hiện reuse revoke toàn bộ token liên quan |
| Refresh token rotate | Bắt buộc, khi phát hiện thì revoke cả family |
| Phát hiện rò rỉ token | Giám sát kết hợp jti + IP |
| Bắt buộc HTTPS | Không nhận HTTP, bật HSTS |
| Cookie secure | Secure, HttpOnly, SameSite=Lax |
9.2 Whitelist redirect_uri
Verify redirect_uri chính thức của Claude / ChatGPT khi đăng ký client:
| Client | Pattern redirect_uri đã biết |
|---|---|
| Claude Desktop | https://claude.ai/api/mcp/auth_callback |
| Claude Code | http://localhost:<port>/callback (động) |
| ChatGPT Connectors | https://chat.openai.com/oauth/callback (tạm) |
Cách xử lý localhost
Claude Code dùng port localhost động, nên cho phép pattern http://localhost:*/callback khi đăng ký (chỉ với client opt-in rõ ràng).
9.3 Security headers
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 Log・Audit
- Cấm log payload chứa password / token / code
- Log chỉ ghi
jti(JWT ID), không ghi token body - Authz failure・token reject giám sát qua CloudWatch Alarm
10. Error response
10.1 OAuth error (chuẩn OAuth 2.1)
| code | HTTP | Ví dụ |
|---|---|---|
invalid_request | 400 | Thiếu tham số bắt buộc |
invalid_client | 401 | client_id sai |
invalid_grant | 400 | code hết hạn・code_verifier không khớp |
unauthorized_client | 400 | Client không được phép dùng grant_type đó |
unsupported_grant_type | 400 | grant_type không support |
invalid_scope | 400 | scope không biết |
access_denied | 403 | User từ chối đồng ý |
server_error | 500 | Lỗi nội bộ server |
10.2 Lỗi khi MCP request (JSON-RPC)
| code | Nghĩa |
|---|---|
| -32000 | Thiếu scope |
| -32001 | Lỗi tenancy (ngoài phạm vi) |
| -32002 | Flag tính năng tắt (user đối tượng tắt tính năng đó) |
| -32003 | Rate limit |
| -32602 | Schema input sai |
| -32603 | Lỗi nội bộ |
11. Điểm tranh luận tại review (chưa chốt)
| # | Điểm | Phương án |
|---|---|---|
| 1 | Hạn access_token | 60 phút (chuẩn) / 30 phút (chặt) |
| 2 | Thời gian rolling refresh_token | 30 ngày / 14 ngày |
| 3 | Thuật toán ký JWT | HS256 (key chung) / RS256 (key công khai) |
| 4 | Có support thao tác thay mặt admin ở Phase 1 không | Đề xuất: Phase 1 không support |
| 5 | Độ chi tiết màn hình đồng ý scope | Theo scope / theo nhóm |
| 6 | Vị trí màn hình hủy liên kết | Thêm vào màn admin Mappy |
| 7 | Liên kết với Web session hiện có | Có nên dùng lại Web session ở endpoint authorize OAuth không |
| 8 | Cách lưu client_secret | bcrypt hash trong DB / mã hóa KMS |
12. Tài liệu liên quan
- MCP Server (Tổng thể)
- Tool Catalog — Áp dụng cụ thể scope cho tool
- Cơ chế an toàn write — Kiểm tra bổ sung cho write
- DB (OAuth client) — DB schema
- DB (audit log) — Ghi authz thành công/thất bại