# API key, Basic Auth, session, JWT hay OIDC: cách đọc đúng cơ chế xác thực ở hệ thống khách hàng

> Lỗi 401 đầu tiên ở site khách chưa phải điều đáng sợ nhất. Đáng sợ hơn là integration vẫn chạy trong khi bạn hiểu sai cách hệ thống xác thực.

Bản gốc: https://fdetimes.net/vi/bach-khoa/api-key-jwt-session-oidc-xac-thuc-he-thong-khach/

Tuần đầu ở site khách, bạn được giao nối một agent vào hệ thống nội bộ của họ. Kỹ sư phía khách nhắn: "Cứ gọi API thôi, auth có sẵn rồi." Request đầu tiên trả về 401, và lúc đó bạn mới thấy "có sẵn rồi" có thể là năm cơ chế hoàn toàn khác nhau.

API key, Basic Auth, session cookie, JWT và OIDC trả lời những câu hỏi khác nhau, nên khi hỏng cũng hỏng theo những kiểu khác nhau. Đọc sai cơ chế, bạn dễ viết ra integration chạy tốt trên laptop nhưng gãy khi lên production. Tệ hơn, credential có thể nằm trong log mà không ai hay.

Kỹ năng này không đòi bạn thuộc lòng spec. Nó là khả năng nhìn vào một request và trả lời được ba câu: credential nằm ở đâu, server có phải nhớ gì không, và ai bảo đảm danh tính người dùng.

## Basic Auth trông như mật khẩu bị giấu, thực ra không

Basic Auth là scheme có sẵn trong giao thức HTTP. Client ghép username và password thành chuỗi `username:password`, encode bằng base64, rồi gửi trong header theo định dạng mà tài liệu của Twilio mô tả: chữ `Authorization: Basic`, theo sau là chuỗi base64 đó.

Cách nhanh nhất để thấy vấn đề là tự làm thử với một tài khoản giả:

```bash
echo -n 'user:pass' | base64
# dXNlcjpwYXNz

echo 'dXNlcjpwYXNz' | base64 -d
# user:pass
```

Không cần key, cũng chẳng có gì để bẻ khóa, vì base64 là encoding chứ không phải encryption. Postman nói rõ credential Basic Auth không được hash và cũng không được mã hóa (encryption). Tài liệu của Swagger khuyến cáo chỉ dùng Basic Auth khi có thêm cơ chế bảo mật khác đi kèm, chẳng hạn HTTPS.

API key cùng nhóm rủi ro với Basic Auth. Postman định nghĩa API key là định danh cấp cho người dùng đã đăng ký, và nhấn mạnh rằng nó phải đi qua HTTPS mới giữ được an toàn.

Ở site khách, việc đầu tiên nên làm vì thế khá đơn giản: kiểm tra mọi endpoint nhận API key hay Basic Auth đều chạy HTTPS, kể cả các endpoint "chỉ dùng nội bộ".

## Session hay token: khác nhau ở chỗ ai phải nhớ

Hai cơ chế tiếp theo khác nhau về kiến trúc chứ không chỉ khác về định dạng. Authgear mô tả session authentication là stateful: server giữ trạng thái trong bộ nhớ, còn trình duyệt giữ cookie. JWT thì stateless, backend không phải lưu token.

Okta giải thích token-based auth theo cùng logic: xác minh danh tính một lần, cấp token, sau đó server không phải giữ bản ghi session nào. Authgear cũng lưu ý rằng session lưu trên server khó scale hơn.

Thử hình dung khách hàng có một web app nội bộ dùng session cookie, và bạn cần agent chạy trên một domain khác gọi vào backend của app đó. Cookie thường chỉ hoạt động trong một domain hoặc các subdomain của nó, nên agent của bạn khó mà "mượn" session của người dùng.

Hệ quả thực tế là đừng cố giả lập trình duyệt để lấy cookie. Hãy hỏi khách xem backend có hỗ trợ token cho các client không chạy trên trình duyệt hay không. Nếu chưa có, đó là một hạng mục cần đưa vào scope ngay từ đầu, đừng để thành một workaround về sau.

## Mở một JWT ra để thấy nó không giữ bí mật

JWT gồm ba phần ngăn cách nhau bằng dấu chấm: header, payload và signature. Bạn có thể tách phần giữa ra và decode giống hệt cách làm với Basic Auth:

```bash
TOKEN='xxxxx.yyyyy.zzzzz'
echo "$TOKEN" | cut -d. -f2 | base64 -d
# có thể phải thêm ký tự '=' ở cuối để đủ padding
```

Theo jwt.io, chữ ký dùng để xác minh rằng thông điệp không bị thay đổi trên đường truyền. Nó chứng minh tính toàn vẹn của dữ liệu chứ không che giấu dữ liệu: ai cầm token cũng đọc được payload. Vì vậy, đừng đặt bí mật vào một JWT chỉ có chữ ký mà không được mã hóa (encryption).

Chiều ngược lại cũng dễ bị bỏ qua: người nhận token phải kiểm tra nó. Tài liệu của Auth0 yêu cầu JWT nhận được phải được kiểm tra chữ ký trước khi đem ra dùng. Nếu code của bạn chỉ decode payload rồi đọc `user_id` mà không verify, bất kỳ ai cũng có thể tự viết một token và mạo danh người khác.

**Điểm mấu chốt:** Base64 là encoding, không bảo vệ gì cả. Chữ ký chứng minh dữ liệu không bị sửa, nhưng không giữ được bí mật.

## JWT không cho biết người dùng là ai, OIDC thì có

Nhiều kỹ sư thấy khách dùng JWT rồi mặc định rằng danh tính đã được giải quyết. Thực ra JWT chỉ là một định dạng token. Spec OpenID Connect Core mô tả OIDC là một lớp danh tính đơn giản chạy trên OAuth 2.0, và chính lớp này mới trả lời câu "người dùng này là ai".

Điểm phân biệt này quyết định cách bạn đặt câu hỏi trong buổi discovery. Đừng hỏi "Anh chị có dùng JWT không?". Hãy hỏi "Danh tính người dùng do hệ thống nào cấp, và agent của chúng tôi sẽ đăng nhập thay ai?".

| Cơ chế | Credential nằm ở đâu | Server có lưu trạng thái? | Rủi ro cần kiểm đầu tiên |
|---|---|---|---|
| Basic Auth | Header `Authorization: Basic` | Không | Endpoint không chạy HTTPS |
| API key | Do API quy định | Không | Key bị lộ khi không có HTTPS |
| Session | Cookie | Có | Gọi chéo domain, khó scale |
| JWT | Token do client giữ | Không | Không verify chữ ký, để bí mật trong payload |
| OIDC | Lớp danh tính trên OAuth 2.0 | Tùy triển khai | Nhầm định dạng token với danh tính |

## Quy trình năm phút trước khi viết dòng code đầu tiên

Bắt đầu bằng việc bắt một request thật từ ứng dụng của khách, qua DevTools hoặc proxy, rồi xem header Authorization và cookie. Bước này cho bạn biết credential nằm ở đâu. Tiếp theo, decode thử: nếu là Basic thì bạn sẽ thấy ngay username, nếu là JWT thì bạn sẽ thấy payload.

Sau đó hỏi khách hai câu: server có giữ session không, và ai cấp lại credential khi nó hết hạn. Cuối cùng, ghi tất cả vào một "auth map" ngắn rồi đưa đội khách xác nhận. Tài liệu này sẽ là điểm tựa cho những lần xử lý sự cố về sau.

## Những lỗi FDE hay mắc

Lỗi phổ biến nhất là coi base64 encoding như encryption, rồi log nguyên header Authorization ra file debug. Lỗi thứ hai là decode JWT để lấy thông tin người dùng mà bỏ qua bước verify chữ ký. Lỗi thứ ba là cố dùng lại session cookie cho một client chạy trên domain khác, trong khi cookie vốn gắn với một domain.

Lỗi khó thấy nhất nằm trong khâu giao tiếp: ghi "dùng JWT" vào tài liệu scope và nghĩ mọi chuyện đã rõ. Khi chưa ai trả lời được câu "danh tính do đâu cấp", phần việc khó nhất của dự án vẫn còn nằm nguyên đó.

Bài tập cho tuần này: chọn một hệ thống bạn đang tích hợp, viết auth map gồm bốn cột như bảng trên, rồi tự kiểm xem team bạn đang mắc lỗi nào trong số các lỗi kể trên.

Nếu muốn dùng bài tập này khi đi xin việc FDE, đừng mang auth map thật của khách ra ngoài; hãy làm lại trên một dự án cá nhân hoặc một bản đã ẩn danh hoàn toàn.

Lần tới có ai nói "auth có sẵn rồi", bạn sẽ biết cần hỏi tiếp câu gì.

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

- Mở DevTools trên một ứng dụng nội bộ bạn đang dùng, tìm header Authorization hoặc cookie của một request, rồi xếp nó vào đúng một trong năm cơ chế
- Lấy một JWT ở môi trường test, decode phần payload bằng base64 và ghi lại những trường không nên để lộ
- Viết một trang 'auth map' cho dự án hiện tại: mỗi hệ thống dùng cơ chế gì, credential lưu ở đâu, ai cấp lại khi credential hết hạn

## Nguồn

- [Basic Authentication (Swagger)](https://swagger.io/docs/specification/authentication/basic-authentication/)

- [Basic Authentication (Twilio)](https://www.twilio.com/docs/glossary/what-is-basic-authentication)

- [What Is API Authentication? Benefits, Methods & Best Practices | Postman](https://www.postman.com/api-platform/api-authentication/)

- [Session vs Token Authentication: Which Should You Choose?](https://www.authgear.com/post/session-vs-token-authentication)

- [JSON Web Token Introduction - jwt.io](https://jwt.io/introduction)

- [JSON Web Tokens (Auth0 Docs)](https://auth0.com/docs/secure/tokens/json-web-tokens)

- [What Is Token-Based Authentication? (Okta)](https://www.okta.com/uk/identity-101/what-is-token-based-authentication/)

- [Final: OpenID Connect Core 1.0 incorporating errata set 2](https://openid.net/specs/openid-connect-core-1_0.html)
