Xác thực
Bạn cần xác thực các request của mình để truy cập bất kỳ endpoint nào trong Fystack API. Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách hoạt động của cơ chế xác thực. Fystack sử dụng xác thực dựa trên HMAC để xác minh an toàn tính xác thực của các request API.
Xác thực HMAC
Fystack API sử dụng hệ thống xác thực an toàn dựa trên HMAC, yêu cầu bạn ký các request bằng API secret của mình. Mỗi request cần ba header xác thực:
Required authentication headers
ACCESS-API-KEY: your_api_key
ACCESS-TIMESTAMP: current_unix_timestamp
ACCESS-SIGN: base64_encoded_hmac_signature
Chữ ký HMAC được tạo bằng cách kết hợp HTTP method, đường dẫn request, timestamp và body của request (nếu có), sau đó ký bằng API secret của bạn.
Ví dụ request với header xác thực
Dưới đây là ví dụ về cách gửi một request đã xác thực tới Fystack API:
Example authenticated request
curl https://api.fystack.io/api/v1/wallets \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature=="
Tính ACCESS-SIGN
Để tính chữ ký HMAC, bạn cần:
- Tạo một chuỗi chuẩn hóa (canonical string) từ các tham số request theo thứ tự cố định:
method,path,timestamp,body - Ký chuỗi này bằng API secret của bạn với HMAC-SHA256
- Mã hóa hex digest HMAC, sau đó mã hóa base64 chuỗi hex đó
Luôn giữ API secret an toàn và đặt lại nó nếu bạn nghi ngờ đã bị lộ. Dưới đây là cách tính chữ ký trong từng ngôn ngữ:
import CryptoJS from 'crypto-js'
function computeSignature(
apiSecret: string,
method: string,
path: string,
timestamp: string,
body: string = ''
): string {
// Build canonical string in exact order
const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`
// HMAC-SHA256 -> hex -> base64
const hexDigest = CryptoJS.HmacSHA256(canonical, apiSecret)
.toString(CryptoJS.enc.Hex)
return btoa(hexDigest)
}
// Example usage
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = computeSignature(
'your_api_secret', 'GET',
'/api/v1/workspaces/your_workspace_id/wallets',
timestamp
)
const headers = {
'ACCESS-API-KEY': 'your_api_key',
'ACCESS-TIMESTAMP': timestamp,
'ACCESS-SIGN': signature
}
Quy trình ký request
-
Tạo chuỗi chuẩn hóa theo thứ tự cố định:
method={METHOD}&path={PATH}×tamp={TIMESTAMP}&body={BODY}method: HTTP method viết hoa (GET, POST, v.v.)path: Đường dẫn request (ví dụ:/api/v1/workspaces/{id}/wallets)timestamp: UNIX timestamp tính bằng giâybody: Body của request dưới dạng chuỗi JSON, hoặc chuỗi rỗng đối với request GET
-
Tạo chữ ký HMAC-SHA256:
- Ký chuỗi chuẩn hóa bằng API secret của bạn
- Mã hóa hex digest
- Mã hóa base64 chuỗi hex
-
Thêm các header bắt buộc:
ACCESS-API-KEY: Khóa API của bạnACCESS-TIMESTAMP: Timestamp đã dùng trong chữ kýACCESS-SIGN: Chữ ký đã mã hóa base64
Hãy giữ API secret an toàn và không bao giờ chia sẻ nó. Khóa API có thể được chia sẻ nhưng secret phải luôn được giữ bí mật.
Tạo chuỗi body
body trong chuỗi chuẩn hóa phải là chính xác các byte bạn gửi đi, áp dụng cho cả HMAC và Ed25519. Với request GET, hoặc bất kỳ request nào không có body, đó là chuỗi rỗng, không bao giờ là "{}" hay null.
Lỗi cần tránh: tuần tự hóa payload hai lần, một lần để ký và một lần khi HTTP client gửi đi. Nếu nội dung bạn đã ký không khớp từng byte với nội dung server nhận được, ValidateAccessSign sẽ thất bại, ngay cả khi payload về mặt logic là cùng một JSON. Hãy tuần tự hóa một lần, sau đó dùng lại đúng chuỗi đó vừa để tạo chuỗi chuẩn hóa vừa làm body thực tế của request; đừng đưa object/dict/map gốc cho HTTP client rồi tin rằng một lần tuần tự hóa thứ hai, độc lập sẽ tái tạo chính xác. Tùy ngôn ngữ, lỗi này có thể xảy ra rõ ràng (một số HTTP client từ chối hoặc xử lý sai body không phải chuỗi) hoặc âm thầm dưới dạng lỗi không khớp chữ ký khó gỡ (nếu hai lần tuần tự hóa cho kết quả khác nhau, do nâng cấp thư viện, tùy chọn mặc định khác, hoặc các giá trị bất thường như NaN), vì vậy đừng dựa vào bất kỳ kết quả nào, chỉ cần không tuần tự hóa hai lần.
const payload = { name: 'my-integration' }
const body = JSON.stringify(payload) // serialize once
// sign() uses `body` in the canonical string, exactly as below
const signature = computeSignature(apiSecret, 'POST', path, timestamp, body)
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', /* ...auth headers */ },
// `fetch` requires body to already be a string (or Blob/ArrayBuffer/etc),
// it will not JSON-serialize `payload` for you, pass `body`, not `payload`
body
})
Ký bằng Ed25519
Nếu khóa API của bạn sử dụng client key pair thay vì HMAC secret, hãy ký cùng chuỗi chuẩn hóa đó bằng khóa riêng tư Ed25519 thay vì tính HMAC. Có hai điểm khác biệt so với luồng HMAC:
- Ký trực tiếp các byte của chuỗi chuẩn hóa, không có bước băm riêng, vì Ed25519 tự thực hiện việc đó bên trong.
- Mã hóa base64 ngay chữ ký thô 64 byte, không có bước hex.
ACCESS-SIGN = base64( Ed25519_Sign(privateKey, canonical_string) )
Nếu bạn đang dùng @fystack/sdk, hãy truyền một signer (LocalPrivateKeySigner hoặc AwsKmsSigner) vào credentials và SDK sẽ làm việc này cho bạn, xem Khóa API. Các ví dụ dưới đây dành cho việc ký không dùng SDK, trong các ngôn ngữ khác.
const crypto = require('crypto');
function computeSignature(privateKeyPem, method, path, timestamp, body = '') {
const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`;
const privateKey = crypto.createPrivateKey(privateKeyPem);
// No digest algorithm, Ed25519 signs the message directly
const signature = crypto.sign(null, Buffer.from(canonical), privateKey);
return signature.toString('base64');
}
// Usage
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = computeSignature(
process.env.PRIVATE_KEY_PEM, 'GET',
'/api/v1/workspaces/your_workspace_id/wallets',
timestamp
);
Không bao giờ gửi khóa riêng tư Ed25519 cho Fystack hoặc nhúng nó vào mã phía client/trình duyệt, chỉ khóa công khai được đăng ký. Với môi trường production, hãy giữ khóa trong kho lưu trữ dựa trên phần cứng như AWS KMS thay vì file PEM, xem Client Key Pair.
Ký qua AWS KMS (không dùng SDK)
Nếu bạn dùng @fystack/sdk trên Node.js, AwsKmsSigner đã làm việc này cho bạn, xem Client Key Pair. Với mọi backend khác, hoặc nếu bạn chỉ muốn tìm hiểu cơ chế, cách tiếp cận tương tự hoạt động với AWS SDK của bất kỳ ngôn ngữ nào: gọi KMS Sign với MessageType: RAW và SigningAlgorithm: ED25519_SHA_512 trên chuỗi chuẩn hóa thô, sau đó mã hóa base64 các byte chữ ký trả về. Đây chính xác là những gì AwsKmsSigner thực hiện bên trong; ví dụ Node.js bên dưới là cùng logic đó được viết ra trực tiếp.
MessageType phải là RAW, không phải DIGEST. Thuật toán ED25519_SHA_512 của KMS nhận thông điệp chưa băm và tự băm bên trong, khớp với cách Ed25519 hoạt động ở mọi nơi khác trên trang này. Truyền DIGEST sẽ ký một giá trị khác, không tương thích.
import { KMSClient, SignCommand } from '@aws-sdk/client-kms'
async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') {
const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`
const response = await kmsClient.send(new SignCommand({
KeyId: keyId,
Message: Buffer.from(canonical, 'utf8'),
MessageType: 'RAW',
SigningAlgorithm: 'ED25519_SHA_512'
}))
return Buffer.from(response.Signature).toString('base64')
}
kmsClient/client/$kms ở trên là bất kỳ KMS client nào mà AWS SDK của ngôn ngữ bạn dùng cung cấp, được cấu hình theo cách bạn vẫn thường làm. Xem Client Key Pair để biết mô hình chung giữa môi trường cục bộ và production (trỏ tới LocalStack/ministack với thông tin xác thực thử nghiệm tĩnh khi chạy cục bộ, bỏ cả hai trong production và để IAM role cung cấp thông tin xác thực); ý tưởng này áp dụng cho mọi ngôn ngữ, chỉ khác ở các tùy chọn cấu hình riêng của từng SDK.