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ơ.

Bắt đầu OAuthhttps://apiuytin.pages.dev/oauth/google
Địa chỉ APIhttps://apiuytin.pages.dev/api/auth
Quy ước: một dự án có Client ID và API key. Client ID dùng để bắt đầu OAuth; API key chỉ dùng ở phía máy chủ khi đổi code hoặc gọi API của dự án.

01 / CHUẨN BỊ DỰ ÁN

Chuẩn bị dự án và callback

  1. Đăng nhập bảng điều khiển APIUYTIN bằng tài khoản chủ sở hữu.
  2. 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.
  3. Lưu clientId trong 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.
  4. Lưu apiKey trong 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.
  5. Đảm bảo tuyến callback có thể nhận tham số truy vấn GET code, state hoặc error.

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."
}
Không mất API key: APIUYTIN không trả lại key trong phiên, danh sách dự án hoặc project-info. Nếu key mất, chủ sở hữu phải cấp lại và cập nhật trình quản lý bí mật.

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_denied và 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
  1. Đọc state, code và error từ chuỗi truy vấn.
  2. 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.
  3. 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.
  4. Nếu có lỗi error, không gọi endpoint token. Hiển thị thông báo phù hợp; access_denied là thao tác hủy của người dùng.
  5. 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.
  6. Chỉ tiếp tục đổi code ở máy chủ. Không đổi code trực tiếp trong trình duyệt.
Không tin 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.

Nếu yêu cầu đổi code hết thời gian chờ sau khi gửi, kết quả có thể chưa rõ vì code dùng một lần. Không tự động thử lại cùng code. Kiểm tra trạng thái nghiệp vụ; nếu không thể xác định, bắt đầu luồng đăng nhập mới.

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

GET/api/authEndpoint khám phá, không cần xác thực
GET/api/auth/healthPhản hồi kiểm tra hệ thống công khai
GET/oauth/googleBắt đầu bộ chọn tài khoản bằng client_id, redirect_uri, state
GET/oauth/consentHiển thị chooser cho transaction hiện tại
POST/oauth/consentTiếp tục hoặc hủy biểu mẫu đồng ý
POST/api/auth/oauth/tokenĐổi code dùng một lần bằng client_id và api_key
GET/api/auth/userinfoBearer access token, trả hồ sơ trực tiếp
GET/api/auth/sessionPhiên bảng điều khiển và các dự án đang hoạt động
POST/api/auth/projectsChủ sở hữu tạo dự án; key dạng văn bản gốc trả một lần
GET/api/auth/projectsChủ sở hữu xem dự án; không trả API key
PATCH/api/auth/projects/:idChủ sở hữu sửa redirectUri
POST/api/auth/projects/:id/rotate-keyChủ sở hữu thay API key; key mới trả một lần
POST/api/auth/projects/:id/revokeChủ sở hữu thu hồi dự án, dọn dữ liệu nền
GET/api/auth/project-infoBearer API key, thông tin dự án không có key
GET/api/auth/allusersBearer API key, người dùng đã đăng nhập vào dự án
POST/api/auth/logoutXóa cookie phiên bảng điều khiển

project-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

400invalid_jsonBody không phải JSON hợp lệ
400invalid_grantCode sai, hết hạn, đã dùng hoặc không khớp dự án/callback
401invalid_clientClient ID, API key, trạng thái dự án hoặc redirect_uri không hợp lệ
401invalid_api_keyAPI key thiếu hoặc không còn hợp lệ cho API dự án
401unauthorizedAccess token/userinfo hoặc phiên chủ sở hữu không hợp lệ
401token_expiredAccess token hết hạn; bắt đầu đăng nhập lại
401project_revokedDự án bị chủ sở hữu thu hồi
403device_session_mismatchTransaction được mở trên device khác
404project_not_foundDự án không tồn tại, không thuộc chủ sở hữu hoặc đã bị thu hồi
409key_rotation_conflictRotation khác đã thắng revision guard; không retry mù
500internal_errorLỗi runtime; dùng correlation ID riêng để điều tra
503oauth_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

  1. Client ID và API key thuộc đúng dự án/môi trường.
  2. API key chỉ tồn tại trong trình quản lý bí mật phía máy chủ.
  3. Callback HTTPS khớp chính xác dự án đã đăng ký.
  4. State được sinh ngẫu nhiên, lưu phía máy chủ, có TTL và dùng một lần.
  5. Đường đi hủy xử lý access_denied mà không gọi token endpoint.
  6. Đổi token gửi đúng bốn trường: client_id, api_key, code, redirect_uri.
  7. 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.
  8. Ứ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.
  9. Nhật ký đã che code, API key, access token, cookie và authorization header.
  10. Đã 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.
  11. Đã thử cấp lại key và xác nhận key cũ bị từ chối.
  12. Đã xác nhận thu hồi dự án làm userinfo và API dự án bị từ chối.