# Agent gọi API thay người dùng: dùng ủy quyền, đừng dùng service account chung

> Lối tắt "cấp cho agent một tài khoản admin" giúp bản demo chạy ngay trong chiều nay, nhưng khi có sự cố, nó không trả lời được câu hỏi đầu tiên mà đội bảo mật sẽ hỏi.

Bản gốc: https://fdetimes.net/vi/bach-khoa/agent-thay-mat-nguoi-dung-oauth-token-exchange/

Tuần thứ hai ở chỗ khách hàng, agent hỗ trợ nội bộ của bạn đã đọc được ticket và cập nhật được đơn hàng. Để kịp demo hôm thứ Sáu, một kỹ sư bên khách đề xuất cấp cho agent một service account có quyền admin trên hệ thống ticket.

Đề xuất này nghe rất hợp lý. Nhưng chính ở chỗ này, nhiều FDE nhận về một khoản nợ bảo mật mà sau này rất khó trả.

Vấn đề nằm ở log. Ba tháng sau, nếu một ticket bị đóng nhầm hoặc dữ liệu khách hàng bị đọc trái phép, đội bảo mật sẽ hỏi ai đã làm việc đó. Với một credential dùng chung, bạn không trả lời được.

Auth0 mô tả đúng tình huống này khi bàn về API key tĩnh dùng chung cho agent: bạn không có cách nào biết người dùng nào, hay hành động nào của agent, đã dùng đến key.

Theo Auth0, thiếu dấu vết kiểm toán như vậy là điều không thể chấp nhận về mặt bảo mật, tuân thủ và xử lý sự cố. Nếu muốn agent của mình qua được vòng review bảo mật ở một doanh nghiệp lớn, bạn cần nắm chắc kỹ năng trong bài này.

## Agent nên trở thành người dùng hay đi bên cạnh họ?

RFC 8693 định nghĩa OAuth 2.0 Token Exchange, giao thức dùng để xin token bảo mật từ authorization server. Giao thức này hỗ trợ cả impersonation (mạo danh) lẫn delegation (ủy quyền). Hai kiểu nghe na ná nhau nhưng để lại log rất khác nhau.

Về impersonation, RFC viết rằng khi A mạo danh B thì trong phạm vi quyền của token, A chính là B. API phía sau chỉ thấy B, còn dấu vết của bên thực sự hành động thì biến mất. Với delegation, A vẫn giữ danh tính riêng, tách khỏi B. Một agent hành động thay người dùng nên chọn kiểu thứ hai.

Công cụ để ghi lại delegation là claim `act` (actor) trong JWT. RFC 8693 tạo ra claim này để ghi rõ rằng token được cấp theo kiểu ủy quyền, và bên đang hành động là ai.

**Điểm mấu chốt:** Agent nên hành động thay người dùng, nhưng không bao giờ được biến thành người dùng.

## Một luồng hoàn chỉnh, từng bước

Thử hình dung một nhân viên hỗ trợ tên Lan nhờ agent tóm tắt các ticket đang mở. Ứng dụng chat gửi cho agent token của Lan, và audience của token này là chính agent. Agent không cầm token đó đi gọi hệ thống ticket. Nó gửi request đổi token tới authorization server, với `grant_type` là giá trị riêng do RFC 8693 quy định:

```http
POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<token Lan gửi cho agent>
&actor_token=<token định danh của chính agent>
&scope=tickets.read
```

Nếu request được chấp nhận, payload của token mới sẽ trông gần giống thế này. Lan vẫn là chủ thể, agent nằm trong `act`, và scope chỉ có quyền đọc:

```json
{
  "sub": "lan@khachhang.example",
  "scope": "tickets.read",
  "act": { "sub": "support-agent" }
}
```

Nhờ vậy, mỗi dòng log của hệ thống ticket trả lời được hai câu hỏi: ai yêu cầu và ai thực hiện. Đây chính là thứ một service account dùng chung không làm được.

## On-behalf-of: ba ràng buộc FDE nên thuộc lòng

Nếu khách hàng dùng Microsoft Entra, phiên bản cụ thể của ý tưởng này là luồng On-Behalf-Of (OBO). Microsoft mô tả OBO chính là delegation trong OAuth, nhằm chuyển danh tính và quyền của người dùng qua cả chuỗi request. Tài liệu của họ nêu ba ràng buộc mà FDE nên nhớ.

Ràng buộc đầu tiên: OBO chỉ dùng delegated scope, không dùng application role. Vì vậy tầng giữa không thể trao cho người dùng nhiều quyền hơn những gì họ vốn có. Nếu Lan không được xóa ticket thì agent làm việc thay Lan cũng không xóa được, kể cả khi bị prompt injection dụ làm vậy.

Ràng buộc thứ hai: một ứng dụng không được đem token cấp cho ứng dụng khác đi đổi. Audience của token đầu vào phải là chính agent. Token cấp cho một API khác thì phải bị từ chối, không được chuyển tiếp.

Ràng buộc thứ ba là cái hay bị bỏ qua nhất. Khi API phía sau đòi Conditional Access, chẳng hạn MFA, tầng giữa không tự đáp ứng được. Nó phải trả lỗi về client để client đưa người dùng qua bước xác thực.

Đây cũng là lý do rõ nhất vì sao danh tính của một service không thay được người dùng: service không có điện thoại để bấm xác nhận MFA.

## Ba lỗi hay gặp ở chỗ khách hàng

Lỗi đầu tiên là token passthrough: nhận token của client rồi chuyển nguyên xuống API phía sau. Hướng dẫn bảo mật của Model Context Protocol gọi đây là anti-pattern, và đặc tả authorization cấm hẳn cách làm này.

Lý do là log của API phía sau sẽ ghi request như thể nó đến từ một nguồn khác, mang một danh tính khác, chứ không phải từ server đã thực sự chuyển tiếp nó.

Lỗi thứ hai là xin sẵn mọi scope ngay từ đầu "cho đỡ phải hỏi lại". Tài liệu MCP cảnh báo rằng scope thiết kế kém khiến thiệt hại lớn hơn khi token bị lộ, làm người dùng khó chịu hơn và làm dấu vết kiểm toán mờ đi.

Với tác vụ tóm tắt, bạn chỉ nên xin `tickets.read`, và chỉ xin quyền ghi khi người dùng thật sự yêu cầu cập nhật.

Lỗi thứ ba là để agent tự giữ refresh token của từng nhà cung cấp. Mẫu Token Vault của Auth0 giải quyết chỗ này: agent chỉ giữ token Auth0 và đổi chúng lấy token của nhà cung cấp khi cần, chứ không bao giờ chạm vào refresh token của nhà cung cấp.

Nếu identity provider của khách hàng đã hỗ trợ cách làm tương tự, hãy dùng nó thay vì tự viết một kho lưu token.

## Tự làm ở dự án tiếp theo

Bắt đầu bằng việc vẽ chuỗi gọi: người dùng, client, agent, rồi từng API phía sau. Trên mỗi mũi tên, ghi rõ token nào đang đi qua và audience của nó là gì. Bản vẽ này thường cho thấy ngay chỗ có người định chuyển tiếp token hoặc dùng chung một key.

Tiếp theo, ở agent, hãy kiểm tra audience của mọi token đầu vào trước khi làm bất cứ việc gì khác. Đổi token bằng token exchange hoặc OBO, xin scope hẹp nhất đủ cho tác vụ, và trả nguyên lỗi Conditional Access về client thay vì nuốt lỗi rồi thử lại bằng một credential khác.

Cuối cùng, cùng đội bảo mật của khách đọc một dòng log thật ở API phía sau, xem nó có cho thấy cả người dùng lẫn agent hay không.

Về nghề nghiệp, đây là kỹ năng rất dễ chứng minh. Khi đọc JD của FDE, hãy để ý xem công ty có nhắc đến OAuth, identity hay tích hợp với hệ thống doanh nghiệp không. Nếu có, hãy đưa kinh nghiệm này lên đầu.

Trong CV, đừng chỉ ghi "có kinh nghiệm OAuth". Hãy mô tả việc bạn đã thiết kế luồng ủy quyền giúp mỗi lệnh gọi API của agent truy được về đúng người dùng.

Một câu như vậy cho người phỏng vấn thấy bạn từng làm ra hệ thống có thể qua được một vòng review bảo mật thật.

Bài tập tuần này: lấy một agent bạn đã viết, giải mã token nó dùng để gọi API phía sau, rồi tự hỏi nếu ngày mai có sự cố, token này có cho bạn biết ai đã yêu cầu agent hành động không. Nếu câu trả lời là không, bạn đã biết việc cần sửa trước buổi demo tiếp theo.

**Thử ngay tuần này:**

- Chọn một agent hoặc tích hợp bạn đang làm, liệt kê mọi credential nó dùng và đánh dấu những cái dùng chung cho nhiều người dùng.
- Giải mã một access token mà agent nhận được và xem audience có đúng là agent không. Sau đó viết thử một request token exchange tới môi trường test của nhà cung cấp danh tính.
- Thêm vào CV hoặc portfolio một dòng mô tả cách bạn thiết kế để mỗi lệnh gọi API của agent đều truy ngược được về người dùng.

## Nguồn

- [RFC 8693 - OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

- [RFC 8693 - OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693.html)

- [Microsoft identity platform and OAuth2.0 On-Behalf-Of flow - Microsoft identity platform | Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow)

- [Security Best Practices - Model Context Protocol](https://modelcontextprotocol.io/specification/draft/basic/security_best_practices)

- [API Keys and AI Agents: Four Common Risks](https://auth0.com/blog/api-key-security-for-ai-agents/)

- [Auth0 Token Vault: Secure Token Exchange for AI Agents](https://auth0.com/blog/auth0-token-vault-secure-token-exchange-for-ai-agents/)
