Skip to main content

hmac-authentication-new

# 🔐 HMAC Authentication for API Security

Welcome to the complete guide for **HMAC Authentication** in the Token City Blockchain API.

This workflow ensures that all API requests and webhook notifications are authenticated and verified, protecting against replay attacks, tampering, and unauthorized access.

🌟 **Objective**: Implement secure HMAC-SHA256 signature-based authentication for API requests and webhook validation.

---

## 📚 OpenAPI Documentation

- [Swagger UI Token City Blockchain API](../api-1.8.1/blockchain-api-token-city)

---

## ✨ Workflow Summary

HMAC authentication is used in two scenarios:

1. 🔹 **Client → API**: Sign outgoing requests to Token City API with HMAC signature
2. 🔹 **API → Client**: Validate incoming webhooks from Token City using HMAC signature
3. 🔹 **Key Management**: Securely store and rotate HMAC credentials
4. 🔹 **Error Handling**: Debug signature validation failures

---

## 📋 HMAC Authentication Components

| Component | Description | Format/Example |
| :--------------- | :----------------------------------------------------- | :---------------------------------------------- |
| HMAC Key ID | Public identifier for your API credentials | `TC-API-CLIENT-abc123def456` |
| HMAC Secret Key | Private key used to generate signatures (never expose) | `sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6` |
| Signature Header | HTTP header containing the signature | `Signature: TC sha256 base64EncodedSignature==` |
| Date Header | RFC 1123 formatted timestamp | `Date: Tue, 10 Jun 2025 14:17:50 GMT` |
| Authorization | Header containing HMAC Key ID | `Authorization: TC-API-CLIENT-abc123def456` |
| Algorithm | Cryptographic hash function | `HMAC-SHA256` |
| Payload Hash | SHA256 hash of request body | Base64 encoded |
| String to Sign | Canonical request string used for signature | `METHOD\nPATH\nQUERY\nHEADERS\nBODY_HASH` |

---

## 🎨 Workflow Visualizations

### 🌊 Mermaid Sequence Diagram - Client Request Authentication

```mermaid
sequenceDiagram
participant Client
participant API Gateway
participant Token City API

Client->>Client: Generate Date header (RFC 1123)
Client->>Client: Hash request body (SHA256)
Client->>Client: Build canonical string
Client->>Client: Sign with HMAC-SHA256 + Secret Key
Client->>API Gateway: POST /api/endpoint<br/>Headers: Signature, Date, Authorization

API Gateway->>API Gateway: Extract signature from header
API Gateway->>API Gateway: Rebuild canonical string
API Gateway->>API Gateway: Compute expected signature

alt Signature Valid
API Gateway->>Token City API: Forward request
Token City API-->>API Gateway: Response
API Gateway-->>Client: (200) Success
else Signature Invalid
API Gateway-->>Client: (401) Unauthorized - Invalid signature
end
```

### 🌊 Mermaid Sequence Diagram - Webhook Validation

```mermaid
sequenceDiagram
participant Token City API
participant Your Webhook
participant Your Backend

Token City API->>Token City API: Transaction completed
Token City API->>Token City API: Generate Date header
Token City API->>Token City API: Hash webhook payload (SHA256)
Token City API->>Token City API: Sign with HMAC-SHA256
Token City API->>Your Webhook: POST /webhook<br/>Headers: Signature, Date, Authorization

Your Webhook->>Your Webhook: Extract signature from header
Your Webhook->>Your Webhook: Rebuild canonical string
Your Webhook->>Your Webhook: Compute expected signature

alt Signature Valid
Your Webhook->>Your Backend: Process webhook event
Your Backend-->>Your Webhook: Success
Your Webhook-->>Token City API: (200) OK
else Signature Invalid
Your Webhook-->>Token City API: (401) Unauthorized
end
```

---

## 💡 Key Concepts

### What is HMAC?

**HMAC (Hash-based Message Authentication Code)** is a cryptographic mechanism that:

- Verifies message **integrity** (data hasn't been tampered)
- Verifies message **authenticity** (sender is who they claim to be)
- Prevents **replay attacks** (same request can't be reused)
- Uses **shared secret key** known only to sender and receiver

### HMAC-SHA256 Algorithm

The Token City API uses HMAC-SHA256:

- **HMAC**: Authentication code algorithm
- **SHA256**: 256-bit Secure Hash Algorithm
- **Output**: Base64-encoded 256-bit signature

### Canonical String Format

The string that gets signed includes:

```
METHOD\n
PATH\n
QUERY_PARAMS\n
HEADERS\n
BODY_HASH
```

**Example:**

```
POST
/v2/erc3643/deploy
chainId=polygon&network=mainnet
authorization:TC-API-CLIENT-abc123
date:Tue, 10 Jun 2025 14:17:50 GMT
a3f8c7b2e1d9f4a6c5b8e7d2f1a9c8b6e5d4f3a2c1b9e8d7f6a5c4b3e2d1f0
```

### Security Properties

1. **Non-repudiation**: Sender cannot deny sending the message
2. **Integrity**: Modifications invalidate the signature
3. **Authenticity**: Only holder of secret key can create valid signature
4. **Replay Protection**: Date header prevents reuse of old requests
5. **Confidentiality**: Secret key never transmitted

---

## 📝 Step-by-Step Implementation

### Step 1: Obtain HMAC Credentials

Contact Token City support to receive your API credentials:

**Credentials Received:**

```json
{
"hmacKeyId": "TC-API-CLIENT-abc123def456",
"hmacSecretKey": "sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"environment": "production",
"createdAt": "2025-01-15T10:30:00Z",
"expiresAt": null
}
```

**Storage Best Practices:**

- Store `hmacSecretKey` in environment variables or secure vault
- Never commit secret key to version control
- Use separate credentials for development and production
- Rotate keys periodically (recommended: every 90 days)

---

### Step 2: Generate HMAC Signature for Outgoing Request

When making API requests to Token City, compute and include the HMAC signature.

**TypeScript/JavaScript Implementation:**

```typescript
import crypto from "crypto";

interface HmacHeaders {
signature: string;
date: string;
authorization: string;
}

function calculateHmacSignature(
hmacKeyId: string,
hmacSecretKey: string,
date: string,
url: string,
payload: string,
method: string = "POST",
queryParams?: Record<string, string>
): HmacHeaders {
// 1. Encode query parameters (sorted alphabetically)
let encodedQueryParams = "";
if (queryParams) {
const sortedParams = Object.entries(queryParams)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
.map(([key, value]) => `${key.toLowerCase()}=${value.trim()}`)
.join("&");
encodedQueryParams = sortedParams;
}

// 2. Encode headers (sorted alphabetically)
const headers = [
{ key: "authorization", value: hmacKeyId },
{ key: "date", value: date },
];
const encodedHeaders = headers
.map((h) => `${h.key.toLowerCase()}:${h.value.trim()}`)
.join("\n");

// 3. Hash the request body (SHA256)
const contentHash = crypto
.createHash("sha256")
.update(payload, "utf8")
.digest("hex");

// 4. Build canonical string
const urlPath = new URL(url).pathname;
const canonicalString = [
method,
urlPath,
encodedQueryParams,
encodedHeaders,
contentHash,
].join("\n");

console.log("Canonical String:", canonicalString);

// 5. Sign with HMAC-SHA256
const signature = crypto
.createHmac("sha256", hmacSecretKey)
.update(canonicalString, "utf8")
.digest("base64");

return {
signature: `TC sha256 ${signature}`,
date: date,
authorization: hmacKeyId,
};
}

// Usage Example
const hmacKeyId = "TC-API-CLIENT-abc123def456";
const hmacSecretKey = "sk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6";
const date = new Date().toUTCString(); // RFC 1123 format
const url = "https://api.tokencity.com/v2/erc3643/deploy";
const payload = JSON.stringify({
name: "Green Bond Token",
symbol: "GBT",
decimals: 0,
erir: "0xErirAddress...",
issuer: "0xIssuerAddress...",
operator: "0xOperatorAddress...",
maxSupply: "1000000",
modules: ["kyc", "aml", "compliance"],
});

const headers = calculateHmacSignature(
hmacKeyId,
hmacSecretKey,
date,
url,
payload,
"POST",
{ chainId: "polygon", network: "mainnet" }
);

console.log("Headers to include:", headers);
// Output:
// {
// signature: 'TC sha256 a3f8c7b2e1d9f4a6c5b8e7d2f1a9c8b6e5d4f3a2c1b9e8==',
// date: 'Tue, 10 Jun 2025 14:17:50 GMT',
// authorization: 'TC-API-CLIENT-abc123def456'
// }
```

**HTTP Request with HMAC:**

```http
POST /v2/erc3643/deploy?chainId=polygon&network=mainnet HTTP/1.1
Host: api.tokencity.com
Content-Type: application/json
Signature: TC sha256 a3f8c7b2e1d9f4a6c5b8e7d2f1a9c8b6e5d4f3a2c1b9e8==
Date: Tue, 10 Jun 2025 14:17:50 GMT
Authorization: TC-API-CLIENT-abc123def456

{
"name": "Green Bond Token",
"symbol": "GBT",
"decimals": 0,
"erir": "0xErirAddress...",
"issuer": "0xIssuerAddress...",
"operator": "0xOperatorAddress...",
"maxSupply": "1000000",
"modules": ["kyc", "aml", "compliance"]
}
```

---

### Step 3: Validate HMAC Signature for Incoming Webhook

When receiving webhooks from Token City, validate the signature to ensure authenticity.

**Node.js/Express Webhook Handler:**

```typescript
import express from "express";
import crypto from "crypto";

const app = express();

// Store raw body for signature validation
app.use(
express.json({
verify: (req, res, buf) => {
(req as any).rawBody = buf.toString("utf8");
},
})
);

function validateHmacSignature(
receivedSignature: string,
receivedDate: string,
receivedAuth: string,
hmacSecretKey: string,
requestPath: string,
requestMethod: string,
rawBody: string
): boolean {
// 1. Extract base64 signature from header
const signatureMatch = receivedSignature.match(/TC sha256 (.+)/);
if (!signatureMatch) {
console.error("Invalid signature format");
return false;
}
const providedSignature = signatureMatch[1];

// 2. Check date freshness (prevent replay attacks)
const requestDate = new Date(receivedDate);
const now = new Date();
const timeDiff = Math.abs(now.getTime() - requestDate.getTime()) / 1000; // seconds
if (timeDiff > 300) {
// 5 minutes tolerance
console.error("Request too old or too far in future");
return false;
}

// 3. Encode headers
const headers = [
{ key: "authorization", value: receivedAuth },
{ key: "date", value: receivedDate },
];
const encodedHeaders = headers
.map((h) => `${h.key.toLowerCase()}:${h.value.trim()}`)
.join("\n");

// 4. Hash the body
const contentHash = crypto
.createHash("sha256")
.update(rawBody, "utf8")
.digest("hex");

// 5. Build canonical string
const canonicalString = [
requestMethod,
requestPath,
"", // No query params in webhook
encodedHeaders,
contentHash,
].join("\n");

console.log("Canonical String:", canonicalString);

// 6. Compute expected signature
const expectedSignature = crypto
.createHmac("sha256", hmacSecretKey)
.update(canonicalString, "utf8")
.digest("base64");

// 7. Compare signatures (constant-time comparison)
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);

if (!isValid) {
console.error("Signature mismatch");
console.error("Expected:", expectedSignature);
console.error("Received:", providedSignature);
}

return isValid;
}

// Webhook endpoint
app.post("/webhook/tokencity", (req, res) => {
const signature = req.headers["signature"] as string;
const date = req.headers["date"] as string;
const authorization = req.headers["authorization"] as string;
const rawBody = (req as any).rawBody;

// Validate required headers
if (!signature || !date || !authorization) {
return res.status(401).json({ error: "Missing HMAC headers" });
}

// Validate signature
const hmacSecretKey = process.env.HMAC_SECRET_KEY!;
const isValid = validateHmacSignature(
signature,
date,
authorization,
hmacSecretKey,
req.path,
req.method,
rawBody
);

if (!isValid) {
return res.status(401).json({ error: "Invalid HMAC signature" });
}

// Process webhook
const event = req.body;
console.log("Valid webhook received:", event.type);

if (event.type === "TX_MINED") {
// Handle successful transaction
console.log("Transaction mined:", event.event.txHash);
} else if (event.type === "TX_FAILED") {
// Handle failed transaction
console.log("Transaction failed:", event.event.reason);
}

res.status(200).json({ status: "received" });
});

app.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});
```

**Python/Flask Webhook Handler:**

```python
import hmac
import hashlib
import base64
from datetime import datetime, timedelta
from flask import Flask, request, jsonify

app = Flask(__name__)

def validate_hmac_signature(
received_signature: str,
received_date: str,
received_auth: str,
hmac_secret_key: str,
request_path: str,
request_method: str,
raw_body: str
) -> bool:
# 1. Extract base64 signature
if not received_signature.startswith('TC sha256 '):
print('Invalid signature format')
return False
provided_signature = received_signature[10:] # Remove 'TC sha256 '

# 2. Check date freshness
try:
request_date = datetime.strptime(received_date, '%a, %d %b %Y %H:%M:%S %Z')
now = datetime.utcnow()
time_diff = abs((now - request_date).total_seconds())
if time_diff > 300: # 5 minutes
print('Request too old')
return False
except ValueError:
print('Invalid date format')
return False

# 3. Encode headers
encoded_headers = f"authorization:{received_auth.strip()}\ndate:{received_date.strip()}"

# 4. Hash body
content_hash = hashlib.sha256(raw_body.encode('utf-8')).hexdigest()

# 5. Build canonical string
canonical_string = f"{request_method}\n{request_path}\n\n{encoded_headers}\n{content_hash}"
print(f'Canonical String: {canonical_string}')

# 6. Compute expected signature
expected_signature = base64.b64encode(
hmac.new(
hmac_secret_key.encode('utf-8'),
canonical_string.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8')

# 7. Compare signatures
is_valid = hmac.compare_digest(expected_signature, provided_signature)

if not is_valid:
print(f'Signature mismatch')
print(f'Expected: {expected_signature}')
print(f'Received: {provided_signature}')

return is_valid

@app.route('/webhook/tokencity', methods=['POST'])
def webhook():
signature = request.headers.get('Signature')
date = request.headers.get('Date')
authorization = request.headers.get('Authorization')
raw_body = request.get_data(as_text=True)

# Validate headers
if not signature or not date or not authorization:
return jsonify({'error': 'Missing HMAC headers'}), 401

# Validate signature
import os
hmac_secret_key = os.getenv('HMAC_SECRET_KEY')
is_valid = validate_hmac_signature(
signature,
date,
authorization,
hmac_secret_key,
request.path,
request.method,
raw_body
)

if not is_valid:
return jsonify({'error': 'Invalid HMAC signature'}), 401

# Process webhook
event = request.json
print(f"Valid webhook received: {event['type']}")

if event['type'] == 'TX_MINED':
print(f"Transaction mined: {event['event']['txHash']}")
elif event['type'] == 'TX_FAILED':
print(f"Transaction failed: {event['event']['reason']}")

return jsonify({'status': 'received'}), 200

if __name__ == '__main__':
app.run(port=3000)
```

---

### Step 4: Handle Signature Validation Errors

Implement proper error handling and logging for signature validation failures.

**Error Response Structure:**

```json
{
"error": "Invalid HMAC signature",
"code": "SIGNATURE_INVALID",
"details": {
"receivedSignature": "TC sha256 xyz...",
"expectedFormat": "TC sha256 <base64>",
"timestamp": "2025-01-15T14:30:00Z"
}
}
```

**Debugging Checklist:**

```typescript
function debugSignatureValidation(
providedSignature: string,
expectedSignature: string,
canonicalString: string
) {
console.log("=== HMAC Signature Debug ===");
console.log("Provided Signature:", providedSignature);
console.log("Expected Signature:", expectedSignature);
console.log("Match:", providedSignature === expectedSignature);
console.log("\nCanonical String:");
console.log(canonicalString);
console.log("\nCanonical String (hex):");
console.log(Buffer.from(canonicalString).toString("hex"));
console.log("===========================");
}
```

---

## ⚠️ Important Notes

1. **Secret Key Security**: Never expose `hmacSecretKey` in client-side code, logs, or version control
2. **Date Header Format**: Must be RFC 1123 format (`new Date().toUTCString()` in JavaScript)
3. **Query Parameter Sorting**: Always sort query parameters alphabetically by key
4. **Header Sorting**: Include only `authorization` and `date` headers, sorted alphabetically
5. **Body Hashing**: Use SHA256 hex-encoded hash of the **raw body**, not parsed JSON
6. **Replay Protection**: Validate that Date header is within ±5 minutes of current time
7. **Constant-Time Comparison**: Use `crypto.timingSafeEqual()` to prevent timing attacks
8. **Character Encoding**: Always use UTF-8 encoding for all strings

---

## 🔗 Related Endpoints

- [Global Webhook Subscription](../api-1.8.1/create-webhook-subscription)
- [Webhook Status Notifications](./send-transaction-and-receive-status-via-webhook)
- [Deploy Diamond Token](./deploy-diamond-token)
- [All API Endpoints](../api-1.8.1/blockchain-api-token-city)

---

## 🆘 Troubleshooting

### Signature Validation Always Fails

If all signatures are rejected:

**Common Causes:**

- ✓ Using wrong secret key (check environment variable)
- ✓ Secret key has extra whitespace or newlines (trim it)
- ✓ Date format is incorrect (must be RFC 1123)
- ✓ Query parameters not sorted alphabetically
- ✓ Using parsed JSON instead of raw body for hash
- ✓ Including extra headers in canonical string

**Debugging Steps:**

1. Print canonical string on both client and server
2. Compare character-by-character (including newlines)
3. Print body hash in hex format
4. Verify secret key matches exactly
5. Use debugging function to log all values

**Example Debug Output:**

```
Canonical String:
POST
/v2/erc3643/deploy
chainId=polygon&network=mainnet
authorization:TC-API-CLIENT-abc123
date:Tue, 10 Jun 2025 14:17:50 GMT
a3f8c7b2e1d9f4a6c5b8e7d2f1a9c8b6e5d4f3a2c1b9e8d7f6a5c4b3e2d1f0
```

---

### Date Header Rejected (Replay Attack)

If you get "Request too old" errors:

**Causes:**

- ✓ Client clock is out of sync
- ✓ Request took too long to reach server
- ✓ Using cached date header (regenerate for each request)

**Solutions:**

- Synchronize system clock with NTP server
- Generate fresh date header for each request
- Reduce network latency
- Increase tolerance window if appropriate

**Clock Sync Commands:**

```bash
# Linux/macOS
sudo ntpdate -s time.nist.gov

# Windows
w32tm /resync
```

---

### Query Parameters Not Matching

If signature fails only with query parameters:

**Common Issues:**

- ✓ Parameters not sorted alphabetically
- ✓ Keys not lowercased
- ✓ Values not trimmed
- ✓ Using `&` at end of query string
- ✓ Special characters not URL-encoded

**Correct Processing:**

```typescript
// ❌ Wrong
const query = `network=mainnet&chainId=polygon`;

// ✅ Correct (sorted alphabetically)
const query = `chainid=polygon&network=mainnet`;

// ✅ Correct (lowercase keys, trimmed values)
const params = { chainId: " polygon ", network: "mainnet" };
const sorted = Object.entries(params)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k.toLowerCase()}=${v.trim()}`)
.join("&");
// Result: "chainid=polygon&network=mainnet"
```

---

### Body Hash Mismatch

If signature fails due to body hash:

**Common Issues:**

- ✓ Using JSON.stringify() output instead of raw body
- ✓ Whitespace differences in JSON
- ✓ Character encoding issues (use UTF-8)
- ✓ Hashing parsed object instead of string

**Solution:**

```typescript
// ❌ Wrong - hashing stringified object
const obj = { name: "Test" };
const hash = crypto
.createHash("sha256")
.update(JSON.stringify(obj))
.digest("hex");

// ✅ Correct - use raw request body
app.post(
"/api",
express.json({
verify: (req, res, buf) => {
(req as any).rawBody = buf.toString("utf8");
},
}),
(req, res) => {
const rawBody = (req as any).rawBody;
const hash = crypto
.createHash("sha256")
.update(rawBody, "utf8")
.digest("hex");
}
);
```

---

### Webhook Signature Invalid

If incoming webhooks fail validation:

**Debugging Steps:**

1. Log raw request body exactly as received
2. Log all headers (Signature, Date, Authorization)
3. Print canonical string before hashing
4. Verify secret key is correct for environment
5. Check date header is within tolerance

**Webhook Test Script:**

```typescript
// Send test webhook to your endpoint
const testPayload = {
type: "TX_MINED",
event: { txHash: "0xtest" },
};

const rawBody = JSON.stringify(testPayload);
const date = new Date().toUTCString();
const headers = calculateHmacSignature(
"your-key-id",
"your-secret",
date,
"https://your-domain.com/webhook",
rawBody,
"POST"
);

fetch("https://your-domain.com/webhook", {
method: "POST",
headers: {
"Content-Type": "application/json",
Signature: headers.signature,
Date: headers.date,
Authorization: headers.authorization,
},
body: rawBody,
});
```

---

## 📚 Additional Resources

- [RFC 2104 - HMAC Specification](https://tools.ietf.org/html/rfc2104)
- [RFC 1123 - Date Format](https://tools.ietf.org/html/rfc1123)
- [OWASP - Cryptographic Storage](https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure)
- [Webhook Security Best Practices](./send-transaction-and-receive-status-via-webhook)
- [Node.js Crypto Module](https://nodejs.org/api/crypto.html)
- [Python HMAC Module](https://docs.python.org/3/library/hmac.html)

---

## 💼 Use Case Examples

### Example 1: Production API Integration

**Scenario:** Integrate Token City API into production trading platform with HMAC authentication.

**Requirements:**

- Secure credential storage
- Request signing for all API calls
- Webhook validation for real-time events
- Key rotation support

**Implementation:**

**1. Environment Configuration:**

```bash
# .env
HMAC_KEY_ID=TC-API-CLIENT-prod-xyz789
HMAC_SECRET_KEY=sk_live_Xn2r5u8x/A?D(G+KbPeShVmYq3t6w9y$
TOKEN_CITY_API_URL=https://api.tokencity.com
```

**2. API Client with HMAC:**

```typescript
import axios from "axios";
import { calculateHmacSignature } from "./hmac";

class TokenCityClient {
private keyId: string;
private secretKey: string;
private baseUrl: string;

constructor() {
this.keyId = process.env.HMAC_KEY_ID!;
this.secretKey = process.env.HMAC_SECRET_KEY!;
this.baseUrl = process.env.TOKEN_CITY_API_URL!;
}

async deployToken(params: any) {
const url = `${this.baseUrl}/v2/erc3643/deploy`;
const payload = JSON.stringify(params);
const date = new Date().toUTCString();

const headers = calculateHmacSignature(
this.keyId,
this.secretKey,
date,
url,
payload,
"POST",
{ chainId: "polygon", network: "mainnet" }
);

const response = await axios.post(url, params, {
params: { chainId: "polygon", network: "mainnet" },
headers: {
"Content-Type": "application/json",
Signature: headers.signature,
Date: headers.date,
Authorization: headers.authorization,
},
});

return response.data;
}
}

// Usage
const client = new TokenCityClient();
const result = await client.deployToken({
name: "Production Token",
symbol: "PROD",
// ... other params
});
```

**3. Webhook Handler:**

```typescript
app.post("/webhook/tokencity", async (req, res) => {
// Validate HMAC
const isValid = validateHmacSignature(/* ... */);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}

// Process event
const event = req.body;
await processWebhookEvent(event);

res.status(200).json({ status: "received" });
});
```

**Result:** Secure API integration with authenticated requests and validated webhooks in production.

---

### Example 2: Multi-Environment Key Management

**Scenario:** Manage separate HMAC credentials for development, staging, and production.

**Configuration:**

```typescript
// config/hmac.ts
interface HmacConfig {
keyId: string;
secretKey: string;
apiUrl: string;
}

const configs: Record<string, HmacConfig> = {
development: {
keyId: "TC-API-CLIENT-dev-abc123",
secretKey: "sk_test_dev_secret_key",
apiUrl: "https://dev-api.tokencity.com",
},
staging: {
keyId: "TC-API-CLIENT-stg-def456",
secretKey: "sk_test_stg_secret_key",
apiUrl: "https://staging-api.tokencity.com",
},
production: {
keyId: "TC-API-CLIENT-prod-xyz789",
secretKey: process.env.PROD_HMAC_SECRET!, // Only from env
apiUrl: "https://api.tokencity.com",
},
};

export function getHmacConfig(): HmacConfig {
const env = process.env.NODE_ENV || "development";
return configs[env];
}
```

**Key Rotation Strategy:**

```typescript
// Store key version with signatures
interface SignedRequest {
payload: any;
signature: string;
keyVersion: string;
timestamp: string;
}

// Support multiple active keys during rotation
const activeKeys = {
v1: "old_secret_key", // Being phased out
v2: "new_secret_key", // Current key
};

function validateWithMultipleKeys(
signature: string,
canonicalString: string
): boolean {
for (const [version, key] of Object.entries(activeKeys)) {
const expectedSig = crypto
.createHmac("sha256", key)
.update(canonicalString)
.digest("base64");

if (
crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(signature))
) {
console.log(`Valid signature with key version: ${version}`);
return true;
}
}
return false;
}
```

**Result:** Secure multi-environment setup with smooth key rotation capabilities.

---

### Example 3: Webhook Relay Service

**Scenario:** Build a webhook relay service that validates Token City webhooks and forwards to multiple internal services.

**Architecture:**

```
Token City API → Relay Service (HMAC validation) → Internal Services

[Validation]

[Transform]

[Fan-out]
```

**Implementation:**

```typescript
import express from "express";
import axios from "axios";

interface WebhookSubscriber {
name: string;
url: string;
events: string[];
}

const subscribers: WebhookSubscriber[] = [
{
name: "Trading Engine",
url: "http://trading/events",
events: ["TX_MINED"],
},
{
name: "Compliance Monitor",
url: "http://compliance/events",
events: ["TX_MINED", "TX_FAILED"],
},
{
name: "Analytics",
url: "http://analytics/events",
events: ["TX_MINED", "TX_FAILED", "GAS_USAGE_HIGH"],
},
];

app.post("/relay/tokencity", async (req, res) => {
// 1. Validate HMAC from Token City
const isValid = validateHmacSignature(
req.headers["signature"] as string,
req.headers["date"] as string,
req.headers["authorization"] as string,
process.env.HMAC_SECRET_KEY!,
req.path,
req.method,
(req as any).rawBody
);

if (!isValid) {
console.error("Invalid webhook signature from Token City");
return res.status(401).json({ error: "Invalid signature" });
}

// 2. Parse event
const event = req.body;
console.log(`Valid webhook received: ${event.type}`);

// 3. Fan out to subscribers
const promises = subscribers
.filter((sub) => sub.events.includes(event.type))
.map(async (sub) => {
try {
await axios.post(sub.url, event, {
headers: { "X-Relay-Source": "TokenCity" },
timeout: 5000,
});
console.log(`Forwarded to ${sub.name}`);
} catch (error) {
console.error(`Failed to forward to ${sub.name}:`, error);
}
});

await Promise.allSettled(promises);

// 4. Acknowledge to Token City
res.status(200).json({ status: "relayed", count: promises.length });
});

app.listen(8080, () => {
console.log("Webhook relay service running on port 8080");
});
```

**Monitoring:**

```typescript
// Track webhook metrics
const metrics = {
received: 0,
validated: 0,
rejected: 0,
forwarded: 0,
errors: 0,
};

app.post("/relay/tokencity", async (req, res) => {
metrics.received++;

const isValid = validateHmacSignature(/* ... */);
if (isValid) {
metrics.validated++;
} else {
metrics.rejected++;
// Alert on high rejection rate
if (metrics.rejected / metrics.received > 0.1) {
sendAlert("High webhook rejection rate");
}
}
// ... rest of handler
});

// Metrics endpoint
app.get("/metrics", (req, res) => {
res.json(metrics);
});
```

**Result:** Centralized webhook validation and distribution service with monitoring and error handling.

---

### Example 4: Debugging Signature Issues in CI/CD

**Scenario:** API tests failing in CI pipeline due to HMAC signature validation errors.

**Problem:**
Tests pass locally but fail in GitHub Actions CI/CD pipeline with "Invalid HMAC signature" errors.

**Root Cause Analysis:**

**1. Date Header Timezone Issue:**

```typescript
// ❌ Wrong - uses local timezone
const date = new Date().toString();
// "Wed Jan 15 2025 10:30:00 GMT-0500 (Eastern Standard Time)"

// ✅ Correct - uses UTC
const date = new Date().toUTCString();
// "Wed, 15 Jan 2025 15:30:00 GMT"
```

**2. Line Ending Differences:**

```bash
# CI environment uses LF, local uses CRLF
# This affects canonical string hash

# Fix: Configure git to use consistent line endings
echo "* text=auto eol=lf" > .gitattributes
git add --renormalize .
```

**3. Environment Variable Whitespace:**

```yaml
# .github/workflows/test.yml
# ❌ Wrong - includes newline
env:
HMAC_SECRET_KEY: |
sk_test_secret_key

# ✅ Correct - single line
env:
HMAC_SECRET_KEY: sk_test_secret_key
```

**4. Test Helper with Debugging:**

```typescript
// tests/helpers/hmac.test.ts
import { calculateHmacSignature } from "../../src/hmac";

describe("HMAC Signature", () => {
const testSecret = "sk_test_secret_key";
const testKeyId = "TC-API-CLIENT-test";

it("generates consistent signatures", () => {
const date = "Wed, 15 Jan 2025 15:30:00 GMT"; // Fixed date
const url = "https://api.test.com/v2/endpoint";
const payload = '{"name":"test"}';

const signature1 = calculateHmacSignature(
testKeyId,
testSecret,
date,
url,
payload
);
const signature2 = calculateHmacSignature(
testKeyId,
testSecret,
date,
url,
payload
);

expect(signature1.signature).toBe(signature2.signature);
});

it("detects body tampering", () => {
const date = "Wed, 15 Jan 2025 15:30:00 GMT";
const url = "https://api.test.com/v2/endpoint";
const payload1 = '{"amount":100}';
const payload2 = '{"amount":999}'; // Tampered

const sig1 = calculateHmacSignature(
testKeyId,
testSecret,
date,
url,
payload1
);
const sig2 = calculateHmacSignature(
testKeyId,
testSecret,
date,
url,
payload2
);

expect(sig1.signature).not.toBe(sig2.signature);
});
});
```

**Result:** Tests pass consistently in both local and CI environments with proper HMAC implementation.

---