Skip to content

MCP Server — Thiết kế Auth & Tenancy

MụcNộ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 chaMCP Server (Tổng thể)
Thiết kế DB liên quanOAuth client DB

1. Mục đích tài liệu

Chốt Authentication (ai)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ụcKết quả
Bảng Laravel PassportKhông tồn tại (bảng oauth_* có 0 dòng)
Bảng Laravel SanctumKhông tồn tại (không có personal_access_tokens)
Bảng session LaravelKhô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)

http
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded

token=...&token_type_hint=access_token

Khi 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

PathMethodAuthMục đích
/.well-known/oauth-authorization-serverGETKhông cầnOAuth metadata
/.well-known/oauth-protected-resourceGETKhông cầnResource server metadata
/oauth/registerPOSTKhông cầnDynamic client registration
/oauth/authorizeGETMappy sessionEndpoint authorize
/oauth/authorize/consentPOSTMappy sessionXử lý đồng ý
/oauth/tokenPOSTclient_id (+ secret)Phát・refresh token
/oauth/revokePOSTclient_id (+ secret)Revoke token
/oauth/introspectPOSTclient_id + secretVerify token (nội bộ)

4.1 Ví dụ OAuth metadata

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. Thiết kế scope

5.1 Quy tắc đặt tên scope

mappy:<domain>:<action>
PhầnVí dụ
mappyPrefix 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

scopeMô tảVí dụ tool tương ứng
mappy:location:readLấy danh sách・chi tiết locationlocation.list, location.get
mappy:reviews:readLấy danh sách・chi tiết reviewreview.list, review.get
mappy:ranking:readLấy xếp hạng tìm kiếmranking.list
mappy:insight:readLấy insightinsight.summary
mappy:keyword:readLấy từ khóa・lưu lượngkeyword.list
mappy:group:readLấy group・cửa hàng thuộcgroup.list
mappy:posts:readLấy danh sách・chi tiết postpost.list
mappy:media:readLấy danh sách mediamedia.list
mappy:report:readLấy danh sách・reportreport.list, report.get
mappy:questionnaire:readLấy danh sách・câu trả lời khảo sátquestionnaire.list
mappy:sms:readLấy log SMSsms.list

Phase 2 — Write

scopeMô tả
mappy:posts:writeTạo・lên lịch post
mappy:reviews:writeTrả lời review・đăng ký template
mappy:media:writeUpload media
mappy:media:deleteXóa media
mappy:location:writeUpdate thuộc tính location

Phase 3 — Báo cáo・Phân tích

scopeMô tả
mappy:report:writeYêu cầu tạo report
mappy:keyword:writeĐăng ký keyword (tương lai)
mappy:group:writeQuả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:readmappy_users.is_enabled = 1
mappy:location:writeTrên + gbp_connection_settings_access_level >= 2
mappy:posts:readis_enabled = 1 + location đó có trong mappy_user_available_gbp_locations
mappy:posts:writeTrên + mappy_business_accounts.permission_level của GBP đó cho phép write
mappy:reviews:writeis_enabled = 1 + location đó dùng được
mappy:ranking:readsearch_ranking_enabled = 1
mappy:insight:readis_enabled = 1
mappy:keyword:readsearch_ranking_enabled = 1
mappy:report:readis_enabled = 1 + (cho MAIN_USER) flag quyền report
mappy:report:writeTrê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 authmappy_users.id (hoặc admins.id) đã đồng ý OAuth và là chủ token
User đối tượngmappy_users.id mà tool thao tác (thường giống user auth)
Cửa hàng đối tượngmappy_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_id

Ngoại lệ tên bảng

  • mappy_group_locationdạng số ít (không phải số nhiều)
  • admins không có prefix mappy_

Đã 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:

sql
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

python
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)

PhaseCá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 2Chỉ admin is_supervisor=1 mới được thay mặt khi chỉ định user_id trong input tool
Phase 3Admin 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

TokenHạnRotateLưu trữ
authorization_code60 giâyTiêu thụ 1 lầnRedis (chỉ memory)
access_token60 phútRefresh sinh mớiChỉ client (server chỉ giữ jti)
refresh_token30 ngày (rolling)Refresh rotate (cũ revoke)DB
id_token (tương lai)60 phútChỉ 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

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 Chiến lược revoke

Sự kiệnHành động
User hủy đăng ký MappyRevoke 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/revokeRevoke token đó
Đổi passwordTươ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
PKCEBắt buộc code_challenge_method=S256, cấm plain
stateChống CSRF, bắt buộc
redirect_uriKhớp tuyệt đối (cấm partial match)
Tái sử dụng codeTiêu thụ 1 lần, khi phát hiện reuse revoke toàn bộ token liên quan
Refresh token rotateBắt buộc, khi phát hiện thì revoke cả family
Phát hiện rò rỉ tokenGiám sát kết hợp jti + IP
Bắt buộc HTTPSKhông nhận HTTP, bật HSTS
Cookie secureSecure, HttpOnly, SameSite=Lax

9.2 Whitelist redirect_uri

Verify redirect_uri chính thức của Claude / ChatGPT khi đăng ký client:

ClientPattern redirect_uri đã biết
Claude Desktophttps://claude.ai/api/mcp/auth_callback
Claude Codehttp://localhost:<port>/callback (động)
ChatGPT Connectorshttps://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)

codeHTTPVí dụ
invalid_request400Thiếu tham số bắt buộc
invalid_client401client_id sai
invalid_grant400code hết hạn・code_verifier không khớp
unauthorized_client400Client không được phép dùng grant_type đó
unsupported_grant_type400grant_type không support
invalid_scope400scope không biết
access_denied403User từ chối đồng ý
server_error500Lỗi nội bộ server

10.2 Lỗi khi MCP request (JSON-RPC)

codeNghĩa
-32000Thiếu scope
-32001Lỗi tenancy (ngoài phạm vi)
-32002Flag tính năng tắt (user đối tượng tắt tính năng đó)
-32003Rate limit
-32602Schema input sai
-32603Lỗi nội bộ

11. Điểm tranh luận tại review (chưa chốt)

#ĐiểmPhương án
1Hạn access_token60 phút (chuẩn) / 30 phút (chặt)
2Thời gian rolling refresh_token30 ngày / 14 ngày
3Thuật toán ký JWTHS256 (key chung) / RS256 (key công khai)
4Có 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 ý scopeTheo scope / theo nhóm
6Vị trí màn hình hủy liên kếtThêm vào màn admin Mappy
7Liên kết với Web session hiện cóCó nên dùng lại Web session ở endpoint authorize OAuth không
8Cách lưu client_secretbcrypt hash trong DB / mã hóa KMS

12. Tài liệu liên quan