身分驗證
InfraIO Pay 有兩個 API 介面,各自採用不同的驗證模型。請依照 呼叫者身份選擇:
| 介面 | 路徑前綴 | 對象 | 驗證 |
|---|---|---|---|
| 商家 B2B | /b2b/v1/* | 你的伺服器 | HMAC-SHA256 請求簽章 |
| 儀表板 | 依服務劃分:/auth/*、/payment/*、/merchant/*、/event/*、/user/* 等 | 商家儀表板的瀏覽器 session | Bearer JWT |
本頁介紹 B2B 介面 — 你從伺服器以 API 金鑰組呼叫的那個。若你 要嵌入 InfraIO 儀表板或打造內部工具,請使用儀表板介面(獨立的文件, 尚未公開)。
gateway 依前導前綴為每個介面路由,並在轉發前 剝除 該前綴:
/b2b/v1/checkout-sessions/quick 抵達 payment-service 時是
/v1/checkout-sessions/quick,而儀表板的 /payment/v1/orders 抵達時是
/v1/orders。所以若你在別處看到光禿禿的 /v1/* 路徑,那是公開前綴
被移除之後的 後端內部 路徑 — 你的用戶端始終送出帶前綴的形式。(對
簽章的一個影響:B2B canonical 字串簽章的路徑 仍帶著 /b2b 前綴
— 見下文。)
Endpoints
| 環境 | Base URL |
|---|---|
| Test | https://api-dev.infraio.xyz |
| Live | https://api.infraio.xyz |
URL 模式相同 — 環境是由金鑰前綴(pk_test_… vs pk_live_…)
控制,而非 URL。
金鑰組
你會從商家儀表板取得兩個值(Developers → API keys → + Add key):
- Publishable key(
pk_test_…或pk_live_…)— 識別你的帳號。 以X-Client-ID傳遞。可以安全地嵌入瀏覽器 bundle(SDK 已經這樣做)。 - Secret key(
sk_test_…或sk_live_…)— HMAC 簽章金鑰。 僅限伺服端使用。視同資料庫密碼般保護。
若 secret key 不慎流入瀏覽器 bundle、git repo、log 或聊天室 — 立刻從儀表板撤銷它。沒有重疊視窗;撤銷是即時的。請發行新金鑰 並重新部署。
簽章請求
每個對 /b2b/v1/* 的呼叫都會帶三個標頭:
X-Client-ID: pk_live_70d0becbd7a53047b0dfe88a960d10ad
X-Timestamp: 1715990400
X-Signature: 9a8b7c6d… (hex HMAC-SHA256)簽章是針對 canonical string 計算的:
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODYMETHOD— 大寫的 HTTP verb(POST、GET等)。PATH— 請求路徑含/b2b前綴,不含 host 且不含 query string (例如/b2b/v1/checkout-sessions/quick)。Gateway 在剝除/b2b之前就會以原始進來的路徑驗章,因此前綴必須存在。Query 參數 不參與簽章 — 對於GET …?cursor=…&limit=20,只簽路徑,不要 簽?…部分。TIMESTAMP— Unix 秒,以十進位字串表達(例如"1715990400"), 須與X-Timestamp完全一致。BODY— 原始請求 body 位元組。GET/DELETE為空字串。
以 secret key 做 HMAC-SHA256,輸出 hex:
Node / TS
import { createHmac } from "node:crypto";
function sign({ method, path, body, secret }: {
method: string; path: string; body: string; secret: string;
}) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const input = [method.toUpperCase(), path, timestamp, body].join("\n");
const signature = createHmac("sha256", secret).update(input).digest("hex");
return { timestamp, signature };
}為什麼是 HMAC,而非 Bearer?
純 Bearer token 的 API 會在每次請求都把你唯一的 secret 上線傳輸。 任何拿到 TLS 終止點代理 log 的人都能取得你帳號的金鑰。HMAC 簽章 代表 secret 永遠不會旅行 — 只傳遞它派生出的簽章,而該簽章是一次性 的(綁定該確切的請求 + 該確切的分鐘)。
代價是:每次呼叫都要計算簽章。Server SDK 會把這部分隱藏起來; 在我們發佈之前,上面的輔助程式碼每種語言約 15 行。
Timestamp 容差
綁定的容差是 ±5 分鐘(300 秒),由 merchant-service 在驗證簽章
時強制執行。Gateway 本身略寬鬆一些(310 秒)作為縱深防禦,但只要
通過 gateway 卻被內層檢查擋下,仍會回 401 invalid_signature —
請以 300 秒作為契約。兩項影響:
- 同步你的伺服器時鐘(使用 NTP)。長時間執行的 cron,若時鐘 漂移,會間歇性失敗。
- 不要預先計算並排隊簽章。 若一個請求在 retry queue 中停留 >5 分鐘,它的簽章會過期。
Key scopes
Secret key 帶有一或多個下列 scope 組合:
| Scope | 預期用途 |
|---|---|
read | 列出 / 讀取訂單、session、退款 |
write_order | 建立結帳 session、訂單 |
write_refund | 發起退款、鑄造退款申請 token |
webhook_manage | 建立 / 更新 / 刪除 webhook endpoint |
儀表板預設發行「full access」金鑰(全部四個 scope)。你可以從 Developers → API keys → + Add key 鑄造受限 scope 金鑰,並只 勾選該整合所需的 scope。
Scope 強制執行目前僅供諮詢,尚未強制阻擋。 Scope 會記錄於
金鑰上、並在儀表板回顯給你,但 gateway middleware 目前還不會
拒絕超出 scope 的呼叫 — 今天任何有效的 sk_… 金鑰都等同於 full
access。逐 endpoint 的 scope gating 排在下一個版本。請暫時不要把
scope 當成安全邊界;先把它視為標籤,並透過輪替 / 撤銷金鑰來限制
存取。
簽章在哪裡被驗證
HMAC 驗證在 gateway 上發生一次。Gateway:
- 讀取
X-Client-ID、X-Timestamp、X-Signature。 - 以
pk_…查找商家 + secret,執行 timestamp 視窗檢查,重新計算 簽章,以 constant-time 比對。 - 成功時剝除驗證標頭,為請求蓋上內部標頭(
X-B2B-Auth: 1、X-Merchant-ID、X-Merchant-Domain)後轉送到下游服務 (payment-service、merchant-service 等)。環境與已解析的 scope 目前不會注入到 header — 需要環境資訊的下游程式碼是從請求 body / 每商家的設定推導,而非從 header。 - 失敗時直接回 401
INVALID_SIGNATURE,不會碰到後端。
下游服務不會重跑 HMAC — 它們信任 gateway 注入的標頭,並以
gateway 解析出的商家身份運作。它們也不會逐 endpoint 做 scope-gate:
如上所述,金鑰的 scope 不會被注入,因此任何通過驗證的 sk_… 都能
抵達其商家的任意 endpoint(scope 強制執行目前僅供諮詢 — 見
Key scopes 下的提示框)。這帶來兩個重點:
- 若你在 InfraIO Pay 前面運作自己的反向代理,請勿剝除
X-B2B-Auth/X-Merchant-ID(也別偽造它們 — gateway 會在公開邊緣拒絕帶有 這些標頭的入站請求)。 - 公開網路路徑(
/b2b/v1/*)是唯一執行 HMAC 步驟的介面。我們服務 之間的內部 gRPC 使用 mTLS — 那是一種不同的信任模型,且不接受X-Client-ID。
下一步
- 錯誤 — 4xx/5xx 的回應格式。
- 安全 → API 金鑰 — 輪替、撤銷,以及 secret 外洩時該怎麼做。
- Webhooks → 簽章驗證
— 使用不同的 HMAC 機制(標頭
X-Signature: sha256=…,簽X-Timestamp + "." + raw_body,加上 24 小時輪替寬限視窗內的選用X-Signature-Prev)。請別把兩套機制混用 — 雖然 hash 演算法相同, 但所簽位元組與 secret 系列(whsec_…vssk_…)都不一樣。