Authentication

ช่องทาง External S2S มี auth 2 ชั้น:

  1. S2S HMAC (ทุก request) — พิสูจน์ว่า request มาจาก backend ของคุณจริง ด้วย X-Api-Key + ลายเซ็น
  2. Member Bearer (เฉพาะ endpoint รายสมาชิก) — พิสูจน์ว่าเป็น session ของสมาชิกคนไหน ด้วย Authorization: Bearer

ชั้น 1 — S2S HMAC

4 headers ทุก request

Headerค่า
X-Api-KeyAPI key ของคุณ (ขึ้นต้น ak_)
X-Timestampเวลาที่ sign เป็น unix milliseconds — เพี้ยนจากเวลา server ได้ไม่เกิน ±300 วินาที
X-Nonceค่าไม่ซ้ำต่อ request (แนะนำ UUIDv4) — ยิงซ้ำภายใน 6 นาทีถูกปฏิเสธ
X-Signaturehex( HMAC-SHA256(secret, canonical) ) ตามสูตรด้านล่าง

Headers เสริม

ไม่ถูกรวมใน canonical — sign เฉพาะตามสูตรด้านล่าง:

Headerใช้เมื่อ
X-Player-IPIP จริงของผู้เล่น — บังคับตอน POST /games/:id/launch · เส้นอื่นแนะนำให้ส่ง (ลง login history)
X-Player-UAUser-Agent ของผู้เล่น (optional — ลง login history)
X-Request-Idcorrelation id ของคุณ — echo กลับใน request_uuid
x-langth (default) หรือ en — ภาษาของ message

Canonical string

text
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

typescript
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"),
  };
}

กติกาบังคับ

  1. ทุก retry sign ใหม่ — เปลี่ยน timestamp + nonce ทุกครั้ง ห้ามส่งค่าเดิมซ้ำ
  2. X-Player-IP = IP จริงของผู้เล่น
  3. X-Timestamp ±300 วินาที

request ที่ sign ผิดตอบ HTTP 401:

Codeสาเหตุ
1019signature ไม่ตรง
1020timestamp เพี้ยน (เกิน ±300 วินาที)
1021nonce ซ้ำ (ยิงซ้ำใน 6 นาที)

ตัวอย่างการ sign request

ค่า demo ที่ใช้ทุกตัวอย่าง:

text
API_KEY    = ak_3f9a1c5e7b2d4f6a8c0e1b3d5f7a9c2e4d6f8a0b1c3d5e7f
API_SECRET = 5b0e5f3c2a9d4e8f7c6b5a49382716053c2b1a0f9e8d7c6b5a4938271605f4e3

sign แต่ละตัวอย่างแล้วต้องได้ 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"}

text
bodyHash    = 7398bfab1d434e577244f950baebd60ef61bf2c6e2c1f8ef71ec9513fdbdbe1d
X-Timestamp = 1783236600000
X-Nonce     = 9f2b7d1e-4a83-4c1f-b2ce-6d5a0e8f3c21
X-Signature = 8e039b28037162d5b09f50f79a2af6ab24086661ddad29a997d2b0d864d762a0

B — GET /profile/balance · ไม่มี body (empty-string hash) + Authorization: Bearer <token>

text
bodyHash    = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Timestamp = 1783236660000
X-Nonce     = c4d8a2f6-1b3e-4f7a-9c0d-2e5b8a1f6d43
X-Signature = f0f002b52554c371d87edb0f9687c5c269ad2f7e0cbf6b58a225ad7096e6a96a

C — POST /auth/verify-otp · body {"phone":"0812345678","otp_code":"123456"}

text
bodyHash    = 37b0f11f251d34d64a8bbc84cef2a340971b983001cb8025fd43c9ae746b7f06
X-Timestamp = 1783236720000
X-Nonce     = 7a1e9c3b-5d2f-4e8a-a6b4-0c9d8e7f2a15
X-Signature = 9ea09be21aefaa87339759b6c90a033572b41055d6f419830fcba386e3edc5b3

D — GET /transactions?page=1&limit=20 · query อยู่ใน canonical ด้วย

text
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_token15 นาที (expires_in = 900)แนบ Authorization: Bearer บนทุก endpoint รายสมาชิก
refresh_token7 วัน · ใช้ได้ครั้งเดียวขอคู่ใหม่ผ่าน POST /auth/refresh

กติกา session

  • single-use rotation — หาก request ขอ refresh token ใหม่แล้ว ค่าเดิมจะใช้งานไม่ได้แล้ว
  • 1 สมาชิก = 1 session — login จากที่ใหม่ทับ session เดิม refresh token ชุดเก่าใช้ไม่ได้อีก
  • POST /auth/logout revoke session ทันที — access token ที่ยังไม่หมดอายุถูกตัดผลด้วย (fast-revoke)

จัดการ token error

code เกี่ยวกับ token มี 3 ตัว:

Codeความหมาย
1001unauthorized (ไม่มี/ไม่ผ่าน token)
1006token หมดอายุ
1007token ไม่ถูกต้อง

เจอ code พวกนี้ → refresh หนึ่งครั้งแล้ว retry request เดิม ถ้า refresh ไม่ผ่านให้พาผู้เล่น login ใหม่

เมื่อลงชื่อเข้าใช้งานผิด

ลงชื่อเข้าใช้ผิด 5 ครั้งใน 15 นาที → บัญชีถูกล็อกชั่วคราว · เข้าใช้สำเร็จหนึ่งครั้งจะรีเซ็ตตัวนับ

CodeHTTPความหมาย
1016429บัญชีถูกล็อกชั่วคราว (ลงชื่อเข้าใช้ผิดเกินกำหนด)

Error Code

codeชื่อHTTPความหมาย
1018API_KEY_INVALID401key ไม่มีอยู่ / ถูกปิด / ถูก revoke
1019API_SIGNATURE_INVALID401signature ไม่ตรง — ไล่เช็คตามกติกาบังคับก่อน
1020API_TIMESTAMP_INVALID401timestamp เพี้ยนเกิน ±300s — เช็ค NTP
1021API_NONCE_REPLAYED401nonce ซ้ำ — ทุก request/retry ต้อง sign ใหม่
1022API_CHANNEL_FORBIDDEN403credential ใช้ช่องทางนี้ไม่ได้ (ยังไม่เปิด External Site / ไม่ใช่ agent TRANSFER / token สมาชิกไม่ใช่ของ agent คุณ)
1023API_IP_NOT_ALLOWED403IP ของ server คุณไม่อยู่ใน allowlist