Tino Developers
Xem .md

Đăng nhập bằng Tino — OAuth 2.0 và OpenID Connect

Ứng dụng của bạn đăng nhập khách bằng tài khoản Tino và gọi API Tino thay khách qua OAuth 2.0 Authorization Code + PKCE (S256), kèm OpenID Connect. Issuer là https://api.tino.vn; trang đăng nhập và đồng ý là https://auth.tino.vn/oauth/consent.

Cập nhật: 2026-10-27. Nguồn sự thật cho endpoint, quyền và claim: tài liệu discovery https://api.tino.vn/.well-known/openid-configuration.

Tổng quan

Mọi endpoint đều có trong tài liệu discovery: đọc https://api.tino.vn/.well-known/openid-configuration (hoặc /.well-known/oauth-authorization-server, cùng nội dung) thay vì ghi cứng địa chỉ.

MụcGiá trị
Issuer (iss)https://api.tino.vn
Trang đăng nhập + đồng ý (authorization_endpoint)https://auth.tino.vn/oauth/consent
Đổi code, làm mới token (token_endpoint)POST https://api.tino.vn/oauth/token
Thu hồi token (revocation_endpoint)POST https://api.tino.vn/oauth/revoke
UserInfo (userinfo_endpoint)GET https://api.tino.vn/oauth/userinfo
Khoá công khai (jwks_uri)GET https://api.tino.vn/oauth/jwks
Gọi API thay khách (resource_gateway)https://api.tino.vn/v1/<route>
Đăng ký ứng dụng (registration_endpoint)POST https://api.tino.vn/oauth/register — client công khai tự đăng ký, xem Đăng nhập từ CLI
Xin mã thiết bị (device_authorization_endpoint)POST https://api.tino.vn/oauth/device_authorization — CLI, xem Đăng nhập từ CLI
Luồng hỗ trợresponse_type=code; grant_type = authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:device_code
PKCEbắt buộc, chỉ S256
Xác thực client ở token endpointclient_secret_post (client có secret) hoặc none (client công khai + PKCE); không hỗ trợ HTTP Basic
Ký ID tokenRS256

Không có implicit flow, hybrid flow, client credentials hay password grant.

Luồng đăng nhập

Khách đồng ý ở auth.tino.vn; ứng dụng đổi code lấy token ở api.tino.vnauth.tino.vntrình duyệt của kháchỨng dụng của bạnserver hoặc appapi.tino.vntoken · userinfo · /v11. Chuyển tới /oauth/consentclient_id · scope · code_challenge2. Khách đăng nhập,chọn tài khoản, đồng ý3. Về redirect_uri?code=…&state=…4. POST /oauth/tokencode + code_verifier5. access_token, id_token,refresh_token6. GET /v1/<route>Bearer access_token
  1. Ứng dụng chuyển trình duyệt của khách tới https://auth.tino.vn/oauth/consent kèm client_id, scope, code_challenge.
  2. Khách đăng nhập (nếu chưa), chọn tài khoản (nếu trình duyệt có từ hai tài khoản), đồng ý cấp quyền.
  3. Tino chuyển trình duyệt về redirect_uri của ứng dụng với ?code=…&state=….
  4. Ứng dụng gọi POST https://api.tino.vn/oauth/token với code + code_verifier.
  5. Tino trả access_token, id_token (khi có openid), refresh_token (khi có offline_access).
  6. Ứng dụng gọi https://api.tino.vn/v1/<route> với Authorization: Bearer <access_token>.

Trình duyệt chỉ mang code về ứng dụng; ứng dụng tự gọi api.tino.vn để đổi code lấy token, kèm code_verifier chứng minh cùng một lượt đăng nhập.

Đăng ký ứng dụng

Tino tạo ứng dụng cho bạn và cấp client_id (cùng client_secret nếu ứng dụng có server). Client công khai (CLI, ứng dụng chạy trên máy khách, MCP client) cũng tự đăng ký được qua POST https://api.tino.vn/oauth/register (RFC 7591), xem Đăng nhập từ CLI.

Loại clientDùng khiXác thực ở /oauth/token
Confidential (có client_secret)Ứng dụng web đổi code ở serverclient_id + client_secret trong body form (client_secret_post) + PKCE
Public (không secret)Ứng dụng chạy trên máy khách: SPA, mobile, CLIchỉ client_id + PKCE

Khi xin ứng dụng, gửi Tino: tên và logo (https) hiện trên trang đồng ý, danh sách redirect URI, các quyền cần (mục Gọi API của Tino), và ứng dụng có giữ được secret ở server không.

Quy tắc redirect URI:

  • So khớp nguyên chuỗi với URI đã đăng ký (kể cả đường dẫn và query); không nhận URI có #fragment.
  • Phải là https://. Riêng khi phát triển trên máy: http://localhost hoặc http://127.0.0.1.
  • URI loopback đăng ký không kèm cổng khớp với mọi cổng (RFC 8252 mục 7.3), hợp cho ứng dụng CLI nghe cổng ngẫu nhiên.

client_id có thể là URL của một Client ID Metadata Document (discovery báo client_id_metadata_document_supported: true), nhưng chỉ các URL Tino đã đăng ký trước mới được nhận.

Bước 1 — Chuyển khách tới trang đồng ý

Ứng dụng tạo code_verifier, state, nonce ngẫu nhiên cho mỗi lần đăng nhập, giữ chúng (cookie httpOnly hoặc session), rồi chuyển trình duyệt tới https://auth.tino.vn/oauth/consent kèm các tham số trên query.

Tham sốBắt buộcGiá trị
response_typecócode
client_idcóclient_id Tino cấp
redirect_uricóđúng nguyên chuỗi đã đăng ký
scopecócác quyền cách nhau dấu cách, trong số quyền ứng dụng được cấp; openid để có ID token, offline_access để có refresh token
statenên cóchuỗi ngẫu nhiên chống CSRF; Tino trả nguyên về
code_challengecóBASE64URL(SHA256(code_verifier)), không dấu =
code_challenge_methodcóS256
noncenên có (OpenID)chuỗi ngẫu nhiên tối đa 255 ký tự; có lại trong ID token
promptkhôngnone: khách đã đăng nhập và đã cấp đủ quyền thì chuyển về ngay, không dừng ở màn nào

Ví dụ:

https://auth.tino.vn/oauth/consent?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&scope=openid%20profile%20email%20offline_access%20services%3Aread&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&nonce=n-0S6_WzA2Mj

Tạo PKCE (Node.js 18+):

import { createHash, randomBytes } from "node:crypto";

const codeVerifier = randomBytes(32).toString("base64url"); // 43 ký tự
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");

Trên trang đồng ý, khách chưa đăng nhập thì đăng nhập trước (OTP, passkey, Google, Zalo, GitHub). Trình duyệt có từ hai tài khoản Tino trở lên thì khách chọn tài khoản. Khách đã cấp đủ các quyền này cho ứng dụng thì không phải đồng ý lại; xin thêm quyền thì hỏi lại.

Kết quả, Tino chuyển trình duyệt về redirect_uri:

Tình huốngQuery về redirect_uri
Khách bấm Cho phépcode=…&state=…
Khách bấm Từ chốierror=access_denied&state=…
scope lạ hoặc ngoài quyền của ứng dụngerror=invalid_scope&state=…
client_id không có hoặc bị tắt, redirect_uri chưa đăng kýkhông chuyển về: trang của Tino báo lỗi

Trước khi dùng code, so state nhận được với state đã giữ; khác nhau thì bỏ.

Bước 2 — Đổi code lấy token

Đổi code trong 60 giây, mỗi code dùng một lần: POST https://api.tino.vn/oauth/token, body application/x-www-form-urlencoded. Client confidential gọi từ server và gửi client_secret trong body; không dùng header Authorization: Basic.

curl -X POST https://api.tino.vn/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=AUTH_CODE \
  -d redirect_uri=https://app.example.com/auth/callback \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d code_verifier=CODE_VERIFIER
Tham sốGiá trị
grant_typeauthorization_code
codecode nhận ở redirect_uri
redirect_uriđúng URI đã gửi ở bước 1
client_idclient_id của bạn
client_secretchỉ client confidential
code_verifierchuỗi gốc đã tạo code_challenge

Câu trả lời (HTTP 200):

{
  "access_token": "tinoat_…",
  "token_type": "Bearer",
  "expires_in": 86400,
  "scope": "openid profile email offline_access services:read",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
  "refresh_token": "tinort_…"
}
TokenCó khiThời hạn mặc định
access_token (tinoat_…, chuỗi mờ, không phải JWT)luôn có86400 giây (24 giờ)
id_token (JWT RS256)scope có openid86400 giây (24 giờ)
refresh_token (tinort_…)scope có offline_access2592000 giây (30 ngày)
code—60 giây, dùng một lần

Thời hạn do Tino cấu hình và có thể đổi: đọc expires_in của mỗi câu trả lời thay vì ghi cứng. Giữ token ở server (hoặc kho bảo mật của thiết bị), không đưa vào URL hay localStorage.

Làm mới và thu hồi token

Mỗi lần làm mới trả một refresh token mới và vô hiệu hoá cái cũ (xoay vòng). Dùng lại một refresh token đã xoay thì Tino thu hồi toàn bộ quyền của ứng dụng trên tài khoản đó; khách phải đăng nhập và đồng ý lại.

curl -X POST https://api.tino.vn/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=tinort_… \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET
  • scope (không bắt buộc): thu hẹp quyền của token mới; vượt quyền đã cấp thì invalid_scope và refresh token cũ vẫn dùng được.
  • Câu trả lời cùng dạng bước 2. Lưu ngay refresh_token mới trước khi dùng access_token.
  • Nhiều tiến trình cùng làm mới một refresh token: chỉ một lần thắng, lần kia bị coi là dùng lại. Gom việc làm mới về một chỗ (khoá theo tài khoản).
  • invalid_grant khi làm mới: token hết hạn, đã thu hồi hoặc khách đã thu hồi quyền ứng dụng. Đưa khách đăng nhập lại từ bước 1.

Thu hồi khi khách đăng xuất khỏi ứng dụng (RFC 7009): POST https://api.tino.vn/oauth/revoke với token=<refresh_token>, client_id, client_secret (client confidential). Refresh token và access token đi kèm cùng hết hiệu lực; endpoint luôn trả HTTP 200.

Khách cũng tự thu hồi được ở trang Ứng dụng được cấp quyền (https://auth.tino.vn/me/apps); token của ứng dụng hết hiệu lực ngay, lần gọi sau nhận 401 invalid_token.

ID token và UserInfo

ID token là JWT ký RS256; kiểm chữ ký bằng khoá có kid trùng header, lấy từ https://api.tino.vn/oauth/jwks (lưu cache, tải lại khi gặp kid lạ). Dùng thư viện OpenID Connect có sẵn (openid-client, jose, Authlib…) thay vì tự viết.

ClaimCó khiKiểm hoặc dùng
issluôn cóphải là https://api.tino.vn
aud, azpluôn cóphải là client_id của bạn
exp, iatluôn cócòn hạn
nonceđã gửi nonce ở bước 1phải trùng giá trị đã giữ
auth_timekhi Tino biếtlúc khách đăng nhập (unix giây)
subluôn cómã khách hàng Tino (chuỗi): khoá để gắn tài khoản trong ứng dụng của bạn
emailscope emailemail của tài khoản (hoặc của liên hệ phụ khi khách đăng nhập bằng tài khoản liên hệ)
given_name, family_name, namescope profiletên, họ, họ tên

Ghép tài khoản theo sub, không theo email: khách đổi được email.

UserInfo trả cùng các claim (sub và các claim theo scope), cần access token có openid:

curl https://api.tino.vn/oauth/userinfo -H "Authorization: Bearer tinoat_…"
{ "sub": "26", "email": "binh@example.com", "given_name": "Bình", "family_name": "Trần", "name": "Bình Trần" }

Access token thiếu openid thì nhận 403 insufficient_scope.

Gọi API của Tino

Ứng dụng gọi API Tino thay khách qua gateway https://api.tino.vn/v1/<route> với Authorization: Bearer <access_token>. Mỗi route thuộc một quyền; route ngoài quyền của token trả 403 insufficient_scope.

curl https://api.tino.vn/v1/details -H "Authorization: Bearer tinoat_…"

GET /v1/details chạy route GET /details của UserAPI bằng tài khoản của khách; body và câu trả lời giữ nguyên dạng của UserAPI.

Quyền (scope)Khách thấy trên trang đồng ýVí dụ routeSố route
openidXác thực danh tính của bạnID token, /oauth/userinfo—
profileHọ tênclaim name, given_name, family_name—
emailĐịa chỉ emailclaim email—
offline_accessDuy trì truy cập khi bạn không trực tuyếncó refresh_token—
profile:readXem thông tin tài khoảnGET /v1/details, GET /v1/contacts11
profile:writeSửa thông tin tài khoảnPUT /v1/notifications/@id/ack1
services:readXem dịch vụ, gồm cả thông tin đăng nhập dịch vụGET /v1/service, GET /v1/service/@id28
services:writeQuản lý dịch vụ của bạnPOST /v1/service/@id/renew, POST /v1/service/@id/vms/@vmid/reboot14
domains:readXem tên miềnGET /v1/domain, GET /v1/domain/@id17
domains:writeĐăng ký, gia hạn và quản lý tên miềnPOST /v1/domain/@id/renew, POST /v1/domain/@id/reg12
dns:readXem bản ghi DNSGET /v1/dns, GET /v1/domain/@id/dns7
dns:writeSửa bản ghi DNSPOST /v1/domain/@id/dns, DELETE /v1/domain/@id/dns/@index10
billing:readXem hoá đơn và thanh toánGET /v1/invoices, GET /v1/balance13
billing:writeThanh toán, nạp tiềnPOST /v1/addfunds7
orders:writeĐặt mua dịch vụ: giỏ hàng, báo giá, tạo đơn (trả bằng số dư cần thêm quyền Thanh toán)POST /v1/quote, POST /v1/carts, POST /v1/order8
tickets:readXem ticket hỗ trợGET /v1/tickets, GET /v1/tickets/@number9
tickets:writeTạo và trả lời ticketPOST /v1/tickets, POST /v1/tickets/@number4

Quyền của dịch vụ (gateway /v1 mở từ 27/10/2026): gọi https://api.tino.vn/v1/service/<id dịch vụ>/<dịch vụ>/… cho dịch vụ của chính khách.

Quyền (scope)Khách thấy trên trang đồng ýRouteHướng dẫn
tinopay:readXem TinoPay: tài khoản nhận tiền, giao dịch, webhook16Bắt đầu với TinoPay
tinopay:writeQuản lý TinoPay: tài khoản ảo, webhook, thông báo, tích hợp Sapo21Bắt đầu với TinoPay
builder:readXem website TinoPage Builder6Bắt đầu với TinoPage Builder
builder:writeQuản lý website TinoPage Builder: tạo, đăng, tên miền riêng7Bắt đầu với TinoPage Builder
waf:readXem website được TinoWAF bảo vệ và nhật ký sự kiện8TinoWAF
waf:writeQuản lý TinoWAF: website, mức bảo vệ, máy chủ gốc, luật IP11TinoWAF

tinopay:owner (tạo API token TinoPay, liên kết tài khoản ngân hàng nhận tiền) như domains:owner: chỉ token API cá nhân có giới hạn IP.

Xin ít quyền nhất đủ dùng: khách thấy từng dòng trên trang đồng ý. domains:owner (lấy mã EPP, đổi chủ thể tên miền) và domains:registrant (hồ sơ chủ thể tên miền đang chờ) chỉ dành cho token API cá nhân; ứng dụng không bao giờ được cấp. Ba lệnh đặt đơn có thể trừ số dư (POST /v1/carts/{uuid}/confirm, POST /v1/order, POST /v1/order/{product_id}) cần cả orders:write và billing:write.

Từng route, tham số, ví dụ request và response: Gọi API Tino.

Lỗi thường gặp

/oauth/token và /oauth/revoke trả lỗi dạng {"error": "<mã>", "error_description": "<câu tiếng Anh>"}. /oauth/userinfo và gateway /v1 kèm header WWW-Authenticate: Bearer error="<mã>".

EndpointHTTPerrorNguyên nhânXử lý
/oauth/token401invalid_clientsai client_id hoặc client_secret, ứng dụng bị tắtkiểm cấu hình; không gửi secret qua HTTP Basic
/oauth/token400invalid_grantcode sai, hết 60 giây, đã dùng, khác redirect_uri hoặc code_verifier; quyền đã bị thu hồicho khách đăng nhập lại từ bước 1
/oauth/token (refresh)400invalid_grantrefresh token hết hạn, đã thu hồi hoặc đã dùng lại (cả quyền bị thu hồi)cho khách đăng nhập lại
/oauth/token (refresh)400invalid_scopescope thu hẹp vượt quyền đã cấprefresh token cũ vẫn dùng được
/oauth/token400unsupported_grant_typegrant_type khác authorization_code và refresh_token—
/oauth/userinfo, /v1/*401invalid_tokenaccess token sai, hết hạn hoặc khách đã thu hồi quyềnlàm mới token; vẫn 401 thì đăng nhập lại
/oauth/userinfo, /v1/*403insufficient_scopetoken không có quyền của route (UserInfo: thiếu openid)xin thêm quyền ở bước 1
/v1/*403ipnotallowedIP gọi API đang bị Tino chặnliên hệ Tino
trang đồng ý—access_denied (về redirect_uri)khách bấm Từ chốibáo khách, cho thử lại
trang đồng ý—invalid_scope (về redirect_uri)scope lạ hoặc ngoài quyền của ứng dụngsửa scope

Lỗi nghiệp vụ của từng route /v1/* (ví dụ dịch vụ không tồn tại) giữ nguyên câu trả lời của UserAPI, thường là HTTP 200 kèm {"error": ["<mã>"]}.

Tham khảo