Tài liệu tích hợp dành cho Partner.
Mỗi callback POST gửi đến Partner đều kèm theo các header sau:
| Header | Kiểu | Mô tả |
|---|---|---|
| timestamp | string | Unix Epoch Milliseconds (UTC) tại thời điểm gửi. |
| nonce | string | Chuỗi ngẫu nhiên 16 ký tự, duy nhất cho mỗi request. Dùng để chống replay attack thay cho time window. |
| signature | string | HMAC-SHA256 của signingString, encode Hex lowercase (64 ký tự). |
Ghép 3 giá trị lại với nhau, dùng ký tự | làm separator:
signingString = clientId + "|" + apiKey + "|" + timestamp + "|" + nonce
Sau đó tính HMAC-SHA256 với secretKey làm key mã hoá, encode kết quả sang Hex lowercase:
signature = HMAC-SHA256(key = secretKey, message = signingString) // hex lowercase
signingString. Partner không cần đọc hay xử lý
body để verify.
| Thành phần | Lấy từ | Ví dụ |
|---|---|---|
| clientId | Do hệ thống cấp riêng cho từng Partner | partner_abc123 |
| timestamp | Header timestamp | 1750000000000 |
| nonce | Header nonce | a3f9b2c1d4e5f607 |
| apiKey | Do hệ thống cấp riêng cho từng Partner | sk_live_xxxxx |
| secretKey | Do hệ thống cấp riêng cho từng Partner (dùng để mã hoá) | secret_live_xxxxx |
Giả sử Partner nhận được POST request với:
timestamp: 1750000000000
nonce: a3f9b2c1d4e5f607
signature: 7f4a2c9e1d3b...
Body: {...} // bất kỳ — không dùng để verify
Partner tự tính lại signature như sau:
// Bước 1: Tạo signingString
signingString = "partner_abc123|sk_live_xxxxxxxxxxxxxxxxxxxx|1750000000000|a3f9b2c1d4e5f607"
// Bước 2: Tính HMAC-SHA256
secretKey = "sk_live_xxxxxxxxxxxxxxxxxxxx" // lấy từ config
signature = HMAC-SHA256(secretKey, signingString)
= "7f4a2c9e1d3b..." // 64 ký tự hex
// Bước 3: So sánh với X-Signature nhận được
"7f4a2c9e1d3b..." === "7f4a2c9e1d3b..." → HỢP LỆ → trả 200
Thực hiện đúng thứ tự 3 bước. Fail bất kỳ bước nào → trả 401 ngay, không xử lý tiếp.
| # | Kiểm tra | Điều kiện fail → 401 |
|---|---|---|
| 1 | Timestamp | |now_ms − X-Timestamp| > 300,000 ms (5 phút) |
| 2 | Nonce | nonce đã thấy trong 10 phút vừa qua (lưu cache/DB theo clientId) |
| 3 | Signature | Tính lại signature ≠ signature (so sánh constant-time) |
key = "clientId:nonce", TTL = 10 phút. Dùng Redis
SET NX EX hoặc DB unique constraint. Nếu SET NX trả false (key đã tồn tại) → nonce đã
dùng → 401.
const crypto = require('crypto');
const CREDENTIALS_STORE = {
MYF88APP: {
clientId: 'MYF88APP',
apiKey: '72bf2963-f1e4-4742-9089-3f08ed860325',
secretKey: 'c6ba90ed558f0c1b28b3d69529bcf886f5b56c85cf512f4e9aef1b65b7336ba6',
},
};
async function verifyWebhook(req) {
const timestamp = req.headers['timestamp'];
const nonce = req.headers['nonce'];
const signature = req.headers['signature'];
if (!timestamp || !nonce || !signature) {
console.log('[verifyWebhook] missing field');
return false;
}
// Timestamp anti-replay (±5 phút)
if (Math.abs(Date.now() - Number(timestamp)) > 300_000) {
console.log('[verifyWebhook] timestamp out of range');
return false;
}
// Lookup credentials theo clientId
const credentials = CREDENTIALS_STORE['MYF88APP'];
if (!credentials) {
console.log('[verifyWebhook] unknown clientId');
return false;
}
// Verify signature
const signingString = `${credentials.clientId}|${credentials.apiKey}|${timestamp}|${nonce}`;
const expected = crypto.createHmac('sha256', credentials.secretKey)
.update(signingString, 'utf8')
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
const signatureBuf = Buffer.from(signature, 'hex');
if (expectedBuf.length !== signatureBuf.length) {
console.log('[verifyWebhook] signature length mismatch');
return false;
}
const valid = crypto.timingSafeEqual(expectedBuf, signatureBuf);
if (!valid) {
console.log('[verifyWebhook] signature mismatch, expected:', expected);
return false;
}
return true;
}
function generateWebhookAuth(credentials) {
if (!credentials || !credentials.clientId || !credentials.apiKey || !credentials.secretKey) {
return {};
}
const timestamp = Date.now().toString();
// Tạo nonce ngẫu nhiên 16 ký tự hex
const nonce = crypto.randomBytes(8).toString('hex');
// Tạo chuỗi ký
const signingString = `${credentials.clientId}|${credentials.apiKey}|${timestamp}|${nonce}`;
// Tính HMAC-SHA256 và chuyển sang dạng hex lowercase
const signature = crypto.createHmac('sha256', credentials.secretKey)
.update(signingString, 'utf8')
.digest('hex');
return {
timestamp,
nonce,
signature
};
}
Partner phải trả về đúng HTTP status code theo bảng sau:
| Status | Khi nào | Hành vi hệ thống |
|---|---|---|
| 200 OK | Xác thực OK, xử lý thành công | Ghi nhận. Không retry. |
| 401 | Xác thực thất bại | Ghi log. Retry có giới hạn. |
| 500 | Lỗi nội bộ phía Partner | Retry exponential backoff. |
| Triệu chứng | Nguyên nhân | Fix |
|---|---|---|
| Signature luôn sai dù key đúng | Dùng sai separator hoặc sai thứ tự | Đảm bảo đúng: clientId|apiKey|timestamp|nonce (ký tự | ASCII 0x7C) |
| Signature đúng nhưng vẫn 401 | Timestamp lệch > 5 phút | Sync NTP, đảm bảo server time chính xác |
| Nonce bị reject liên tục | TTL cache quá ngắn hoặc nonce bị trùng khi retry | Tăng TTL lên 10 phút; hệ thống không retry với cùng nonce |
| Signature sai khi có ký tự đặc biệt | Encoding không nhất quán | Dùng UTF-8 cho toàn bộ chuỗi khi tính HMAC |
timingSafeEqual / hash_equals / MessageDigest.isEqual) — không dùng
== hay === để tránh Timing Attack.