Cách thức hoạt động của CLI login trong EzyLogin
Back to ezylogin
Updated at
1793288780000
CLI login là cơ chế cho phép một công cụ dòng lệnh, script tự động hoá hay AI agent nhận được access token của một tài khoản EzyPlatform mà không cần người dùng sao chép token bằng tay. Công cụ mở trình duyệt, người dùng đăng nhập như bình thường và bấm Phê duyệt, sau đó công cụ tự đổi kết quả nhận được thành access token. EzyLogin triển khai cơ chế này theo chuẩn OAuth 2.0 Authorization Code kết hợp PKCE (RFC 7636) với redirect về địa chỉ loopback (RFC 8252), cùng mô hình mà GitHub CLI hay Google Cloud CLI đang dùng.
Bài viết này đi sâu vào bên trong: các thành phần tham gia, mã uỷ quyền được tạo và lưu thế nào, máy chủ kiểm tra những gì trước khi cấp token, khác biệt giữa phía người dùng và phía quản trị viên, và các lớp bảo vệ giúp cơ chế này an toàn. Hướng dẫn bật tính năng và mã mẫu tích hợp nằm ở bài Hướng dẫn cho phép truy cập đến EzyPlatform cho quản trị viên và người dùng qua CLI với EzyLogin.
Kiến trúc tổng quát
CLI login không cần đăng ký client trước như OAuth 2.0 hay SSO: không có
client_id, không có client_secret. Công cụ dòng lệnh là một public client chạy trên máy người dùng nên không thể giữ bí mật lâu dài, vì vậy EzyLogin thay bí mật cố định bằng một bí mật dùng một lần (code_verifier) được tạo mới ở mỗi lần đăng nhập, và chỉ chấp nhận trả kết quả về chính máy tính đang chạy công cụ.
flowchart LR
Cli["Công cụ dòng lệnh"] --> Loopback["HTTP server tạm<br/>127.0.0.1:cổng ngẫu nhiên"]
Cli --> TrinhDuyet["Trình duyệt mặc định"]
TrinhDuyet --> TrangPheDuyet["Trang phê duyệt<br/>/ezylogin/cli/authorize"]
Cli --> ApiToken["API đổi token<br/>/ezylogin/api/v1/cli/..."]
TrangPheDuyet --> KiemTra["Bộ kiểm tra tham số<br/>và cài đặt"]
ApiToken --> KiemTra
TrangPheDuyet --> KhoMa["Kho mã uỷ quyền<br/>(meta của tài khoản)"]
ApiToken --> KhoMa
ApiToken --> KhoToken["Access token<br/>của EzyPlatform"]
ApiToken --> KhoPhien["Phiên đăng nhập<br/>của EzyLogin"]
Phần lõi của luồng (kiểm tra tham số, tạo và tiêu thụ mã uỷ quyền, so khớp PKCE, ghi phiên đăng nhập) được viết một lần trong SDK của EzyLogin. Plugin web và plugin quản trị chỉ cung cấp phần riêng của mình: đọc cài đặt nào, lưu mã vào meta của người dùng hay của quản trị viên, phát hành loại access token nào. Nhờ vậy hai phía luôn có cùng quy tắc kiểm tra và cùng mức bảo mật.
| Thành phần | Phía người dùng (website) | Phía quản trị viên (trang quản trị) |
|---|---|---|
| Trang phê duyệt | GET /ezylogin/cli/authorize | GET /ezylogin/cli/authorize |
| Xử lý phê duyệt | POST /ezylogin/cli/authorize/approve | POST /ezylogin/cli/authorize/approve |
| API đổi token | POST /ezylogin/api/v1/cli/token | POST /ezylogin/api/v1/cli/me/token |
| Cài đặt bật/tắt | ezylogin_allow_user_cli_login | ezylogin_allow_admin_cli_login |
| Nơi lưu mã uỷ quyền | Meta của người dùng | Meta của quản trị viên |
| Token được phát hành | Access token người dùng | Access token quản trị viên |
Toàn bộ luồng đăng nhập
Một lần đăng nhập đi qua ba chặng: công cụ chuẩn bị và mở trình duyệt, người dùng phê duyệt trên trình duyệt, rồi công cụ đổi mã uỷ quyền lấy access token. Sơ đồ dưới đây mô tả đầy đủ các bước, các phần sau sẽ giải thích chi tiết từng chặng.
sequenceDiagram
autonumber
participant Cli as Công cụ dòng lệnh
participant Browser as Trình duyệt
participant EzyLogin as EzyLogin
participant Kho as Kho dữ liệu
Cli->>Cli: Tạo code_verifier, code_challenge, state
Cli->>Cli: Mở HTTP server tạm trên 127.0.0.1
Cli->>Browser: Mở /ezylogin/cli/authorize?...
Browser->>EzyLogin: GET /ezylogin/cli/authorize
alt Chưa đăng nhập
EzyLogin->>Browser: Yêu cầu đăng nhập (kể cả xác thực hai lớp)
Browser->>EzyLogin: Đăng nhập rồi quay lại trang phê duyệt
end
EzyLogin->>EzyLogin: Kiểm tra cài đặt và tham số
EzyLogin->>Browser: Hiển thị trang phê duyệt
Browser->>EzyLogin: POST /ezylogin/cli/authorize/approve
EzyLogin->>EzyLogin: Kiểm tra lại cài đặt và tham số
EzyLogin->>Kho: Lưu SHA-256 của mã uỷ quyền kèm code_challenge, redirect_uri, hạn 2 phút
EzyLogin->>Browser: Redirect tới redirect_uri?code=...&state=...
Browser->>Cli: GET http://127.0.0.1:cổng/callback?code=...&state=...
Cli->>Cli: So khớp state
Cli->>EzyLogin: POST API đổi token {code, codeVerifier, redirectUri}
EzyLogin->>Kho: Lấy và xoá mã uỷ quyền
EzyLogin->>EzyLogin: Kiểm tra hạn, redirectUri, PKCE, trạng thái tài khoản
EzyLogin->>Kho: Tạo access token và phiên đăng nhập
EzyLogin->>Cli: {accessToken, accessTokenExpiredAt, accessTokenStatus}
Điểm quan trọng của thiết kế này là mã uỷ quyền đi qua trình duyệt (nơi có thể bị lộ qua lịch sử hay tiện ích mở rộng), còn access token thì không bao giờ đi qua trình duyệt: token chỉ được trả về trong response của request mà công cụ tự gửi tới API đổi token.
Chặng phê duyệt trên trình duyệt
Chặng này diễn ra hoàn toàn trên trình duyệt và kết thúc khi EzyLogin chuyển trình duyệt về cổng tạm của công cụ kèm mã uỷ quyền. EzyLogin xử lý hai request:
GET để hiển thị trang phê duyệt và POST khi người dùng bấm Phê duyệt.Bắt buộc có phiên đăng nhập
Cả hai request đều yêu cầu một phiên đăng nhập hợp lệ trên trình duyệt. Nếu người dùng chưa đăng nhập, EzyPlatform đưa người dùng tới màn đăng nhập trước, bao gồm cả bước xác thực hai lớp nếu tài khoản đã bật, rồi mới tới trang phê duyệt. Phía quản trị viên hoạt động tương tự với phiên đăng nhập của trang quản trị.
Vì vậy CLI login không tạo ra một con đường đăng nhập mới: công cụ dòng lệnh không bao giờ nhìn thấy mật khẩu hay mã xác thực hai lớp, mọi bước xác thực vẫn diễn ra trên giao diện quen thuộc của website hoặc trang quản trị. Tài khoản được cấp quyền chính là tài khoản đang đăng nhập trên trình duyệt tại thời điểm bấm Phê duyệt.
Kiểm tra cài đặt và tham số
Trước khi hiển thị trang phê duyệt, và một lần nữa khi nhận request phê duyệt, EzyLogin kiểm tra cài đặt cho phép CLI login của phía tương ứng. Cả hai cài đặt mặc định là tắt; khi đang tắt, EzyLogin trả về
403 {"cliLogin":"notAllowed"}.Sau đó các tham số được kiểm tra. Mọi lỗi được gom lại và trả về cùng lúc trong một response
400, mỗi tham số sai có một khoá riêng:| Tham số | Quy tắc | Lỗi |
|---|---|---|
client_name | Không bắt buộc, tối đa 64 ký tự | clientName: overLength |
redirect_uri | Bắt buộc, tối đa 256 ký tự, scheme http, host là 127.0.0.1, localhost hoặc [::1], có cổng, không có user:pass@, không có # | redirectUri: invalid |
state | Bắt buộc, tối đa 256 ký tự | state: required / overLength |
code_challenge | Bắt buộc, đúng 43 ký tự base64url (A-Z a-z 0-9 - _) | codeChallenge: required / invalid |
code_challenge_method | Chỉ chấp nhận S256 | codeChallengeMethod: invalid |
Một vài quyết định thiết kế đáng chú ý:
- Chỉ chấp nhận loopback: danh sách host được cố định trong mã nguồn, không phụ thuộc cấu hình. Kẻ xấu không thể gửi cho nạn nhân một đường dẫn phê duyệt có
redirect_uritrỏ về máy chủ của mình, vì EzyLogin sẽ từ chối ngay từ bước hiển thị. - Chỉ chấp nhận
S256: phương thứcplaincủa PKCE bị loại bỏ, vì vớiplainthìcode_challengechính làcode_verifiervà việc lộ đường dẫn phê duyệt đồng nghĩa với lộ bí mật. -
code_challengeđúng 43 ký tự: đó là độ dài của SHA-256 (32 byte) khi mã hoá base64url không có=, nên mọi giá trị khác chắc chắn không phải là một challenge hợp lệ. - Kiểm tra lại khi phê duyệt: các tham số được gửi lại qua form ẩn, nên EzyLogin kiểm tra lại toàn bộ ở request
POSTthay vì tin vào lần kiểm tra trước.
- Tên công cụ lấy từ
client_name, chỉ dùng để hiển thị và được lưu kèm mã uỷ quyền. - Quyền mà công cụ sẽ nhận: tương đương với tài khoản đang phê duyệt.
- Cảnh báo an toàn nhắc người dùng chỉ phê duyệt khi chính mình vừa chạy lệnh đăng nhập.
- Địa chỉ nhận kết quả là
redirect_uriđã qua kiểm tra, để người dùng tự đối chiếu. - Huỷ chỉ quay lại trang trước trên trình duyệt, không tạo mã uỷ quyền nào; Phê duyệt gửi request
POSTđể tạo mã.
Tạo và lưu mã uỷ quyền
Khi người dùng bấm Phê duyệt, EzyLogin tạo một mã uỷ quyền mới cho tài khoản đang đăng nhập. Mã có cùng định dạng với access token của EzyPlatform: phần đầu là định danh của tài khoản được mã hoá AES bằng khoá của máy chủ, phần sau là một chuỗi ngẫu nhiên an toàn. Nhờ phần đầu này, khi nhận lại mã ở API đổi token, EzyLogin giải mã được tài khoản sở hữu mà không cần tra cứu toàn bảng; mã bị sửa hay giả mạo sẽ không giải mã được và bị từ chối ngay.
Mã uỷ quyền không được lưu ở dạng gốc. EzyLogin lưu vào meta của tài khoản (meta của người dùng hoặc của quản trị viên) một bản ghi:
flowchart LR
Ma["Mã uỷ quyền<br/>(chỉ gửi cho trình duyệt)"] --> Bam["SHA-256"]
Bam --> Khoa["meta_value<br/>= hash của mã"]
Meta["meta_key<br/>= ezyloginCliAuthorizationCode"] --- Khoa
Khoa --- NoiDung["meta_text_value<br/>= JSON clientName, redirectUri,<br/>codeChallenge, expiredAt"]
-
meta_keycố định làezyloginCliAuthorizationCode. -
meta_valuelà SHA-256 của mã uỷ quyền, dùng làm khoá tra cứu duy nhất. Kể cả khi cơ sở dữ liệu bị lộ, kẻ xấu cũng không thể khôi phục lại mã từ giá trị này. -
meta_text_valuelà JSON chứaclientName,redirectUri,codeChallengevàexpiredAt(thời điểm tạo cộng 2 phút).
Cuối cùng EzyLogin chuyển trình duyệt tới
redirect_uri với hai tham số truy vấn được thêm vào: code là mã uỷ quyền và state là giá trị công cụ đã gửi lên. EzyLogin không tự kiểm tra state; giá trị này được trả nguyên vẹn để công cụ đối chiếu với giá trị mình đã tạo, nhằm chống việc một mã uỷ quyền của lần đăng nhập khác bị "tiêm" vào cổng tạm.
Chặng đổi mã uỷ quyền lấy access token
Khi đã có mã uỷ quyền, công cụ gửi request
POST tới API đổi token với code, codeVerifier và redirectUri. Đây là request duy nhất trong luồng không cần phiên đăng nhập: với phía người dùng, API của website vốn không bắt buộc đăng nhập; với phía quản trị viên, /ezylogin/api/v1/cli/me/token được đăng ký vào danh sách URI được bỏ qua bước xác thực quản trị viên, vì công cụ chưa có token nào để gửi kèm. Thay vào đó, chính mã uỷ quyền và code_verifier đóng vai trò bằng chứng xác thực.Thứ tự kiểm tra
EzyLogin kiểm tra request theo thứ tự dưới đây và dừng ở bước đầu tiên thất bại:
flowchart TD
BatDau["POST API đổi token"] --> CaiDat{"Cài đặt CLI login<br/>đang bật?"}
CaiDat -- "Không" --> Loi403["403 cliLogin: notAllowed"]
CaiDat -- "Có" --> DinhDang{"code, codeVerifier, redirectUri<br/>đúng định dạng?"}
DinhDang -- "Không" --> Loi400a["400 theo từng trường"]
DinhDang -- "Có" --> LayMa["Giải mã chủ sở hữu từ code,<br/>tìm theo SHA-256 của code<br/>và XOÁ bản ghi ngay"]
LayMa --> TonTai{"Tìm thấy?"}
TonTai -- "Không" --> Loi400b["400 code: invalid"]
TonTai -- "Có" --> HetHan{"Còn hạn 2 phút?"}
HetHan -- "Không" --> Loi400b
HetHan -- "Có" --> Uri{"redirectUri trùng khớp?"}
Uri -- "Không" --> Loi400b
Uri -- "Có" --> Pkce{"BASE64URL(SHA256(codeVerifier))<br/>= codeChallenge?"}
Pkce -- "Không" --> Loi400b
Pkce -- "Có" --> Owner{"Tài khoản còn<br/>hoạt động?"}
Owner -- "Không" --> Loi400c["400 owner: invalid"]
Owner -- "Có" --> CapToken["Tạo access token và phiên đăng nhập"]
Các điểm đáng chú ý trong bước này:
-
code_verifierđược kiểm tra định dạng trước: dài từ 43 đến 128 ký tự và chỉ gồmA-Z a-z 0-9 - . _ ~, đúng như RFC 7636 quy định. - Mã bị xoá trước khi kiểm tra: EzyLogin lấy và xoá bản ghi trong cùng một bước, sau đó mới so hạn,
redirectUrivà PKCE. Vì vậy mỗi mã chỉ có đúng một cơ hội: một request đổi token sai (dù saicodeVerifier) cũng làm mã mất hiệu lực, kẻ xấu không thể thử đi thử lại nhiềucode_verifiervới cùng một mã. - So sánh PKCE trong thời gian không đổi:
code_challengetính lại từcode_verifierđược so với giá trị đã lưu bằng phép so sánh constant-time, tránh rò rỉ thông tin qua thời gian phản hồi. - Lỗi gộp chung: mã không tồn tại, hết hạn, đã dùng, sai
redirectUrihay sai PKCE đều trả về cùng một lỗicode: invalid, để không tiết lộ cho bên gọi biết điều kiện nào đã sai. - Trạng thái tài khoản được kiểm tra lúc đổi token: nếu tài khoản bị khoá trong khoảng thời gian giữa lúc phê duyệt và lúc đổi token, EzyLogin từ chối với
owner: invalid.
Phát hành access token
Khi mọi kiểm tra đều qua, EzyLogin phát hành một access token mới hoàn toàn cho tài khoản sở hữu mã, không dùng lại token của phiên trình duyệt:
- Loại token là access token thông thường của EzyPlatform (người dùng hoặc quản trị viên), nên mọi API, bộ lọc xác thực và phân quyền hiện có đều hoạt động với token này mà không cần thay đổi gì. Token của quản trị viên mang đúng các vai trò và quyền của tài khoản đó.
- Thời hạn lấy theo cài đặt Thời gian token hết hạn của EzyPlatform, dành cho người dùng hoặc dành cho quản trị viên.
- Trạng thái là
ACTIVATED_2FAnếu tài khoản đã bật xác thực hai lớp, ngược lại làACTIVATED. Token được đánh dấu đã qua xác thực hai lớp vì để phê duyệt, người dùng bắt buộc phải có phiên đăng nhập trên trình duyệt, mà phiên đó đã đi qua bước xác thực hai lớp.
Response trả về cho công cụ chỉ gồm ba trường
accessToken, accessTokenExpiredAt (mili giây) và accessTokenStatus. CLI login không phát hành refresh token: khi token hết hạn, công cụ chạy lại luồng đăng nhập.
Ghi phiên đăng nhập
Cùng lúc với việc phát hành token, EzyLogin tạo một phiên đăng nhập gắn với token đó, giống như khi đăng nhập trên trình duyệt. Phiên lưu loại chủ sở hữu (người dùng hoặc quản trị viên), định danh tài khoản, token, địa chỉ IP và
User-Agent của request đổi token:- Địa chỉ IP được lấy theo cấu hình proxy của EzyPlatform (đọc từ header của reverse proxy khi máy chủ chạy sau proxy) và cắt tối đa 64 ký tự.
-
User-Agentlà header công cụ gửi lên, cắt tối đa 1200 ký tự. Đây là thông tin giúp người dùng và quản trị viên nhận ra phiên nào là của công cụ nào, vì vậy công cụ nên gửi mộtUser-Agentcó tên và phiên bản rõ ràng.
- Phiên đăng nhập từ dòng lệnh xuất hiện trong EzyLogin → Các phiên người dùng như mọi phiên khác, với cột Tác nhân người dùng là
User-Agentcủa công cụ. Đăng xuất phiên này sẽ thu hồi token tương ứng, mọi request sau đó dùng token này nhận lỗi401.
Khác biệt giữa phía người dùng và phía quản trị viên
Hai phía dùng chung toàn bộ quy tắc ở các phần trên, chỉ khác nhau ở những điểm gắn với loại tài khoản. Bảng dưới đây tổng hợp các khác biệt đó:
| Người dùng | Quản trị viên | |
|---|---|---|
| Địa chỉ gốc | Website | Trang quản trị |
| Phiên đăng nhập cần có | Phiên người dùng trên website | Phiên quản trị viên trên trang quản trị |
| API đổi token | /ezylogin/api/v1/cli/token | /ezylogin/api/v1/cli/me/token, được bỏ qua bước xác thực quản trị viên |
| Kiểm tra tài khoản hoạt động | Trạng thái người dùng là ACTIVATED | Quản trị viên tồn tại và có trạng thái ACTIVATED |
| Thời hạn token | Thời gian token hết hạn của người dùng | Thời gian token hết hạn của quản trị viên |
| Tuỳ biến giao diện phê duyệt | Page fragment cli_approval_screen, theo ngôn ngữ đang chọn | Theo giao diện trang quản trị |
Hai cài đặt bật/tắt độc lập với nhau, nên quản trị viên có thể chỉ mở CLI login cho người dùng mà vẫn khoá hoàn toàn phía trang quản trị. Tắt cài đặt chỉ chặn các lần phê duyệt và đổi token mới, các token đã phát hành vẫn có hiệu lực tới khi hết hạn hoặc bị đăng xuất.
Các lớp bảo vệ
Mỗi bước trong luồng được thiết kế để chặn một kiểu tấn công cụ thể. Bảng dưới đây tổng hợp các rủi ro thường gặp với đăng nhập từ dòng lệnh và cách EzyLogin xử lý:
| Rủi ro | Cách EzyLogin xử lý |
|---|---|
| Mã uỷ quyền bị lộ qua lịch sử trình duyệt, log hay tiện ích mở rộng | Mã vô dụng nếu không có code_verifier, chỉ dùng được một lần và hết hạn sau 2 phút |
Kẻ xấu gửi đường dẫn phê duyệt có redirect_uri trỏ về máy chủ của mình | Chỉ chấp nhận http tới 127.0.0.1, localhost, [::1] có cổng, kiểm tra ở cả bước hiển thị lẫn bước phê duyệt |
Đoán code_verifier bằng cách thử nhiều lần | Mã bị xoá ngay ở lần đổi đầu tiên, dù thành công hay thất bại |
| Đọc trộm cơ sở dữ liệu để lấy mã đang chờ | Chỉ lưu SHA-256 của mã, không lưu mã gốc |
| Giả mạo mã để trỏ sang tài khoản khác | Định danh tài khoản trong mã được mã hoá AES bằng khoá của máy chủ, tra cứu theo đúng tài khoản giải mã được |
| Mã của lần đăng nhập khác bị tiêm vào cổng tạm | state được trả nguyên vẹn để công cụ đối chiếu |
| Dò thông tin qua thông báo lỗi hoặc thời gian phản hồi | Lỗi gộp chung code: invalid, so sánh PKCE constant-time |
| Bỏ qua xác thực hai lớp | Phê duyệt bắt buộc phải có phiên trình duyệt đã qua mọi bước đăng nhập, kể cả xác thực hai lớp |
| Token bị lộ sau khi đã cấp | Mỗi token là một phiên riêng, có thể đăng xuất bất cứ lúc nào từ danh sách phiên |
Ngoài các lớp bảo vệ phía máy chủ, phần còn lại phụ thuộc vào công cụ dòng lệnh: tạo
code_verifier và state bằng bộ sinh số ngẫu nhiên an toàn, chỉ lắng nghe trên 127.0.0.1, đóng HTTP server tạm ngay sau khi nhận kết quả, và lưu access token ở nơi chỉ chủ sở hữu đọc được.