Skip to content

Ký webhook HMAC-SHA256 ​

Webhook nội dung sâu hơn và sample code đầy đủ ở Webhooks chung → Ký HMAC-SHA256. Trang này tóm tắt nhanh để CRM kỹ thuật onboarding mới hiểu cơ chế ký + verify.

Vì sao cần ký HMAC ​

CRM nhận webhook từ Zorio gửi đến URL của bạn. Nếu KHÔNG ký:

  • Kẻ tấn công có thể giả Zorio → gửi event giả tới URL của bạn → CRM xử lý nhầm.
  • Payload có thể bị sửa giữa đường (man-in-the-middle).
  • Kẻ tấn công replay lại event hợp lệ đã bắt được.

HMAC-SHA256 giúp ngăn chặn cả ba tình huống trên.

Cơ chế ​

Lúc tạo webhook ​

Khi đăng ký webhook qua Admin Console, hệ thống sinh secret 32 ký tự alphanumeric. Bạn:

  1. Copy secret ngay (chỉ hiện 1 lần).
  2. Lưu vào biến môi trường (Environment Variable) của ứng dụng nhận webhook. (OMNISERVE_WEBHOOK_SECRET (hoặc NICECRM_WEBHOOK_SECRET)).
  3. KHÔNG hardcode trong code, KHÔNG commit vào git.

Mỗi event Zorio gửi ​

Zorio gửi POST request kèm headers:

X-Zorio-Event: pbx.call.hangup
X-Zorio-Timestamp: 1782706011
X-Zorio-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Zorio-Signature: sha256=ab9d7f4...
Content-Type: application/json; charset=utf-8

Cách Zorio tính signature ​

signature = "sha256=" + lowercase(hex(HMAC-SHA256(secret, raw_body)))
  • secret: secret bạn lưu lúc tạo webhook.
  • raw_body: bytes nguyên văn của body request (KHÔNG parse JSON trước).

Receiver verify như nào ​

4 bước ​

  1. Từ chối request nếu timestamp quá cũ. — nếu X-Zorio-Timestamp lệch quá 300 giây với now → reject 400.
  2. Tính lại signature với secret + raw body.
  3. So sánh bằng hàm timing-safe (constant-time). với X-Zorio-Signature — dùng API timing-safe của ngôn ngữ.
  4. Dedupe theo X-Zorio-Delivery UUID — khoá phân tán NX với TTL 24h.

Sample Node.js (tối giản) ​

js
const crypto = require('crypto');

function verifyZorioWebhook(req, secret) {
  // 1. Timestamp window
  const ts = parseInt(req.headers['x-zorio-timestamp'], 10);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) {
    throw new Error('Stale timestamp');
  }

  // 2. Recompute
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex');

  // 3. Timing-safe compare
  const sig = req.headers['x-zorio-signature'] || '';
  if (sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw new Error('Invalid signature');
  }
}

Sample PHP / Python / Go ​

Xem Mẫu ứng dụng nhận webhook đầy đủ 4 ngôn ngữ.

Pitfall thường gặp ​

❌ Parse JSON trước khi tính HMAC ​

js
// SAI — body đã bị parser thay đổi
app.use(express.json());
app.post('/webhook', (req, res) => {
  const expected = createHmac(secret, JSON.stringify(req.body)).digest('hex');
  // → Không khớp vì JSON.stringify khác raw bytes Zorio gửi
});

// ĐÚNG — dùng raw bytes
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = createHmac(secret, req.body).digest('hex');
});

❌ So == thay vì timing-safe ​

js
// SAI — có thể làm lộ thông tin signature qua Timing Attack
if (sig === expected) { ... }

// ĐÚNG
if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { ... }

❌ Không check timestamp ​

Không check timestamp → tấn công phát lại (Replay Attack). Kẻ tấn công bắt 1 event hợp lệ → gửi lại 1.000 lần cách 1 năm sau vẫn pass signature.

❌ Reverse proxy thay đổi nội dung body ​

Một số reverse proxy (nginx + module compress) hoặc Cloudflare có thể nén lại body → bytes khác. Tắt nén (compression) cho endpoint webhook. cho route webhook:

nginx
location /webhook {
    gzip off;
    proxy_pass http://app;
}

❌ Cấu hình TLS sai ​

Zorio CHỈ gửi đến URL HTTPS. Nếu cert TLS: Chứng chỉ tự ký (Self-signed) sẽ bị Zorio từ chối.

  • Expired → Zorio reject.
  • Hostname mismatch → Zorio reject.

Test: curl https://your-webhook-url/ từ ngoài internet, không lỗi cert.

Replay protection cấp 2 (optional) ​

Để bảo mật cao hơn, dedupe theo cả (timestamp + body hash) thay vì chỉ Delivery UUID:

js
const fingerprint = ts + ':' + sha256(rawBody);
const isNew = await redis.set(`zorio:fp:${fingerprint}`, '1', 'NX', 'EX', 600);
if (!isNew) return res.status(200).send('Duplicate fingerprint');

Lý do: nếu kẻ tấn công chặn được một request hợp lệ trong window 300s, vẫn không replay được vì fingerprint đã thấy.

Tài liệu liên quan ​

Cấp phép theo điều khoản sử dụng của Zorio.