Tích hợp đăng nhập từ đầu đến cuối.
Website tích hợp chuyển người dùng sang APIUYTIN. Người dùng chọn tài khoản APIUYTIN đã nhớ trên thiết bị và đồng ý chia sẻ hồ sơ. APIUYTIN trả authorization code một lần về callback đã đăng ký; máy chủ của website đổi code thành access token và hồ sơ.
https://apiuytin.pages.dev/oauth/googlehttps://apiuytin.pages.dev/api/auth01 / CHUẨN BỊ DỰ ÁN
Chuẩn bị dự án và callback
- Đăng nhập bảng điều khiển APIUYTIN bằng tài khoản chủ sở hữu.
- Tạo dự án với tên dễ nhận biết và callback HTTPS chính xác của ứng dụng, ví dụ
https://app.example.com/auth/apiuytin/callback. - Lưu
clientIdtrong cấu hình ứng dụng. Đây là mã định danh công khai, nhưng vẫn nên dùng đúng dự án theo môi trường. - Lưu
apiKeytrong trình quản lý bí mật phía máy chủ. Phản hồi tạo dự án hiển thị key một lần. - Đảm bảo tuyến callback có thể nhận tham số truy vấn GET
code,statehoặcerror.
Callback phải là HTTPS và phải khớp dự án đã đăng ký. APIUYTIN chuẩn hóa URL bằng bộ phân tích URL rồi so sánh callback của yêu cầu với giá trị lưu trên dự án. Không thêm fragment, username hoặc password vào URL.
{
"project": {
"id": "project-...",
"name": "Example Web",
"clientId": "pk_...",
"redirectUri": "https://app.example.com/auth/apiuytin/callback",
"createdAt": "2026-11-08T00:00:00.000Z",
"status": "active",
"apiKey": "CHUOTAPI_..."
},
"warning": "API key chỉ hiển thị một lần."
}
02 / YÊU CẦU ỦY QUYỀN
Tạo state và chuyển hướng người dùng
Tạo state ngẫu nhiên cho từng lần đăng nhập. Lưu state trong phiên phía máy chủ hoặc kho state có TTL ngắn, gắn với phiên trình duyệt hiện tại và xóa ngay khi callback xử lý xong. Không dùng một state cố định cho nhiều người dùng hoặc nhiều tab.
// Tuyến Express chạy trên máy chủ của website tích hợp.
import { randomBytes } from "node:crypto";
app.get("/login/apiuytin", async (req, res, next) => {
try {
const state = randomBytes(32).toString("base64url");
req.session.apiuytinStates ||= {};
req.session.apiuytinStates[state] = {
createdAt: Date.now(),
returnTo: "/account"
};
// Bảo đảm session đã được lưu trước khi rời website.
await new Promise((resolve, reject) => {
req.session.save((error) => error ? reject(error) : resolve());
});
const loginUrl = new URL("https://apiuytin.pages.dev/oauth/google");
loginUrl.searchParams.set("client_id", process.env.APIUYTIN_CLIENT_ID);
loginUrl.searchParams.set(
"redirect_uri",
"https://app.example.com/auth/apiuytin/callback"
);
loginUrl.searchParams.set("state", state);
res.redirect(302, loginUrl.toString());
} catch (error) {
next(error);
}
});
Ba tham số truy vấn được gửi tới APIUYTIN:
client_id: Client ID của dự án.redirect_uri: callback đã đăng ký, phải là HTTPS.state: nonce do ứng dụng tích hợp sinh và kiểm tra lại ở callback.
Không gửi API key, hồ sơ người dùng, access token hoặc dữ liệu bí mật trong URL OAuth.
03 / BỘ CHỌN TÀI KHOẢN APIUYTIN
Người dùng chọn tài khoản và đồng ý
APIUYTIN xác thực dự án và callback, tạo transaction ngắn hạn, rồi mở bộ chọn trên cùng origin. Bộ chọn hiển thị tên dự án, domain callback, các tài khoản đã liên kết với thiết bị, avatar, tên, email và mô tả dữ liệu sẽ chia sẻ.
- Thiết bị đã có tài khoản: người dùng chọn một tài khoản rồi bấm Tiếp tục.
- Thêm tài khoản: Google OAuth chạy để xác thực tài khoản mới, APIUYTIN tạo hoặc cập nhật apiUser, liên kết tài khoản với thiết bị và quay lại màn hình đồng ý.
- Thiết bị mới hoặc cookie hết hạn: cần xác thực Google để liên kết tài khoản với thiết bị đó.
- Hủy: APIUYTIN redirect về callback với
error=access_deniedvà state ban đầu nếu state đã được gửi.
Cookie thiết bị tên apiuytin_device, có thuộc tính HttpOnly, Secure, SameSite=Lax và thời hạn tối đa 30 ngày. Cookie này chỉ giúp APIUYTIN nhớ tài khoản trên thiết bị; website tích hợp không đọc cookie này.
Bộ chọn tài khoản không gửi Google access token hoặc refresh token cho ứng dụng tích hợp. Hồ sơ trả về dùng ID riêng của APIUYTIN.
04 / CALLBACK
Kiểm tra callback trước khi đổi code
Callback nhận một trong hai dạng sau. Thứ tự tham số truy vấn không được xem là cố định.
https://app.example.com/auth/apiuytin/callback?code=ONE_TIME_CODE&state=STATE_CREATED_BY_YOUR_APP
https://app.example.com/auth/apiuytin/callback?error=access_denied&state=STATE_CREATED_BY_YOUR_APP
- Đọc state, code và error từ chuỗi truy vấn.
- Tìm state trong phiên phía máy chủ của trình duyệt hiện tại; từ chối nếu thiếu, không khớp hoặc đã hết hạn. TTL cục bộ nên ngắn, ví dụ 10 phút.
- Xóa state khỏi state store theo cách dùng một lần, kể cả khi người dùng hủy.
- Nếu có lỗi
error, không gọi endpoint token. Hiển thị thông báo phù hợp;access_deniedlà thao tác hủy của người dùng. - Nếu không có code hoặc state hợp lệ, dừng xử lý và không tạo session đăng nhập.
- Chỉ tiếp tục đổi code ở máy chủ. Không đổi code trực tiếp trong trình duyệt.
returnTo từ query string để chuyển hướng trực tiếp. Lưu đường dẫn nội bộ trong phiên lúc bắt đầu đăng nhập và chỉ cho phép đường dẫn cục bộ đã kiểm tra.05 / ĐỔI TOKEN
Máy chủ đổi authorization code
Code có thời hạn tối đa 5 phút, ràng buộc với dự án và redirect URI, và chỉ đổi thành công một lần. Gửi JSON đến endpoint token bằng HTTPS. API key đọc từ biến môi trường hoặc trình quản lý bí mật ở máy chủ.
const tokenResponse = await fetch(
"https://apiuytin.pages.dev/api/auth/oauth/token",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
client_id: process.env.APIUYTIN_CLIENT_ID,
api_key: process.env.APIUYTIN_API_KEY,
code,
redirect_uri: "https://app.example.com/auth/apiuytin/callback"
}),
signal: AbortSignal.timeout(8000)
}
);
const payload = await tokenResponse.json();
if (!tokenResponse.ok) {
// Map payload.error; không log request body hoặc api_key.
throw new Error("Đổi token APIUYTIN không thành công: " + payload.error);
}
const apiuytinUser = payload.user;
Nội dung yêu cầu bắt buộc có đúng các giá trị cần thiết sau. client_secret không được hỗ trợ và yêu cầu có trường này sẽ bị từ chối.
{
"client_id": "pk_YOUR_CLIENT_ID",
"api_key": "CHUOTAPI_SERVER_SIDE_KEY",
"code": "ONE_TIME_CODE",
"redirect_uri": "https://app.example.com/auth/apiuytin/callback"
}
Response thành công có dạng:
{
"access_token": "SIGNED_ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": "user-apiuytin-...",
"sub": "user-apiuytin-...",
"email": "user@example.com",
"name": "User Name",
"picture": "https://example.com/avatar.jpg",
"given_name": "User",
"family_name": "Name",
"locale": "vi",
"verified_email": true
}
}
Tạo phiên đăng nhập riêng của website từ hồ sơ đã nhận. Nếu website cần gọi userinfo sau này, chỉ lưu access token ở máy chủ; token hết hạn sau 3600 giây và không có luồng refresh-token trong quy ước này.
06 / HỒ SƠ
Đọc hồ sơ bằng userinfo
Phản hồi token có hồ sơ trong user. Khi cần xác nhận hồ sơ theo access token hiện tại, máy chủ có thể gọi:
const response = await fetch(
"https://apiuytin.pages.dev/api/auth/userinfo",
{
headers: { authorization: "Bearer " + accessToken },
signal: AbortSignal.timeout(8000)
}
);
if (!response.ok) {
// 401: token không hợp lệ/hết hạn hoặc dự án đã bị thu hồi.
}
const user = await response.json();
/userinfo trả trực tiếp đối tượng hồ sơ, cùng schema với tokenResponse.user:
{
"id": "user-apiuytin-...",
"sub": "user-apiuytin-...",
"email": "user@example.com",
"name": "User Name",
"picture": "https://example.com/avatar.jpg",
"given_name": "User",
"family_name": "Name",
"locale": "vi",
"verified_email": true
}
id và sub là định danh ổn định của APIUYTIN. Subject Google không được gửi cho website tích hợp. Nếu Google không cung cấp trường tùy chọn, trường tương ứng là chuỗi rỗng; verified_email là giá trị đúng/sai.
07 / QUẢN LÝ DỰ ÁN
Quản lý dự án và API key
Các endpoint quản lý dự án yêu cầu phiên chủ sở hữu APIUYTIN, thường do bảng điều khiển cấp qua cookie HttpOnly apiuytin_session. Chúng không phải endpoint đăng nhập người dùng cuối. Từ bảng điều khiển, các yêu cầu cùng origin tự gửi cookie; nếu tích hợp bảng điều khiển khác origin, cần gửi thông tin xác thực và cấu hình phiên phù hợp.
Tạo dự án
POST /api/auth/projects
Content-Type: application/json
Cookie: apiuytin_session=...
{
"name": "Example Web",
"redirectUri": "https://app.example.com/auth/apiuytin/callback"
}
Thành công trả HTTP 201. Chỉ phản hồi tạo dự án có API key dạng văn bản gốc lần đầu. Lưu key ngay vào trình quản lý bí mật.
Cập nhật callback
PATCH /api/auth/projects/PROJECT_ID
Content-Type: application/json
Cookie: apiuytin_session=...
{ "redirectUri": "https://app.example.com/auth/apiuytin/callback" }
Giá trị callback mới áp dụng ngay cho yêu cầu OAuth kế tiếp. Đồng bộ cấu hình bên tích hợp trước khi chuyển lưu lượng sang callback mới.
Cấp lại key
POST /api/auth/projects/PROJECT_ID/rotate-key
Cookie: apiuytin_session=...
Chỉ chủ sở hữu mới được cấp lại key. APIUYTIN thay hash bằng cơ chế bảo vệ phiên bản; key mới trả một lần trong project.apiKey. Sau khi cấp lại thành công, cập nhật trình quản lý bí mật và khởi động lại/tải lại các dịch vụ dùng key. Không có cách lấy lại key cũ dạng văn bản gốc. Nếu hai lần cấp lại chạy đồng thời, yêu cầu thua phiên bản trả key_rotation_conflict; đọc lại dự án và dùng key hiện tại đã lưu an toàn.
Thu hồi dự án
POST /api/auth/projects/PROJECT_ID/revoke
Cookie: apiuytin_session=...
Endpoint đổi trạng thái dự án sang revoked trước, làm token/userinfo/API dự án từ chối sử dụng dự án. Dọn auth code, ánh xạ người dùng và giao dịch OAuth chạy nền khi runtime hỗ trợ waitUntil. Phản hồi có cleanupScheduled; hiệu lực thu hồi được xác nhận bởi trạng thái revoked, không bởi việc dọn dữ liệu đã hoàn tất.
08 / DANH SÁCH ENDPOINT
Danh sách endpoint
/api/authEndpoint khám phá, không cần xác thực/api/auth/healthPhản hồi kiểm tra hệ thống công khai/oauth/googleBắt đầu bộ chọn tài khoản bằng client_id, redirect_uri, state/oauth/consentHiển thị chooser cho transaction hiện tại/oauth/consentTiếp tục hoặc hủy biểu mẫu đồng ý/api/auth/oauth/tokenĐổi code dùng một lần bằng client_id và api_key/api/auth/userinfoBearer access token, trả hồ sơ trực tiếp/api/auth/sessionPhiên bảng điều khiển và các dự án đang hoạt động/api/auth/projectsChủ sở hữu tạo dự án; key dạng văn bản gốc trả một lần/api/auth/projectsChủ sở hữu xem dự án; không trả API key/api/auth/projects/:idChủ sở hữu sửa redirectUri/api/auth/projects/:id/rotate-keyChủ sở hữu thay API key; key mới trả một lần/api/auth/projects/:id/revokeChủ sở hữu thu hồi dự án, dọn dữ liệu nền/api/auth/project-infoBearer API key, thông tin dự án không có key/api/auth/allusersBearer API key, người dùng đã đăng nhập vào dự án/api/auth/logoutXóa cookie phiên bảng điều khiểnproject-info và allusers dùng API key dưới dạng Bearer. userinfo dùng access token của người dùng cuối dưới dạng Bearer. Hai loại token có mục đích khác nhau, không dùng thay thế nhau.
09 / XỬ LÝ LỖI
Lỗi thường gặp và retry
invalid_jsonBody không phải JSON hợp lệinvalid_grantCode sai, hết hạn, đã dùng hoặc không khớp dự án/callbackinvalid_clientClient ID, API key, trạng thái dự án hoặc redirect_uri không hợp lệinvalid_api_keyAPI key thiếu hoặc không còn hợp lệ cho API dự ánunauthorizedAccess token/userinfo hoặc phiên chủ sở hữu không hợp lệtoken_expiredAccess token hết hạn; bắt đầu đăng nhập lạiproject_revokedDự án bị chủ sở hữu thu hồidevice_session_mismatchTransaction được mở trên device khácproject_not_foundDự án không tồn tại, không thuộc chủ sở hữu hoặc đã bị thu hồikey_rotation_conflictRotation khác đã thắng revision guard; không retry mùinternal_errorLỗi runtime; dùng correlation ID riêng để điều traoauth_unavailableDịch vụ bên ngoài hoặc lưu trữ tạm thời không khả dụng; bắt đầu lại giao dịch sau khoảng chờ ngắn- Không retry lỗi 4xx mà không đổi input hoặc trạng thái.
- Không thử lại mù POST token bằng cùng code sau khi hết thời gian chờ; authorization code là code dùng một lần.
- Không thử lại thao tác tạo mới, cấp lại hoặc thu hồi vô hạn. Nếu kết quả không rõ, truy vấn trạng thái phù hợp rồi quyết định bước tiếp theo.
- APIUYTIN thử lại truy vấn Sanity tạm thời với giới hạn; mutation không được thử lại mù. Batch dọn dữ liệu dùng tối đa 50 mutation và đồng thời tối đa 6 tác vụ.
- Giới hạn đồng thời giảm fan-out nhưng không loại bỏ quota/giới hạn tần suất của Google, Sanity hoặc Cloudflare.
10 / BẢO MẬT
Ranh giới bảo mật
- Đổi code và mọi yêu cầu mang API key chỉ chạy ở máy chủ.
- Không đặt API key trong JavaScript trình duyệt, HTML, URL, localStorage, gói ứng dụng di động, analytics hoặc log.
- Không ghi authorization header, callback code, access token, cookie hoặc token request body vào log.
- Kiểm tra state phía máy chủ và xóa state sau một lần xử lý.
- Giới hạn return path về đường dẫn nội bộ để tránh open redirect.
- Đặt thời gian chờ cho yêu cầu HTTP và giới hạn kích thước nội dung.
- Chỉ lưu các trường hồ sơ cần thiết. Bảo vệ dữ liệu email và avatar theo chính sách riêng của ứng dụng.
- Gắn HTTPS cho callback ở mọi môi trường được đăng ký.
- Không cố tạo luồng vô hạn để né rate limit của upstream.
11 / KIỂM TRA TRƯỚC KHI CHẠY
Kiểm tra trước khi bật đăng nhập
- Client ID và API key thuộc đúng dự án/môi trường.
- API key chỉ tồn tại trong trình quản lý bí mật phía máy chủ.
- Callback HTTPS khớp chính xác dự án đã đăng ký.
- State được sinh ngẫu nhiên, lưu phía máy chủ, có TTL và dùng một lần.
- Đường đi hủy xử lý
access_deniedmà không gọi token endpoint. - Đổi token gửi đúng bốn trường:
client_id,api_key,code,redirect_uri. - Máy chủ từ chối invalid_client/invalid_grant và không tự tạo phiên khi phản hồi token lỗi.
- Ứng dụng tạo phiên cục bộ từ hồ sơ APIUYTIN, không dùng Google subject làm ID người dùng.
- Nhật ký đã che code, API key, access token, cookie và authorization header.
- Đã thử tài khoản đã nhớ, thiết bị mới, thêm tài khoản, hủy, callback sai và phát lại code.
- Đã thử cấp lại key và xác nhận key cũ bị từ chối.
- Đã xác nhận thu hồi dự án làm userinfo và API dự án bị từ chối.
