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ầnPhía người dùng (website)Phía quản trị viên (trang quản trị)
Trang phê duyệtGET /ezylogin/cli/authorizeGET /ezylogin/cli/authorize
Xử lý phê duyệtPOST /ezylogin/cli/authorize/approvePOST /ezylogin/cli/authorize/approve
API đổi tokenPOST /ezylogin/api/v1/cli/tokenPOST /ezylogin/api/v1/cli/me/token
Cài đặt bật/tắtezylogin_allow_user_cli_loginezylogin_allow_admin_cli_login
Nơi lưu mã uỷ quyềnMeta của người dùngMeta của quản trị viên
Token được phát hànhAccess token người dùngAccess 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ắcLỗi
client_nameKhông bắt buộc, tối đa 64 ký tựclientName: overLength
redirect_uriBắ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
stateBắt buộc, tối đa 256 ký tựstate: required / overLength
code_challengeBắt buộc, đúng 43 ký tự base64url (A-Z a-z 0-9 - _)codeChallenge: required / invalid
code_challenge_methodChỉ chấp nhận S256codeChallengeMethod: 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_uri trỏ 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ức plain của PKCE bị loại bỏ, vì với plain thì code_challenge chính là code_verifier và 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 POST thay vì tin vào lần kiểm tra trước.
Trang phê duyệt đăng nhập từ dòng lệnh của người dùng
  1. 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.
  2. Quyền mà công cụ sẽ nhận: tương đương với tài khoản đang phê duyệt.
  3. 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.
  4. Địa chỉ nhận kết quả là redirect_uri đã qua kiểm tra, để người dùng tự đối chiếu.
  5. 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_key cố định là ezyloginCliAuthorizationCode.
  • meta_value là 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_value là JSON chứa clientName, redirectUri, codeChallenge và 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ồm A-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, redirectUri và PKCE. Vì vậy mỗi mã chỉ có đúng một cơ hội: một request đổi token sai (dù sai codeVerifier) cũng làm mã mất hiệu lực, kẻ xấu không thể thử đi thử lại nhiều code_verifier với cùng một mã.
  • So sánh PKCE trong thời gian không đổi: code_challenge tí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 redirectUri hay sai PKCE đều trả về cùng một lỗi code: 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_2FA nế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-Agent là 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ột User-Agent có tên và phiên bản rõ ràng.
Phiên đăng nhập từ dòng lệnh trong danh sách các phiên người dùng
  1. 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-Agent củ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ỗi 401.

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ùngQuản trị viên
Địa chỉ gốcWebsiteTrang quản trị
Phiên đăng nhập cần cóPhiên người dùng trên websitePhiê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 độngTrạng thái người dùng là ACTIVATEDQuản trị viên tồn tại và có trạng thái ACTIVATED
Thời hạn tokenThời gian token hết hạn của người dùngThời gian token hết hạn của quản trị viên
Tuỳ biến giao diện phê duyệtPage fragment cli_approval_screen, theo ngôn ngữ đang chọnTheo 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 roCá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ộngMã 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ìnhChỉ 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ầnMã 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ạmstate đượ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ồiLỗi gộp chung code: invalid, so sánh PKCE constant-time
Bỏ qua xác thực hai lớpPhê 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ấpMỗ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.