Đă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ục | Giá 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 |
| PKCE | bắt buộc, chỉ S256 |
| Xác thực client ở token endpoint | client_secret_post (client có secret) hoặc none (client công khai + PKCE); không hỗ trợ HTTP Basic |
| Ký ID token | RS256 |
Không có implicit flow, hybrid flow, client credentials hay password grant.
Luồng đăng nhập
- Ứng dụng chuyển trình duyệt của khách tới
https://auth.tino.vn/oauth/consentkèmclient_id,scope,code_challenge. - 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.
- Tino chuyển trình duyệt về
redirect_uricủa ứng dụng với?code=…&state=…. - Ứng dụng gọi
POST https://api.tino.vn/oauth/tokenvớicode+code_verifier. - Tino trả
access_token,id_token(khi cóopenid),refresh_token(khi cóoffline_access). - Ứng dụng gọi
https://api.tino.vn/v1/<route>vớiAuthorization: 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 client | Dùng khi | Xác thực ở /oauth/token |
|---|---|---|
Confidential (có client_secret) | Ứng dụng web đổi code ở server | client_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, CLI | chỉ 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://localhosthoặchttp://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ộc | Giá trị |
|---|---|---|
response_type | có | code |
client_id | có | client_id Tino cấp |
redirect_uri | có | đúng nguyên chuỗi đã đăng ký |
scope | có | 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 |
state | nên có | chuỗi ngẫu nhiên chống CSRF; Tino trả nguyên về |
code_challenge | có | BASE64URL(SHA256(code_verifier)), không dấu = |
code_challenge_method | có | S256 |
nonce | nên có (OpenID) | chuỗi ngẫu nhiên tối đa 255 ký tự; có lại trong ID token |
prompt | không | none: 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_WzA2MjTạ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ống | Query về redirect_uri |
|---|---|
| Khách bấm Cho phép | code=…&state=… |
| Khách bấm Từ chối | error=access_denied&state=… |
scope lạ hoặc ngoài quyền của ứng dụng | error=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_type | authorization_code |
code | code nhận ở redirect_uri |
redirect_uri | đúng URI đã gửi ở bước 1 |
client_id | client_id của bạn |
client_secret | chỉ client confidential |
code_verifier | chuỗ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_…"
}| Token | Có khi | Thờ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ó openid | 86400 giây (24 giờ) |
refresh_token (tinort_…) | scope có offline_access | 2592000 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_SECRETscope(không bắt buộc): thu hẹp quyền của token mới; vượt quyền đã cấp thìinvalid_scopevà refresh token cũ vẫn dùng được.- Câu trả lời cùng dạng bước 2. Lưu ngay
refresh_tokenmới trước khi dùngaccess_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_grantkhi 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.
| Claim | Có khi | Kiểm hoặc dùng |
|---|---|---|
iss | luôn có | phải là https://api.tino.vn |
aud, azp | luôn có | phải là client_id của bạn |
exp, iat | luôn có | còn hạn |
nonce | đã gửi nonce ở bước 1 | phải trùng giá trị đã giữ |
auth_time | khi Tino biết | lúc khách đăng nhập (unix giây) |
sub | luô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 |
email | scope email | email 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, name | scope profile | tê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ụ route | Số route |
|---|---|---|---|
openid | Xác thực danh tính của bạn | ID token, /oauth/userinfo | — |
profile | Họ tên | claim name, given_name, family_name | — |
email | Địa chỉ email | claim email | — |
offline_access | Duy trì truy cập khi bạn không trực tuyến | có refresh_token | — |
profile:read | Xem thông tin tài khoản | GET /v1/details, GET /v1/contacts | 11 |
profile:write | Sửa thông tin tài khoản | PUT /v1/notifications/@id/ack | 1 |
services:read | Xem dịch vụ, gồm cả thông tin đăng nhập dịch vụ | GET /v1/service, GET /v1/service/@id | 28 |
services:write | Quản lý dịch vụ của bạn | POST /v1/service/@id/renew, POST /v1/service/@id/vms/@vmid/reboot | 14 |
domains:read | Xem tên miền | GET /v1/domain, GET /v1/domain/@id | 17 |
domains:write | Đăng ký, gia hạn và quản lý tên miền | POST /v1/domain/@id/renew, POST /v1/domain/@id/reg | 12 |
dns:read | Xem bản ghi DNS | GET /v1/dns, GET /v1/domain/@id/dns | 7 |
dns:write | Sửa bản ghi DNS | POST /v1/domain/@id/dns, DELETE /v1/domain/@id/dns/@index | 10 |
billing:read | Xem hoá đơn và thanh toán | GET /v1/invoices, GET /v1/balance | 13 |
billing:write | Thanh toán, nạp tiền | POST /v1/addfunds | 7 |
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/order | 8 |
tickets:read | Xem ticket hỗ trợ | GET /v1/tickets, GET /v1/tickets/@number | 9 |
tickets:write | Tạo và trả lời ticket | POST /v1/tickets, POST /v1/tickets/@number | 4 |
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 ý | Route | Hướng dẫn |
|---|---|---|---|
tinopay:read | Xem TinoPay: tài khoản nhận tiền, giao dịch, webhook | 16 | Bắt đầu với TinoPay |
tinopay:write | Quản lý TinoPay: tài khoản ảo, webhook, thông báo, tích hợp Sapo | 21 | Bắt đầu với TinoPay |
builder:read | Xem website TinoPage Builder | 6 | Bắt đầu với TinoPage Builder |
builder:write | Quản lý website TinoPage Builder: tạo, đăng, tên miền riêng | 7 | Bắt đầu với TinoPage Builder |
waf:read | Xem website được TinoWAF bảo vệ và nhật ký sự kiện | 8 | TinoWAF |
waf:write | Quản lý TinoWAF: website, mức bảo vệ, máy chủ gốc, luật IP | 11 | TinoWAF |
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ã>".
| Endpoint | HTTP | error | Nguyên nhân | Xử lý |
|---|---|---|---|---|
/oauth/token | 401 | invalid_client | sai client_id hoặc client_secret, ứng dụng bị tắt | kiểm cấu hình; không gửi secret qua HTTP Basic |
/oauth/token | 400 | invalid_grant | code sai, hết 60 giây, đã dùng, khác redirect_uri hoặc code_verifier; quyền đã bị thu hồi | cho khách đăng nhập lại từ bước 1 |
/oauth/token (refresh) | 400 | invalid_grant | refresh 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) | 400 | invalid_scope | scope thu hẹp vượt quyền đã cấp | refresh token cũ vẫn dùng được |
/oauth/token | 400 | unsupported_grant_type | grant_type khác authorization_code và refresh_token | — |
/oauth/userinfo, /v1/* | 401 | invalid_token | access token sai, hết hạn hoặc khách đã thu hồi quyền | làm mới token; vẫn 401 thì đăng nhập lại |
/oauth/userinfo, /v1/* | 403 | insufficient_scope | token không có quyền của route (UserInfo: thiếu openid) | xin thêm quyền ở bước 1 |
/v1/* | 403 | ipnotallowed | IP gọi API đang bị Tino chặn | liên hệ Tino |
| trang đồng ý | — | access_denied (về redirect_uri) | khách bấm Từ chối | bá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ụng | sử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
- OpenID discovery của Tino: endpoint, quyền, claim đang chạy trên production
- Protected resource metadata (RFC 9728): cho client MCP; xem Kết nối Claude, ChatGPT với Tino (MCP)
- JWKS: khoá công khai kiểm ID token
- RFC 6749 OAuth 2.0
- RFC 7636 PKCE
- RFC 7009 thu hồi token
- RFC 8252 ứng dụng native
- OpenID Connect Core 1.0