Skip to content

mappy_mcp_oauth_* (DB OAuth 2.1 Authz Server cho MCP)

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ể)
Design liên quanAuth & 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âmNội dung
Tách JWT + Opaqueaccess_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_idTheo dõi chain rotate. Khi phát hiện trộm thì revoke toàn family
Xác thực clientPublic client (Claude/ChatGPT) chỉ PKCE, secret là optional
Dynamic client registrationSupport 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ềnChỉ 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)

NoTên cột (logical)Tên cột (physical)KiểuNULLKeyMô tả
1IDidbigint unsignedNOPKAuto increment
2Client IDclient_idvarchar(64)NOUKIdentifier có thể public (UUIDv4 v.v.)
3Hash client secretclient_secret_hashvarchar(255)YES-Hash bcrypt. Public client = NULL
4Tên clientclient_namevarchar(128)NO-"Claude Desktop", "ChatGPT Connector" v.v.
5Client URIclient_urivarchar(255)YES-URL của dịch vụ client
6Logo URIlogo_urivarchar(255)YES-Logo hiển thị trên màn đồng ý
7Policy URIpolicy_urivarchar(255)YES-Privacy policy
8TOS URItos_urivarchar(255)YES-Điều khoản sử dụng
9Danh sách redirect URIredirect_urisjsonNO-Mảng callback URL được phép
10grant_types được phépgrant_typesjsonNO-["authorization_code", "refresh_token"]
11Scope được phépscopesjsonNO-Mảng scope mà client này có thể yêu cầu
12Phương thức xác thực tokentoken_endpoint_auth_methodvarchar(64)NO-client_secret_basic / client_secret_post / none
13Loại clientclient_typevarchar(16)NO-confidential / public
14Cách đăng kýregistration_methodvarchar(16)NO-manual / dynamic
15Registration access tokenregistration_access_token_hashvarchar(255)YES-Cho RFC 7592, phát khi đăng ký động
16Flag hoạt độngis_enabledtinyint(1)NO-1: bật / 0: dừng
17Mức độ tin cậytrust_leveltinyintNO-0: untrusted / 1: trusted (SDK chính thức v.v.)
18Thời điểm dùng cuốilast_used_attimestampYES-Thời điểm phát token cuối
19Created atcreated_attimestampNO-Thời điểm đăng ký
20Updated atupdated_attimestampNO-Thời điểm update cuối
21Deleted atdeleted_attimestampYES-Logical delete

Index

TênCột
uk_client_idclient_id UNIQUE
idx_clients_enabled(is_enabled, created_at)
idx_clients_trusttrust_level

Dữ liệu khởi tạo (đăng ký thủ công)

Client chính thức đăng ký trước:

client_nameclient_typeredirect_uristrust_level
Claude Desktoppublic["https://claude.ai/api/mcp/auth_callback"]1
Claude Codepublic["http://localhost:*/callback"] (cho phép pattern)1
ChatGPT Connectorpublic["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.

NoTên cộtKiểuNULLKeyMô tả
1idbigint unsignedNOPKAuto increment
2client_idvarchar(64)NOIDX, FKClient
3user_idbigint unsignedNOIDX, FKmappy_users.id
4user_kindvarchar(16)NO-mappy / admin
5admin_idbigint unsignedYESFKadmins.id (khi user_kind=admin)
6scope_grantedtextNO-Chuỗi scope cách bằng space
7granted_attimestampNO-Thời điểm đồng ý
8granted_ipvarchar(45)YES-IP khi đồng ý
9revoked_attimestampYES-Thời điểm hủy
10revoked_reasonvarchar(64)YES-user_action / admin_action / token_compromise / expired

Index

TênCộ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.

NoTên cộtKiểuNULLKeyMô tả
1idbigint unsignedNOPKAuto increment
2code_hashvarchar(255)NOUKSHA-256(code)
3client_idvarchar(64)NOIDX, FK
4user_idbigint unsignedNOFK
5user_kindvarchar(16)NO-
6scopetextNO-Scope yêu cầu
7redirect_urivarchar(255)NO-redirect_uri lúc authorize
8code_challengevarchar(255)NO-PKCE
9code_challenge_methodvarchar(16)NO-"S256"
10noncevarchar(64)YES-Cho OpenID (tương lai)
11issued_attimestampNO-
12expires_attimestampNOIDXissued_at + 60 sec
13consumed_attimestampYES-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.

NoTên cộtKiểuNULLKeyMô tả
1idbigint unsignedNOPKAuto increment
2token_hashvarchar(255)NOUKSHA-256(token)
3client_idvarchar(64)NOIDX, FK
4user_idbigint unsignedNOIDX, FK
5user_kindvarchar(16)NO-
6admin_idbigint unsignedYESFK
7scopetextNO-
8family_idvarchar(64)NOIDXIdentifier chain rotate. Cho detection trộm
9parent_token_hashvarchar(255)YES-Hash của token ngay trước (chain)
10issued_attimestampNO-
11expires_attimestampNOIDXissued_at + 30 ngày
12last_used_attimestampYES-Update khi refresh
13rotated_attimestampYES-Thời điểm revoke do rotate
14revoked_attimestampYES-Thời điểm revoke
15revoke_reasonvarchar(64)YES-user_revoke / rotation / family_compromise / expiry
16request_ipvarchar(45)YES-IP khi phát
17user_agentvarchar(255)YES-
18created_attimestampNO-

Index

TênCột
uk_token_hashtoken_hash UNIQUE
idx_refresh_client_user(client_id, user_id)
idx_refresh_familyfamily_id
idx_refresh_expiresexpires_at
idx_refresh_active(user_id, revoked_at, expires_at)

Detection trộm bằng family_id

  1. Parent token đã rotate nhưng vẫn được dùng → phát hiện token reuse
  2. Revoke toàn bộ token thuộc family_id đó
  3. 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.

NoTên cộtKiểuNULLKeyMô tả
1idbigint unsignedNOPKAuto increment
2jtivarchar(64)NOUKJWT ID
3client_idvarchar(64)NOIDX
4user_idbigint unsignedNOIDX
5revoked_attimestampNO-Thời điểm revoke
6revoke_reasonvarchar(64)NO-
7expires_attimestampNOIDXexp của JWT gốc. Qua thời điểm này có thể xóa khỏi DB
8created_attimestampNO-

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ênCột
uk_jtijti UNIQUE
idx_revocations_expiresexpires_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=1
  • registration_method='manual'

B) Đăng ký động (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"]
}

Response:

json
{
  "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ảngSố 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)

sql
-- ① 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

JobTần suấtNội dung
Xóa refresh_token hết hạnHàng ngàyPhysical delete WHERE expires_at < NOW() - INTERVAL 30 DAY
Xóa revocations đã quá expires_atHàng ngàyPhysical delete WHERE expires_at < NOW()
Cảnh báo client 30 ngày không dùngHàng thángList last_used_at < NOW() - INTERVAL 30 DAY
Tổng hợp log detection family_compromiseHàng ngàyTổng hợp revoke_reason='family_compromise', thông báo management

Quản lý key (key ký JWT)

MụcNội dung
AlgorithmRS256 (đề xuất) hoặc HS256
Độ dài keyRS256: ≥ 2048bit
Chu kỳ rotation90 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 keyPhâ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ụcPhương án
1Algorithm ký JWTRS256 (đề xuất) / HS256
2TTL refresh_token30 ngày / 14 ngày
3Lưu authorization_codeBắt buộc Redis / dùng cả DB
4TTL access_token60 phút / 30 phút
5Cách thông báo user khi family_compromiseEmail / Slack / Admin panel
6Cho phép đăng ký độngCho phép tất / Allow list giới hạn
7Chủ thể quyết định trust_levelAuto / Admin duyệt
8Hành vi khi dừng clientRevoke ngay / Token hiện có vẫn dùng đến expires_at

Tài liệu liên quan