การยืนยันตัวตนด้วย API Key (3 Header)
ภาพรวม
webhook-go ไม่ใช้ JWT เลย ซึ่งต่างจาก cms-api และ client-api ระบบยืนยันตัวตนที่นี่มีเพียงรูปแบบเดียว คือ api-key แบบ 3 header สำหรับ integration ประเภท B2B
| Header | ความหมาย |
|---|---|
x-client-id | รหัสระบุ client อ้างอิงตาราง api_client |
x-client-secret | secret ของ client เปรียบเทียบแบบ plaintext ตรง ๆ ใน SQL |
x-api-key | key ที่ระบุขอบเขตการเข้าถึง อ้างอิงตาราง api_key ซึ่งผูกกับ line_oa_id และ organization_id |
แนวคิดคือการแยก "คุณคือใคร" (client id และ secret) ออกจาก "คุณเข้าถึงอะไรได้" (api key) client หนึ่งรายสามารถถือ api_key ได้หลายใบ โดยแต่ละใบผูกกับ LINE OA คนละตัว
route ที่ถูกป้องกันด้วยกลไกนี้มีเพียง /api/audience และ /api/line-oa/verify รวม 4 route
ส่วน route ที่รับ event จาก LINE และ Chatwoot ทั้งหมดเปิดสาธารณะโดยเจตนา
Business Flow
ขั้น middleware (APIKeyAuth)
- อ่านค่าจาก 3 header หากขาดตัวใดตัวหนึ่งจะตอบ
401 "Missing credentials in headers"และหยุดทันที - หาก repository ไม่ถูก wire (กรณี boot โดยไม่มีฐานข้อมูล) จะตอบ
500เป็นการ fail closed ไม่ปล่อยผ่าน - เรียก
ValidateClientด้วยSELECT ... FROM api_client WHERE client_id=$1 AND client_secret=$2 AND status='active'- error จริงจากฐานข้อมูลต้องตอบ
500ห้าม collapse เป็น401ตามกฎ "ห้ามกลืน error" - ไม่พบ row จะตอบ
401 "Invalid client credentials"
- error จริงจากฐานข้อมูลต้องตอบ
- เมื่อสำเร็จ จะเก็บค่าลง gin context ได้แก่
apiClientId(เป็นint64จากapi_client.id) และapiKey(สตริงดิบจาก header) ซึ่งทำหน้าที่แทนClsServiceของ NestJS เดิม - เรียก
c.Next()เพื่อส่งต่อให้ handler
ขั้น service (resolve scope)
- handler ดึงค่าออกจาก context ด้วย
middleware.ApiClientID(c)และmiddleware.APIKey(c)หากไม่มีค่าแสดงว่า guard ถูก bypass จะตอบ401 - service เรียก
apikey.Repository.GetByClientAndKeyด้วยSELECT ... FROM api_key WHERE api_client_id=$1 AND key=$2 AND status='active' - ผลลัพธ์ที่ได้คือ
{id, lineOaId, organizationId}ซึ่งจะถูกใช้เป็น scope ของทุก query ต่อจากนี้
หากไม่พบ row ในขั้นตอนนี้ แต่ละ endpoint จัดการต่างกัน ซึ่งเป็น parity ตามต้นฉบับที่ไม่สม่ำเสมอ
GET /audience ตอบ 200 null, PUT ตอบ 500, DELETE ตอบ 400 "Step 1: ..."
ส่วน verify จะเดินหน้าต่อแล้วไปจบที่ 404
ข้อสังเกตด้านความปลอดภัย
client_secretถูกเก็บและเปรียบเทียบเป็น plaintext ในฐานข้อมูล ซึ่งเป็นการทำตามต้นฉบับ โดยเจตนา (ไฟล์internal/middleware/apikey.goมี comment กำกับว่า "plaintext compare — match source; no hashing") แพ็กเกจinternal/middleware/apikey_test.goและ helper bcrypt ของ template ยังคงอยู่ แต่ไม่ได้ถูกใช้ในเส้นทางจริง- ไม่มี rate limit เฉพาะสำหรับกรณี auth ล้มเหลว มีเพียง rate limit รวมต่อ IP
- ไม่มีกลไก expiry ของ api key ในโค้ดชุดนี้ มีเพียงคอลัมน์
status
ไฟล์และฟังก์ชันหลัก
| ไฟล์ | ของสำคัญ |
|---|---|
internal/middleware/apikey.go | APIKeyAuth(v ClientValidator) gin.HandlerFunc, ApiClientID(c), APIKey(c), const CtxKeyAPIClientID / CtxKeyAPIKey, interface ClientValidator |
internal/server/server.go | (*Deps).APIKeyAuth() — ผูก middleware เข้ากับ deps.APIClient |
internal/apiclient/repository.go | ValidateClient(ctx, clientID, clientSecret) |
internal/apiclient/entity.go | struct ApiClient |
internal/apikey/repository.go | GetByClientAndKey(ctx, apiClientID, key) |
internal/apikey/entity.go | struct ApiKey (ID, LineOaID, OrganizationID และอื่น ๆ), const StatusActive |
cmd/api/main.go | สร้าง repository ทั้งสองตัวเมื่อ a.SQL != nil |
cmd/api/wiring_test.go | ยืนยันว่า 4 route ถูก guard และ 3 route เปิดสาธารณะ |
สรุปการป้องกันรายเส้นทาง
| Route | Auth |
|---|---|
GET /api, GET /api/health | ไม่มี |
POST /api/line/:id, POST /api/Line/:id | ไม่มี |
POST /api/tracking | ไม่มี |
POST /api/mbox/callback/:oaHash | ไม่มี |
GET /api/audience | api-key |
PUT /api/audience/:id | api-key |
DELETE /api/audience/:id | api-key |
POST /api/line-oa/verify | api-key |
จุดเชื่อมต่อกับ Service อื่น
- Postgres — ตาราง
api_clientและapi_keyทั้งคู่ query ผ่าน pgx simple protocol โดย id เป็นint64คอลัมน์ที่เป็น nullable ใช้ pointer และ enumstatusbind เป็น string - cms-api — โมดูล
api-clientและapi-keyเป็นหน้าจอสำหรับออกและเพิกถอน credential เหล่านี้ - โดเมนฐานข้อมูลที่เกี่ยวข้อง:
api-client-key - ผู้ใช้งาน middleware นี้ ได้แก่ จัดการสมาชิก Audience และ ยืนยันสิทธิ์ LINE OA
- โครงสร้าง middleware chain ทั้งหมดอธิบายไว้ที่ แกนกลาง HTTP, Middleware Pipeline และการเชื่อมฐานข้อมูล