Authentication
ช่องทาง External S2S มี auth 2 ชั้น:
- S2S HMAC (ทุก request) — พิสูจน์ว่า request มาจาก backend ของคุณจริง ด้วย
X-Api-Key+ ลายเซ็น - Member Bearer (เฉพาะ endpoint รายสมาชิก) — พิสูจน์ว่าเป็น session ของสมาชิกคนไหน ด้วย
Authorization: Bearer
ชั้น 1 — S2S HMAC
4 headers ทุก request
| Header | ค่า |
|---|---|
X-Api-Key | API key ของคุณ (ขึ้นต้น ak_) |
X-Timestamp | เวลาที่ sign เป็น unix milliseconds — เพี้ยนจากเวลา server ได้ไม่เกิน ±300 วินาที |
X-Nonce | ค่าไม่ซ้ำต่อ request (แนะนำ UUIDv4) — ยิงซ้ำภายใน 6 นาทีถูกปฏิเสธ |
X-Signature | hex( HMAC-SHA256(secret, canonical) ) ตามสูตรด้านล่าง |
Headers เสริม
ไม่ถูกรวมใน canonical — sign เฉพาะตามสูตรด้านล่าง:
| Header | ใช้เมื่อ |
|---|---|
X-Player-IP | IP จริงของผู้เล่น — บังคับตอน POST /games/:id/launch · เส้นอื่นแนะนำให้ส่ง (ลง login history) |
X-Player-UA | User-Agent ของผู้เล่น (optional — ลง login history) |
X-Request-Id | correlation id ของคุณ — echo กลับใน request_uuid |
x-lang | th (default) หรือ en — ภาษาของ message |
Canonical string
canonical = METHOD (uppercase) + "\n"
+ PATH_WITH_QUERY + "\n" ← เริ่ม "/api/v1/..." รวม "?query" ตามที่ส่งจริง
+ X-Timestamp + "\n"
+ X-Nonce + "\n"
+ hex( SHA-256(raw body bytes) ) ← ไม่มี body → hash ของ empty string:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855- 5 บรรทัดคั่นด้วย
\n(LF ตัวเดียว ไม่ใช่\r\n) — sign จาก raw bytes ที่ส่งจริง
ตัวอย่าง Node.js
import { createHash, createHmac, randomUUID } from "node:crypto";
export function signedHeaders(method: string, pathWithQuery: string, bodyStr = "") {
const timestamp = Date.now().toString();
const nonce = randomUUID();
const bodyHash = createHash("sha256").update(bodyStr, "utf8").digest("hex");
const canonical = [method.toUpperCase(), pathWithQuery, timestamp, nonce, bodyHash].join("\n");
return {
"X-Api-Key": process.env.API_KEY!,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": createHmac("sha256", process.env.API_SECRET!).update(canonical).digest("hex"),
};
}กติกาบังคับ
- ทุก retry sign ใหม่ — เปลี่ยน timestamp + nonce ทุกครั้ง ห้ามส่งค่าเดิมซ้ำ
X-Player-IP= IP จริงของผู้เล่นX-Timestamp±300 วินาที
request ที่ sign ผิดตอบ HTTP 401:
| Code | สาเหตุ |
|---|---|
1019 | signature ไม่ตรง |
1020 | timestamp เพี้ยน (เกิน ±300 วินาที) |
1021 | nonce ซ้ำ (ยิงซ้ำใน 6 นาที) |
ตัวอย่างการ sign request
ค่า demo ที่ใช้ทุกตัวอย่าง:
API_KEY = ak_3f9a1c5e7b2d4f6a8c0e1b3d5f7a9c2e4d6f8a0b1c3d5e7f
API_SECRET = 5b0e5f3c2a9d4e8f7c6b5a49382716053c2b1a0f9e8d7c6b5a4938271605f4e3sign แต่ละตัวอย่างแล้วต้องได้ X-Signature ตรงทุกตัวอักษร — path ใน canonical เป็น path เต็ม เช่น /api/v1/auth/login ไม่ใช่ /auth/login ตามหัวข้อด้านล่าง · ไม่ตรง เทียบ bodyHash ก่อน (ผิดตั้งแต่ hash แปลว่า body bytes ไม่ตรง) แล้วไล่ canonical ทีละบรรทัด
A — POST /auth/login · body {"phone":"0812345678","password":"P@ssw0rd123"}
bodyHash = 7398bfab1d434e577244f950baebd60ef61bf2c6e2c1f8ef71ec9513fdbdbe1d
X-Timestamp = 1783236600000
X-Nonce = 9f2b7d1e-4a83-4c1f-b2ce-6d5a0e8f3c21
X-Signature = 8e039b28037162d5b09f50f79a2af6ab24086661ddad29a997d2b0d864d762a0B — GET /profile/balance · ไม่มี body (empty-string hash) + Authorization: Bearer <token>
bodyHash = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Timestamp = 1783236660000
X-Nonce = c4d8a2f6-1b3e-4f7a-9c0d-2e5b8a1f6d43
X-Signature = f0f002b52554c371d87edb0f9687c5c269ad2f7e0cbf6b58a225ad7096e6a96aC — POST /auth/verify-otp · body {"phone":"0812345678","otp_code":"123456"}
bodyHash = 37b0f11f251d34d64a8bbc84cef2a340971b983001cb8025fd43c9ae746b7f06
X-Timestamp = 1783236720000
X-Nonce = 7a1e9c3b-5d2f-4e8a-a6b4-0c9d8e7f2a15
X-Signature = 9ea09be21aefaa87339759b6c90a033572b41055d6f419830fcba386e3edc5b3D — GET /transactions?page=1&limit=20 · query อยู่ใน canonical ด้วย
bodyHash = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Timestamp = 1783236780000
X-Nonce = e2f4a6c8-0b1d-4c3e-9f5a-7d8e9c0a1b2f
X-Signature = bd3430969ab63792c9a9ca3ea9faafb3525920db53606aab1fa1777807a5d27bชั้น 2 — Member token
login/register/refresh คืน JWT 2 ตัวต่อ session:
| Token | อายุ | ใช้อย่างไร |
|---|---|---|
access_token | 15 นาที (expires_in = 900) | แนบ Authorization: Bearer บนทุก endpoint รายสมาชิก |
refresh_token | 7 วัน · ใช้ได้ครั้งเดียว | ขอคู่ใหม่ผ่าน POST /auth/refresh |
กติกา session
- single-use rotation — หาก request ขอ refresh token ใหม่แล้ว ค่าเดิมจะใช้งานไม่ได้แล้ว
- 1 สมาชิก = 1 session — login จากที่ใหม่ทับ session เดิม refresh token ชุดเก่าใช้ไม่ได้อีก
POST /auth/logoutrevoke session ทันที — access token ที่ยังไม่หมดอายุถูกตัดผลด้วย (fast-revoke)
จัดการ token error
code เกี่ยวกับ token มี 3 ตัว:
| Code | ความหมาย |
|---|---|
1001 | unauthorized (ไม่มี/ไม่ผ่าน token) |
1006 | token หมดอายุ |
1007 | token ไม่ถูกต้อง |
เจอ code พวกนี้ → refresh หนึ่งครั้งแล้ว retry request เดิม ถ้า refresh ไม่ผ่านให้พาผู้เล่น login ใหม่
เมื่อลงชื่อเข้าใช้งานผิด
ลงชื่อเข้าใช้ผิด 5 ครั้งใน 15 นาที → บัญชีถูกล็อกชั่วคราว · เข้าใช้สำเร็จหนึ่งครั้งจะรีเซ็ตตัวนับ
| Code | HTTP | ความหมาย |
|---|---|---|
1016 | 429 | บัญชีถูกล็อกชั่วคราว (ลงชื่อเข้าใช้ผิดเกินกำหนด) |
Error Code
| code | ชื่อ | HTTP | ความหมาย |
|---|---|---|---|
1018 | API_KEY_INVALID | 401 | key ไม่มีอยู่ / ถูกปิด / ถูก revoke |
1019 | API_SIGNATURE_INVALID | 401 | signature ไม่ตรง — ไล่เช็คตามกติกาบังคับก่อน |
1020 | API_TIMESTAMP_INVALID | 401 | timestamp เพี้ยนเกิน ±300s — เช็ค NTP |
1021 | API_NONCE_REPLAYED | 401 | nonce ซ้ำ — ทุก request/retry ต้อง sign ใหม่ |
1022 | API_CHANNEL_FORBIDDEN | 403 | credential ใช้ช่องทางนี้ไม่ได้ (ยังไม่เปิด External Site / ไม่ใช่ agent TRANSFER / token สมาชิกไม่ใช่ของ agent คุณ) |
1023 | API_IP_NOT_ALLOWED | 403 | IP ของ server คุณไม่อยู่ใน allowlist |