Skip to content

การยืนยันตัวตนด้วย HMAC-SHA256 (HMAC Authentication)

กระบวนการ Authentication ของ RawPush ออกแบบมาเพื่อความปลอดภัยสูงสุด โดยใช้การสร้าง Signature จาก Backend ของคุณด้วยอัลกอริทึม HMAC-SHA256 เพื่อยืนยันว่า Client ที่กำลังจะ connect WebSocket ได้รับอนุญาตจาก Backend ของคุณจริง


🔒 1. ทำไมถึงใช้ HMAC-SHA256?

  • ความปลอดภัยสูง: Secret Key ไม่ต้องถูกส่งข้าม Network แม้แต่ครั้งเดียว สิ่งที่ส่งคือ Signature ที่คำนวณจากข้อมูลผ่าน Secret Key เท่านั้น
  • ป้องกันการปลอมแปลง (Tamper-proof): หากมีใครแก้ไขข้อมูลใน Token ค่า Signature จะไม่ตรงกัน และระบบจะปฏิเสธทันที
  • ป้องกัน Replay Attack: ด้วยการแนบ ID ไม่ซ้ำ (JTI) ใน Payload และให้ RawPush เก็บไว้ แม้จะมีคนดักจับ Token เก่าไป ก็ไม่สามารถนำกลับมาใช้ซ้ำได้

🛠️ 2. ขั้นตอนการสร้าง Token ที่ฝั่ง Backend ของคุณ

เป้าหมายคือการสร้าง Token ส่งกลับไปให้ Client ที่ผ่านการ Login แล้ว เพื่อนำไปใช้ authenticate กับ WebSocket

โครงสร้างของ Token (Data Payload)

โครงสร้าง Token Payload (JSON) ที่ใช้สำหรับสร้าง Signature:

json
{
  "v": "v1",
  "purpose": "ws-auth",
  "sub": "usr_xxxxxx",
  "aud": "websocket",
  "iat": 1718000000000,
  "exp": 1718000600000,
  "jti": "random_unique_uuid"
}
  • v: เวอร์ชันของ Token (ใช้ "v1") - เผื่อรองรับการอัปเกรดโครงสร้างในอนาคต
  • purpose: จุดประสงค์การใช้งาน (ใช้ "ws-auth" เป็นมาตรฐาน)
  • sub: ย่อมาจาก Subject คือ User ID ของผู้ใช้ในระบบของคุณ
  • aud: ย่อมาจาก Audience (ใช้ "websocket" เป็นมาตรฐาน)
  • iat: ย่อมาจาก Issued At (Unix Timestamp เป็น มิลลิวินาที) คือเวลาที่สร้าง Token
  • exp: ย่อมาจาก Expiration (Unix Timestamp เป็น มิลลิวินาที) คือเวลาหมดอายุ แนะนำให้บวก 30-60 วินาที
  • jti: ย่อมาจาก JWT ID (Json Token ID) คือตัวอักษรสุ่มที่ไม่ซ้ำกัน (เช่น UUID) เพื่อป้องกัน Replay Attack แบบเจาะจง

ทำไมถึงแนะนำให้ Fix ค่า purpose และ aud ?

ทางเทคนิคแล้ว RawPush ไม่ได้บังคับ ว่าสองฟิลด์นี้จะต้องเป็นคำว่าอะไร (คุณจะใช้ชื่อแอปของคุณก็ได้) ระบบขอเพียงแค่ ส่งค่าเดิมให้ตรงกันทั้งฝั่ง Backend และ Client

แต่ในเชิง Security (Best Practice) หาก Backend ของคุณมีการใช้ Secret Key ตัวเดียวกันนี้ในการสร้าง Token สำหรับบริการอื่นๆ ด้วย การฮาร์ดโค้ด 2 ฟิลด์นี้ไว้ จะช่วยป้องกันการโจมตีข้ามระบบ (Cross-Service Replay Attack) ได้อย่างเด็ดขาด:

  • ถ้านำ Token ของระบบแชท ไปยิงใส่ระบบไฟแนนซ์: จะพังทันทีเพราะค่า aud ไม่ตรง
  • ถ้านำ Token ระบบใดๆ มายิงใส่ WebSocket: Gateway ของ RawPush จะปฏิเสธทันทีเพราะ purpose หรือ Payload ภายในไม่ตรง ทำให้ Signature Hash ผิดพลาด

💻 3. ตัวอย่างโค้ดสร้าง Signature (Backend Code)

นำค่าใน Payload มาต่อกันเป็น string 7 บรรทัด แล้ว sign ด้วย Secret Key (sk_...) ของคุณ

วิธีการต่อ String คั่นด้วย newline (\n) ในลำดับดังนี้: v\npurpose\nsub\naud\niat\nexp\njti

ตัวอย่าง: Node.js / TypeScript

typescript
import crypto from 'crypto';

// กำหนดตัวแปรสำคัญ
const secretKey = 'sk_your_secret_key'; // เก็บเป็น Environment variable เท่านั้น!
const userId = 'usr_123456';

// 1. กำหนดเวลาเป็น มิลลิวินาที 
const nowMs = Date.now();
const expMs = nowMs + (10 * 60 * 1000); // หมดอายุใน 10 นาที

// 2. ข้อมูล Token Payload
const tokenPayload = {
  v: "v1",
  purpose: "ws-auth",
  sub: userId,
  aud: "websocket",
  iat: nowMs,
  exp: expMs,
  jti: crypto.randomUUID()
};

// 3. เรียงข้อมูลก่อน sign (ใช้ newline \n คั่น)
const payloadString = `${tokenPayload.v}\n${tokenPayload.purpose}\n${tokenPayload.sub}\n${tokenPayload.aud}\n${tokenPayload.iat}\n${tokenPayload.exp}\n${tokenPayload.jti}`;

// 4. สร้าง Signature (HMAC-SHA256) และ encode เป็น HEX
const signature = crypto
  .createHmac('sha256', secretKey)
  .update(payloadString)
  .digest('hex');

// 5. ส่งค่านี้กลับไปให้ Frontend (Client) นำไปใช้งาน
const tokenForFrontend = {
  token: tokenPayload,
  signature: signature
};

// res.json(tokenForFrontend);

ตัวอย่าง: Python

python
import hmac
import hashlib
import time
import uuid

secret_key = 'sk_your_secret_key'
user_id = 'usr_123456'

# 1. กำหนดเวลาเป็น มิลลิวินาที
now_ms = int(time.time() * 1000)
exp_ms = now_ms + (10 * 60 * 1000)

token_payload = {
    "v": "v1",
    "purpose": "ws-auth",
    "sub": user_id,
    "aud": "websocket",
    "iat": now_ms,
    "exp": exp_ms,
    "jti": str(uuid.uuid4())
}

# 2. เรียงข้อมูลก่อน sign (Newline delimited)
payload_string = f"{token_payload['v']}\n{token_payload['purpose']}\n{token_payload['sub']}\n{token_payload['aud']}\n{token_payload['iat']}\n{token_payload['exp']}\n{token_payload['jti']}"

# 3: สร้าง Signature (HMAC-SHA256 Hex)
signature = hmac.new(
    secret_key.encode('utf-8'),
    msg=payload_string.encode('utf-8'),
    digestmod=hashlib.sha256
).hexdigest()

print({
    "token": token_payload,
    "signature": signature
})

ตัวอย่าง: Go

go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"time"

	"github.com/google/uuid"
)

func generateToken(userID, secretKey string) map[string]interface{} {
	nowMs := time.Now().UnixMilli()
	expMs := nowMs + (10 * 60 * 1000) // 10 นาที
	jti := uuid.New().String()

	token := map[string]interface{}{
		"v":       "v1",
		"purpose": "ws-auth",
		"sub":     userID,
		"aud":     "websocket",
		"iat":     nowMs,
		"exp":     expMs,
		"jti":     jti,
	}

	payloadString := fmt.Sprintf("%s\n%s\n%s\n%s\n%d\n%d\n%s",
		token["v"], token["purpose"], token["sub"], token["aud"], token["iat"], token["exp"], token["jti"])

	h := hmac.New(sha256.New, []byte(secretKey))
	h.Write([]byte(payloadString))
	signature := hex.EncodeToString(h.Sum(nil))

	return map[string]interface{}{
		"token":     token,
		"signature": signature,
	}
}

🚀 4. นำ Token ไปให้ Client ใช้งาน

เมื่อ Backend ส่ง token และ signature กลับมาให้ Frontend แล้ว Client แค่นำข้อมูล 2 ส่วนนี้แนบไปกับ Command auth หลัง connect WebSocket เป็นอันเสร็จ

ตัวอย่างใน Frontend (JavaScript):

javascript
ws.onopen = () => {
   ws.send(JSON.stringify({
     cmd: "auth",
     token: {
        v: "v1",
        purpose: "ws-auth",
        sub: "usr_123456",
        aud: "websocket",
        iat: 1718000000000,
        exp: 1718000600000,
        jti: "random_uuid"
     },
     signature: "d83c2a6f...." // Signature (HEX 64 หลัก) จาก Backend
   }));
};

Released under the MIT License.