Skip to content

🔐 HMAC Authentication Implementation Guide

📋 Overview

Partner Services uses HMAC SHA-256 authentication for secure API access. This guide provides complete implementation details for multiple programming languages with working code examples.

Security Features:

  • HMAC SHA-256 signature verification
  • Timestamp validation (5-minute window)
  • Request body integrity checking
  • Partner-specific secret keys
  • Replay attack prevention

🔧 Authentication Flow

Step-by-Step Process

  1. Generate timestamp (Unix timestamp)
  2. Create string to sign from method + path + body + timestamp
  3. Calculate HMAC signature using SHA-256 and secret key
  4. Add headers to request (x-partner-id, x-timestamp, x-signature)
  5. Send request to API endpoint

String to Sign Format

{HTTP_METHOD}\n{REQUEST_PATH}\n{REQUEST_BODY}\n{TIMESTAMP}

Example:

POST\n/api/v1/shipments/calculate-charges\n{"weight":2500,"pickupPincode":"110001"}\n1642752000

💻 Implementation Examples

Node.js / JavaScript

Complete Implementation

javascript
const crypto = require("crypto");
const axios = require("axios");

class PartnerServicesAuth {
  constructor(config) {
    this.baseURL = config.baseURL;
    this.partnerId = config.partnerId;
    this.secretKey = config.secretKey;
  }

  /**
   * Generate HMAC SHA-256 signature
   * @param {string} method - HTTP method (GET, POST, etc.)
   * @param {string} path - Request path (/api/v1/...)
   * @param {object|null} body - Request body object
   * @param {number} timestamp - Unix timestamp
   * @returns {string} HMAC signature
   */
  generateSignature(method, path, body, timestamp) {
    // Convert body to JSON string or empty string
    const bodyStr = body ? JSON.stringify(body) : "";

    // Create string to sign
    const stringToSign = `${method}\n${path}\n${bodyStr}\n${timestamp}`;

    // Generate HMAC SHA-256 signature
    return crypto
      .createHmac("sha256", this.secretKey)
      .update(stringToSign)
      .digest("hex");
  }

  /**
   * Generate authentication headers
   * @param {string} method - HTTP method
   * @param {string} path - Request path
   * @param {object|null} body - Request body
   * @returns {object} Headers object
   */
  generateAuthHeaders(method, path, body = null) {
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = this.generateSignature(method, path, body, timestamp);

    return {
      "Content-Type": "application/json",
      "x-partner-id": this.partnerId,
      "x-timestamp": timestamp,
      "x-signature": signature,
    };
  }

  /**
   * Make authenticated API request
   * @param {string} method - HTTP method
   * @param {string} path - Request path
   * @param {object|null} body - Request body
   * @returns {Promise} API response
   */
  async makeRequest(method, path, body = null) {
    const headers = this.generateAuthHeaders(method, path, body);

    try {
      const response = await axios({
        method: method.toLowerCase(),
        url: `${this.baseURL}${path}`,
        headers,
        data: body,
      });

      return response.data;
    } catch (error) {
      // Enhanced error handling
      if (error.response) {
        const errorData = error.response.data;
        console.error("API Error:", {
          status: error.response.status,
          code: errorData?.error?.code,
          message: errorData?.error?.message,
          details: errorData?.error?.details,
        });
        throw new Error(
          `API Error: ${errorData?.error?.message || "Unknown error"}`
        );
      } else {
        console.error("Network Error:", error.message);
        throw new Error(`Network Error: ${error.message}`);
      }
    }
  }
}

// Usage Example
const auth = new PartnerServicesAuth({
  baseURL: "https://partner-services.logistics.com/api/v1",
  partnerId: "your_partner_id",
  secretKey: "your_secret_key",
});

// Test authentication
async function testAuth() {
  try {
    const result = await auth.makeRequest("GET", "/health");
    console.log("✅ Authentication successful:", result);
  } catch (error) {
    console.error("❌ Authentication failed:", error.message);
  }
}

// Calculate charges example
async function calculateCharges() {
  const shipmentData = {
    pickupPincode: "110001",
    deliveryPincode: "400001",
    weight: 2500,
    shipmentType: "B2C",
    paymentType: "COD",
  };

  try {
    const result = await auth.makeRequest(
      "POST",
      "/shipments/calculate-charges",
      shipmentData
    );
    console.log("✅ Charges calculated:", result.data.charges);
  } catch (error) {
    console.error("❌ Calculation failed:", error.message);
  }
}

Browser/Frontend Implementation

javascript
class BrowserPartnerAuth {
  constructor(config) {
    this.baseURL = config.baseURL;
    this.partnerId = config.partnerId;
    this.secretKey = config.secretKey; // ⚠️ Consider server-side proxy for security
  }

  async generateSignature(method, path, body, timestamp) {
    const bodyStr = body ? JSON.stringify(body) : "";
    const stringToSign = `${method}\n${path}\n${bodyStr}\n${timestamp}`;

    // Use Web Crypto API for HMAC
    const encoder = new TextEncoder();
    const keyData = encoder.encode(this.secretKey);
    const messageData = encoder.encode(stringToSign);

    const cryptoKey = await crypto.subtle.importKey(
      "raw",
      keyData,
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"]
    );

    const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
    return Array.from(new Uint8Array(signature))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");
  }

  async makeRequest(method, path, body = null) {
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = await this.generateSignature(
      method,
      path,
      body,
      timestamp
    );

    const headers = {
      "Content-Type": "application/json",
      "x-partner-id": this.partnerId,
      "x-timestamp": timestamp,
      "x-signature": signature,
    };

    const response = await fetch(`${this.baseURL}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : null,
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.error?.message || "API request failed");
    }

    return await response.json();
  }
}

Python Implementation

python
import hashlib
import hmac
import json
import time
import requests
from typing import Optional, Dict, Any

class PartnerServicesAuth:
    def __init__(self, base_url: str, partner_id: str, secret_key: str):
        self.base_url = base_url
        self.partner_id = partner_id
        self.secret_key = secret_key.encode('utf-8')

    def generate_signature(self, method: str, path: str, body: Optional[Dict], timestamp: int) -> str:
        """Generate HMAC SHA-256 signature"""
        # Convert body to JSON string or empty string
        body_str = json.dumps(body, separators=(',', ':')) if body else ''

        # Create string to sign
        string_to_sign = f"{method}\n{path}\n{body_str}\n{timestamp}"

        # Generate HMAC SHA-256 signature
        signature = hmac.new(
            self.secret_key,
            string_to_sign.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()

        return signature

    def generate_auth_headers(self, method: str, path: str, body: Optional[Dict] = None) -> Dict[str, str]:
        """Generate authentication headers"""
        timestamp = int(time.time())
        signature = self.generate_signature(method, path, body, timestamp)

        return {
            'Content-Type': 'application/json',
            'x-partner-id': self.partner_id,
            'x-timestamp': str(timestamp),
            'x-signature': signature
        }

    def make_request(self, method: str, path: str, body: Optional[Dict] = None) -> Dict[str, Any]:
        """Make authenticated API request"""
        headers = self.generate_auth_headers(method, path, body)
        url = f"{self.base_url}{path}"

        try:
            response = requests.request(
                method=method.lower(),
                url=url,
                headers=headers,
                json=body,
                timeout=30
            )

            # Raise exception for HTTP errors
            response.raise_for_status()

            return response.json()

        except requests.exceptions.HTTPError as e:
            error_data = response.json() if response.content else {}
            error_message = error_data.get('error', {}).get('message', 'Unknown error')
            raise Exception(f"API Error: {error_message}")
        except requests.exceptions.RequestException as e:
            raise Exception(f"Network Error: {str(e)}")

# Usage Example
auth = PartnerServicesAuth(
    base_url='https://partner-services.logistics.com/api/v1',
    partner_id='your_partner_id',
    secret_key='your_secret_key'
)

# Test authentication
try:
    result = auth.make_request('GET', '/health')
    print('✅ Authentication successful:', result)
except Exception as e:
    print('❌ Authentication failed:', str(e))

# Calculate charges
shipment_data = {
    'pickupPincode': '110001',
    'deliveryPincode': '400001',
    'weight': 2500,
    'shipmentType': 'B2C',
    'paymentType': 'COD'
}

try:
    result = auth.make_request('POST', '/shipments/calculate-charges', shipment_data)
    print('✅ Charges calculated:', result['data']['charges'])
except Exception as e:
    print('❌ Calculation failed:', str(e))

PHP Implementation

php
<?php

class PartnerServicesAuth {
    private $baseUrl;
    private $partnerId;
    private $secretKey;

    public function __construct($baseUrl, $partnerId, $secretKey) {
        $this->baseUrl = $baseUrl;
        $this->partnerId = $partnerId;
        $this->secretKey = $secretKey;
    }

    /**
     * Generate HMAC SHA-256 signature
     */
    public function generateSignature($method, $path, $body, $timestamp) {
        // Convert body to JSON string or empty string
        $bodyStr = $body ? json_encode($body, JSON_UNESCAPED_SLASHES) : '';

        // Create string to sign
        $stringToSign = "{$method}\n{$path}\n{$bodyStr}\n{$timestamp}";

        // Generate HMAC SHA-256 signature
        return hash_hmac('sha256', $stringToSign, $this->secretKey);
    }

    /**
     * Generate authentication headers
     */
    public function generateAuthHeaders($method, $path, $body = null) {
        $timestamp = time();
        $signature = $this->generateSignature($method, $path, $body, $timestamp);

        return [
            'Content-Type' => 'application/json',
            'x-partner-id' => $this->partnerId,
            'x-timestamp' => (string)$timestamp,
            'x-signature' => $signature
        ];
    }

    /**
     * Make authenticated API request
     */
    public function makeRequest($method, $path, $body = null) {
        $headers = $this->generateAuthHeaders($method, $path, $body);
        $url = $this->baseUrl . $path;

        // Prepare cURL
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CUSTOMREQUEST => strtoupper($method),
            CURLOPT_HTTPHEADER => $this->formatHeaders($headers),
            CURLOPT_POSTFIELDS => $body ? json_encode($body, JSON_UNESCAPED_SLASHES) : null
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($error) {
            throw new Exception("Network Error: $error");
        }

        $responseData = json_decode($response, true);

        if ($httpCode >= 400) {
            $errorMessage = $responseData['error']['message'] ?? 'Unknown error';
            throw new Exception("API Error: $errorMessage");
        }

        return $responseData;
    }

    /**
     * Format headers for cURL
     */
    private function formatHeaders($headers) {
        $formatted = [];
        foreach ($headers as $key => $value) {
            $formatted[] = "$key: $value";
        }
        return $formatted;
    }
}

// Usage Example
$auth = new PartnerServicesAuth(
    'https://partner-services.logistics.com/api/v1',
    'your_partner_id',
    'your_secret_key'
);

// Test authentication
try {
    $result = $auth->makeRequest('GET', '/health');
    echo "✅ Authentication successful: " . json_encode($result) . "\n";
} catch (Exception $e) {
    echo "❌ Authentication failed: " . $e->getMessage() . "\n";
}

// Calculate charges
$shipmentData = [
    'pickupPincode' => '110001',
    'deliveryPincode' => '400001',
    'weight' => 2500,
    'shipmentType' => 'B2C',
    'paymentType' => 'COD'
];

try {
    $result = $auth->makeRequest('POST', '/shipments/calculate-charges', $shipmentData);
    echo "✅ Charges calculated: " . json_encode($result['data']['charges']) . "\n";
} catch (Exception $e) {
    echo "❌ Calculation failed: " . $e->getMessage() . "\n";
}
?>

🔒 Security Best Practices

1. Secret Key Management

✅ Secure Practices:

javascript
// Use environment variables
const secretKey = process.env.PARTNER_SERVICES_SECRET_KEY;

// Use secure key management services
const secretKey = await getSecretFromVault("partner-services-key");

// Rotate keys regularly
const secretKey = await getCurrentActiveKey();

❌ Insecure Practices:

javascript
// Never hardcode in source code
const secretKey = "sk_live_1234567890abcdef"; // ❌ DON'T DO THIS

// Never commit to version control
// Never log secret keys
// Never send keys in API responses

2. Timestamp Validation

Implementation:

javascript
function validateTimestamp(requestTimestamp) {
  const currentTime = Math.floor(Date.now() / 1000);
  const timeDiff = Math.abs(currentTime - requestTimestamp);

  // Allow 5-minute window (300 seconds)
  if (timeDiff > 300) {
    throw new Error("Request timestamp is too old or too far in the future");
  }

  return true;
}

3. Request Body Integrity

Ensure consistency:

javascript
// Always use the same JSON formatting
const bodyStr = body ? JSON.stringify(body, null, 0) : "";

// Avoid extra whitespace or formatting differences
// Use consistent key ordering if possible

4. HTTPS Only

Always use HTTPS in production:

javascript
const baseURL = "https://partner-services.logistics.com/api/v1"; // ✅ Secure
// Never use HTTP in production: http://... // ❌ Insecure

🧪 Testing Authentication

Authentication Test Suite

javascript
class AuthTestSuite {
  constructor(auth) {
    this.auth = auth;
  }

  async runAllTests() {
    const tests = [
      { name: "Basic Authentication", test: () => this.testBasicAuth() },
      { name: "Invalid Partner ID", test: () => this.testInvalidPartnerId() },
      { name: "Invalid Signature", test: () => this.testInvalidSignature() },
      { name: "Old Timestamp", test: () => this.testOldTimestamp() },
      { name: "Future Timestamp", test: () => this.testFutureTimestamp() },
      { name: "Body Integrity", test: () => this.testBodyIntegrity() },
    ];

    const results = [];
    for (const test of tests) {
      try {
        await test.test();
        results.push({ name: test.name, status: "PASSED" });
        console.log(`✅ ${test.name}: PASSED`);
      } catch (error) {
        results.push({
          name: test.name,
          status: "FAILED",
          error: error.message,
        });
        console.log(`❌ ${test.name}: FAILED - ${error.message}`);
      }
    }

    return results;
  }

  async testBasicAuth() {
    const result = await this.auth.makeRequest("GET", "/health");
    if (!result.success) {
      throw new Error("Basic authentication failed");
    }
  }

  async testInvalidPartnerId() {
    const invalidAuth = { ...this.auth, partnerId: "invalid_partner" };
    try {
      await invalidAuth.makeRequest("GET", "/health");
      throw new Error("Should have failed with invalid partner ID");
    } catch (error) {
      if (!error.message.includes("AUTHENTICATION_FAILED")) {
        throw error;
      }
    }
  }

  async testInvalidSignature() {
    const invalidAuth = { ...this.auth, secretKey: "invalid_secret" };
    try {
      await invalidAuth.makeRequest("GET", "/health");
      throw new Error("Should have failed with invalid signature");
    } catch (error) {
      if (!error.message.includes("AUTHENTICATION_FAILED")) {
        throw error;
      }
    }
  }

  async testOldTimestamp() {
    // Test with timestamp 10 minutes ago (should fail)
    const oldTimestamp = Math.floor(Date.now() / 1000) - 600;
    const signature = this.auth.generateSignature(
      "GET",
      "/health",
      null,
      oldTimestamp
    );

    try {
      await axios.get(`${this.auth.baseURL}/health`, {
        headers: {
          "x-partner-id": this.auth.partnerId,
          "x-timestamp": oldTimestamp,
          "x-signature": signature,
        },
      });
      throw new Error("Should have failed with old timestamp");
    } catch (error) {
      if (!error.response?.data?.error?.code?.includes("TIMESTAMP")) {
        throw error;
      }
    }
  }

  async testFutureTimestamp() {
    // Test with timestamp 10 minutes in future (should fail)
    const futureTimestamp = Math.floor(Date.now() / 1000) + 600;
    const signature = this.auth.generateSignature(
      "GET",
      "/health",
      null,
      futureTimestamp
    );

    try {
      await axios.get(`${this.auth.baseURL}/health`, {
        headers: {
          "x-partner-id": this.auth.partnerId,
          "x-timestamp": futureTimestamp,
          "x-signature": signature,
        },
      });
      throw new Error("Should have failed with future timestamp");
    } catch (error) {
      if (!error.response?.data?.error?.code?.includes("TIMESTAMP")) {
        throw error;
      }
    }
  }

  async testBodyIntegrity() {
    const originalBody = { weight: 2500, pincode: "110001" };
    const modifiedBody = { weight: 2500, pincode: "110002" }; // Modified after signing

    // Sign with original body
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = this.auth.generateSignature(
      "POST",
      "/test",
      originalBody,
      timestamp
    );

    try {
      // Send with modified body (should fail)
      await axios.post(
        `${this.auth.baseURL}/shipments/calculate-charges`,
        modifiedBody,
        {
          headers: {
            "Content-Type": "application/json",
            "x-partner-id": this.auth.partnerId,
            "x-timestamp": timestamp,
            "x-signature": signature,
          },
        }
      );
      throw new Error("Should have failed with modified body");
    } catch (error) {
      if (!error.response?.data?.error?.code?.includes("SIGNATURE")) {
        throw error;
      }
    }
  }
}

// Run tests
const auth = new PartnerServicesAuth(config);
const testSuite = new AuthTestSuite(auth);

testSuite.runAllTests().then((results) => {
  console.log("\n📊 Test Results:");
  const passed = results.filter((r) => r.status === "PASSED").length;
  console.log(`${passed}/${results.length} tests passed`);
});

🛠️ Debugging Authentication Issues

Common Issues & Solutions

❌ Issue: "Invalid HMAC signature"

Error: AUTHENTICATION_FAILED - Invalid HMAC signature

🔧 Solutions:

  1. Check secret key is correct
  2. Verify string-to-sign format
  3. Ensure consistent JSON formatting
  4. Check for extra whitespace in body

❌ Issue: "Request timestamp too old"

Error: AUTHENTICATION_FAILED - Request timestamp is too old

🔧 Solutions:

  1. Synchronize system clock
  2. Generate timestamp just before request
  3. Check timezone settings
  4. Verify timestamp is in Unix format

❌ Issue: "Partner not found"

Error: PARTNER_NOT_FOUND - Partner ID not found

🔧 Solutions:

  1. Verify partner ID is correct
  2. Check partner account is active
  3. Ensure partner has API access enabled

Debug Helper Function

javascript
function debugAuthGeneration(method, path, body, partnerId, secretKey) {
  const timestamp = Math.floor(Date.now() / 1000);
  const bodyStr = body ? JSON.stringify(body) : "";
  const stringToSign = `${method}\n${path}\n${bodyStr}\n${timestamp}`;
  const signature = crypto
    .createHmac("sha256", secretKey)
    .update(stringToSign)
    .digest("hex");

  console.log("🔍 Authentication Debug Information:");
  console.log("Method:", method);
  console.log("Path:", path);
  console.log("Body String:", bodyStr);
  console.log("Timestamp:", timestamp);
  console.log("String to Sign:", JSON.stringify(stringToSign));
  console.log("Secret Key Length:", secretKey.length);
  console.log("Generated Signature:", signature);
  console.log("Partner ID:", partnerId);

  return {
    "x-partner-id": partnerId,
    "x-timestamp": timestamp,
    "x-signature": signature,
  };
}

Performance Optimization

Caching Authentication Headers

javascript
class CachedPartnerAuth extends PartnerServicesAuth {
  constructor(config) {
    super(config);
    this.signatureCache = new Map();
    this.cacheTimeout = 60; // 1 minute cache
  }

  generateCachedSignature(method, path, body, timestamp) {
    const cacheKey = `${method}:${path}:${JSON.stringify(body)}:${timestamp}`;

    if (this.signatureCache.has(cacheKey)) {
      return this.signatureCache.get(cacheKey);
    }

    const signature = this.generateSignature(method, path, body, timestamp);

    // Cache with expiration
    setTimeout(() => {
      this.signatureCache.delete(cacheKey);
    }, this.cacheTimeout * 1000);

    this.signatureCache.set(cacheKey, signature);
    return signature;
  }
}

Connection Pooling

javascript
const axios = require("axios");
const https = require("https");

// Create HTTPS agent with connection pooling
const httpsAgent = new https.Agent({
  keepAlive: true,
  keepAliveMsecs: 1000,
  maxSockets: 50,
  maxFreeSockets: 10,
  timeout: 60000,
});

// Configure axios with connection pooling
const apiClient = axios.create({
  httpsAgent,
  timeout: 30000,
  maxRedirects: 3,
});

🎯 Next Steps

  1. Main System Flow - Complete workflow implementation
  2. Performance Optimization - Caching and optimization strategies
  3. Error Handling - Robust error management
  4. API Examples - More implementation examples

Your authentication implementation is now secure and production-ready! 🔐

Released under the MIT License.