---
title: "Xác thực"
description: "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 API sử dụng xác thực dựa trên HMAC để xác minh an toàn các request API của bạn."
url: https://docs.fystack.io/vi/authentication
last_updated: 2026-11-13
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# 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:

```bash
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:

```bash
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:

1. 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`
2. Ký chuỗi này bằng API secret của bạn với HMAC-SHA256
3. 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ữ:

```typescript
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}&timestamp=${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
}
```

```go
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/base64"
  "encoding/hex"
  "fmt"
  "strconv"
  "time"
)

func computeSignature(apiSecret string, method, path, timestamp, body string) string {
  // Build canonical string in fixed order
  message := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s",
    method, path, timestamp, body)

  // Create HMAC-SHA256
  h := hmac.New(sha256.New, []byte(apiSecret))
  h.Write([]byte(message))
  hexSig := hex.EncodeToString(h.Sum(nil))

  // Base64-encode the hex string
  return base64.StdEncoding.EncodeToString([]byte(hexSig))
}

// Usage
ts := strconv.FormatInt(time.Now().Unix(), 10)
signature := computeSignature(
  apiSecret, "GET",
  "/api/v1/workspaces/your_workspace_id/wallets",
  ts, "",
)
```

```php
<?php
function computeSignature($apiSecret, $method, $path, $timestamp, $body = '') {
  // Build canonical string in fixed order
  $message = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  // Create HMAC-SHA256 hex digest
  $hexSig = hash_hmac('sha256', $message, $apiSecret);

  // Base64-encode the hex string
  return base64_encode($hexSig);
}

// Usage
$timestamp = time();
$signature = computeSignature(
  $apiSecret, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  $timestamp, ''
);

$headers = [
  'ACCESS-API-KEY: ' . $apiKey,
  'ACCESS-TIMESTAMP: ' . $timestamp,
  'ACCESS-SIGN: ' . $signature
];
```

```python
import hmac
import hashlib
import time
import base64

def compute_signature(api_secret: str, method: str, path: str, timestamp: str, body: str = '') -> str:
  # Build canonical string in fixed order
  message = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  # Create HMAC-SHA256 hex digest
  hex_sig = hmac.new(
    api_secret.encode(),
    message.encode(),
    hashlib.sha256
  ).hexdigest()

  # Base64-encode the hex string
  return base64.b64encode(hex_sig.encode()).decode()

# Usage
timestamp = str(int(time.time()))
signature = compute_signature(
  api_secret, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp, ''
)

headers = {
  'ACCESS-API-KEY': api_key,
  'ACCESS-TIMESTAMP': timestamp,
  'ACCESS-SIGN': signature
}
```

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;

public static String computeSignature(String apiSecret, String method, String path, String timestamp, String body) throws Exception {
  // Build canonical string in fixed order
  String canonical = String.format("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body);

  // Create HMAC-SHA256
  Mac mac = Mac.getInstance("HmacSHA256");
  mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
  byte[] digest = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));

  // Hex-encode the digest
  StringBuilder hexDigest = new StringBuilder();
  for (byte b : digest) {
    hexDigest.append(String.format("%02x", b));
  }

  // Base64-encode the hex string
  return Base64.getEncoder().encodeToString(hexDigest.toString().getBytes(StandardCharsets.UTF_8));
}

// Usage
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String signature = computeSignature(
  apiSecret, "GET",
  "/api/v1/workspaces/your_workspace_id/wallets",
  timestamp, ""
);

Map<String, String> headers = Map.of(
  "ACCESS-API-KEY", apiKey,
  "ACCESS-TIMESTAMP", timestamp,
  "ACCESS-SIGN", signature
);
```

### Quy trình ký request

1. Tạo chuỗi chuẩn hóa theo thứ tự cố định: `method={METHOD}&path={PATH}&timestamp={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ây
   - `body`: Body của request dưới dạng chuỗi JSON, hoặc chuỗi rỗng đối với request GET

2. 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

3. Thêm các header bắt buộc:
   - `ACCESS-API-KEY`: Khóa API của bạn
   - `ACCESS-TIMESTAMP`: Timestamp đã dùng trong chữ ký
   - `ACCESS-SIGN`: Chữ ký đã mã hóa base64

> **Warning.** 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.

```javascript
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
})
```

```go
payload := map[string]string{"name": "my-integration"}
bodyBytes, _ := json.Marshal(payload) // serialize once
body := string(bodyBytes)

signature := computeSignature(apiSecret, "POST", path, timestamp, body)

req, _ := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) // same bytes
```

```python
import json
import requests

payload = {'name': 'my-integration'}
body = json.dumps(payload)  # serialize once

signature = compute_signature(api_secret, 'POST', path, timestamp, body)

# Pass data=body (the exact string you signed). Passing json=payload
# instead would have requests() serialize the dict itself, a second,
# independent call that isn't guaranteed to match what you signed
requests.post(url, data=body, headers={'Content-Type': 'application/json', **auth_headers})
```

```php
<?php
$payload = ['name' => 'my-integration'];
$body = json_encode($payload); // serialize once

$signature = computeSignature($apiSecret, 'POST', $path, $timestamp, $body);

curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // same string
```

```java
String body = objectMapper.writeValueAsString(payload); // serialize once

String signature = computeSignature(apiSecret, "POST", path, timestamp, body);

HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create(url))
  .POST(HttpRequest.BodyPublishers.ofString(body)) // same string
  .build();
```

## Ký bằng Ed25519

Nếu khóa API của bạn sử dụng [client key pair](https://docs.fystack.io/api-keys.md#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](https://docs.fystack.io/api-keys.md#client-key-pair). 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.

```javascript
const crypto = require('crypto');

function computeSignature(privateKeyPem, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${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
);
```

```go
import (
  "crypto/ed25519"
  "crypto/x509"
  "encoding/base64"
  "encoding/pem"
  "errors"
  "fmt"
)

func computeSignature(privateKeyPEM []byte, method, path, timestamp, body string) (string, error) {
  canonical := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body)

  block, _ := pem.Decode(privateKeyPEM)
  if block == nil {
    return "", errors.New("invalid PEM private key")
  }
  key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  if err != nil {
    return "", err
  }
  edKey, ok := key.(ed25519.PrivateKey)
  if !ok {
    return "", errors.New("not an Ed25519 private key")
  }

  // ed25519.Sign hashes internally, sign the canonical string directly
  signature := ed25519.Sign(edKey, []byte(canonical))
  return base64.StdEncoding.EncodeToString(signature), nil
}
```

```python
import time
import base64
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def compute_signature(private_key_pem: bytes, method: str, path: str, timestamp: str, body: str = '') -> str:
  # Build canonical string in fixed order
  canonical = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  private_key = load_pem_private_key(private_key_pem, password=None)
  # .sign() on an Ed25519PrivateKey signs the message directly, no digest argument
  signature = private_key.sign(canonical.encode())

  return base64.b64encode(signature).decode()

# Usage
timestamp = str(int(time.time()))
signature = compute_signature(
  private_key_pem, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
)
```

```php
<?php
// Requires PHP 8.4+ (Ed25519 key support landed in ext-openssl in 8.4)
function computeSignature($privateKeyPem, $method, $path, $timestamp, $body = '') {
  $canonical = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  $privateKey = openssl_pkey_get_private($privateKeyPem);
  // The digest argument is ignored for Ed25519 keys
  openssl_sign($canonical, $signature, $privateKey, 0);

  return base64_encode($signature);
}

// Usage
$timestamp = time();
$signature = computeSignature(
  $privateKeyPem, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  $timestamp
);
```

> **Warning.** 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](https://docs.fystack.io/api-keys.md#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](https://docs.fystack.io/api-keys.md#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.

> **Warning.** `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.

```javascript
import { KMSClient, SignCommand } from '@aws-sdk/client-kms'

async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${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')
}
```

```go
import (
  "context"
  "encoding/base64"
  "fmt"

  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/service/kms"
  "github.com/aws/aws-sdk-go-v2/service/kms/types"
)

func computeSignature(ctx context.Context, client *kms.Client, keyId, method, path, timestamp, body string) (string, error) {
  canonical := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body)

  out, err := client.Sign(ctx, &kms.SignInput{
    KeyId:            aws.String(keyId),
    Message:          []byte(canonical),
    MessageType:      types.MessageTypeRaw,
    SigningAlgorithm: types.SigningAlgorithmSpecEd25519Sha512,
  })
  if err != nil {
    return "", err
  }

  return base64.StdEncoding.EncodeToString(out.Signature), nil
}
```

```java
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.MessageType;
import software.amazon.awssdk.services.kms.model.SignRequest;
import software.amazon.awssdk.services.kms.model.SignResponse;
import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;
import java.util.Base64;

public String computeSignature(KmsClient kmsClient, String keyId, String method, String path, String timestamp, String body) {
  String canonical = String.format("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body);

  SignRequest request = SignRequest.builder()
    .keyId(keyId)
    .message(SdkBytes.fromUtf8String(canonical))
    .messageType(MessageType.RAW)
    .signingAlgorithm(SigningAlgorithmSpec.ED25519_SHA_512)
    .build();

  SignResponse response = kmsClient.sign(request);
  return Base64.getEncoder().encodeToString(response.signature().asByteArray());
}
```

```python
import base64

def compute_signature(kms_client, key_id: str, method: str, path: str, timestamp: str, body: str = '') -> str:
  canonical = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  response = kms_client.sign(
    KeyId=key_id,
    Message=canonical.encode(),
    MessageType='RAW',
    SigningAlgorithm='ED25519_SHA_512'
  )

  return base64.b64encode(response['Signature']).decode()
```

```php
<?php
use Aws\Kms\KmsClient;

function computeSignature(KmsClient $kms, string $keyId, string $method, string $path, string $timestamp, string $body = ''): string {
  $canonical = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  $result = $kms->sign([
    'KeyId' => $keyId,
    'Message' => $canonical,
    'MessageType' => 'RAW',
    'SigningAlgorithm' => 'ED25519_SHA_512',
  ]);

  // $result['Signature'] is already raw bytes, the SDK base64-decodes it for you
  return base64_encode($result['Signature']);
}
```

`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](https://docs.fystack.io/api-keys.md#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.
