Authentication
Secure your Site Availability API with HMAC-SHA256 authentication.
Overview
The API supports HMAC (Hash-based Message Authentication Code) authentication using SHA-256 for secure access. This ensures that requests are authentic and haven't been tampered with.
Enabling Authentication
Configuration
Enable HMAC authentication in your configuration:
# config.yaml
authentication:
hmac:
enabled: true
secret: ${SA_HMAC_SECRET} # Use environment variable
algorithm: sha256
clock_skew_tolerance: 300 # 5 minutes
Environment Variable
Set the HMAC secret via environment variable:
export SA_HMAC_SECRET="your-very-secure-secret-key-here"
HMAC Signature Process
1. Create the Canonical Request
Construct a canonical string from the request:
METHOD\nPATH\nBODY\nTIMESTAMP
Example:
GET\n/api/apps\n\n1638360000
For POST requests with body:
POST\n/api/scrape-interval\n{"interval":"60s"}\n1638360000
2. Generate the Signature
Create HMAC-SHA256 signature:
const crypto = require("crypto");
function generateSignature(method, path, body, timestamp, secret) {
const canonicalRequest = `${method}\n${path}\n${body || ""}\n${timestamp}`;
const signature = crypto
.createHmac("sha256", secret)
.update(canonicalRequest)
.digest("hex");
return signature;
}
3. Add Headers to Request
Include the signature and timestamp in request headers:
Authorization: HMAC-SHA256 <signature>
X-Timestamp: <unix_timestamp>
Content-Type: application/json
Implementation Examples
JavaScript/Node.js
class SiteAvailabilityClient {
constructor(baseUrl, secret) {
this.baseUrl = baseUrl;
this.secret = secret;
}
generateSignature(method, path, body, timestamp) {
const crypto = require("crypto");
const canonicalRequest = `${method}\n${path}\n${body || ""}\n${timestamp}`;
return crypto
.createHmac("sha256", this.secret)
.update(canonicalRequest)
.digest("hex");
}
async makeRequest(method, path, body = null) {
const timestamp = Math.floor(Date.now() / 1000);
const bodyString = body ? JSON.stringify(body) : "";
const signature = this.generateSignature(
method,
path,
bodyString,
timestamp,
);
const headers = {
Authorization: `HMAC-SHA256 ${signature}`,
"X-Timestamp": timestamp.toString(),
"Content-Type": "application/json",
};
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers,
body: bodyString || undefined,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Usage examples
async getApplications() {
return this.makeRequest("GET", "/api/apps");
}
async updateScrapeInterval(interval) {
return this.makeRequest("POST", "/api/scrape-interval", { interval });
}
}
// Usage
const client = new SiteAvailabilityClient(
"http://localhost:8080",
"your-secret-key",
);
const apps = await client.getApplications();
Go
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
type Client struct {
BaseURL string
Secret string
HTTP *http.Client
}
func NewClient(baseURL, secret string) *Client {
return &Client{
BaseURL: baseURL,
Secret: secret,
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
func (c *Client) generateSignature(method, path, body string, timestamp int64) string {
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%d", method, path, body, timestamp)
h := hmac.New(sha256.New, []byte(c.Secret))
h.Write([]byte(canonicalRequest))
return hex.EncodeToString(h.Sum(nil))
}
func (c *Client) makeRequest(method, path string, body interface{}) (*http.Response, error) {
var bodyBytes []byte
var bodyString string
if body != nil {
var err error
bodyBytes, err = json.Marshal(body)
if err != nil {
return nil, err
}
bodyString = string(bodyBytes)
}
timestamp := time.Now().Unix()
signature := c.generateSignature(method, path, bodyString, timestamp)
req, err := http.NewRequest(method, c.BaseURL+path, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("HMAC-SHA256 %s", signature))
req.Header.Set("X-Timestamp", strconv.FormatInt(timestamp, 10))
req.Header.Set("Content-Type", "application/json")
return c.HTTP.Do(req)
}
// Usage examples
func (c *Client) GetApplications() ([]Application, error) {
resp, err := c.makeRequest("GET", "/api/apps", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Data []Application `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Data, nil
}
func (c *Client) UpdateScrapeInterval(interval string) error {
body := map[string]string{"interval": interval}
resp, err := c.makeRequest("POST", "/api/scrape-interval", body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return nil
}
Python
import hashlib
import hmac
import json
import time
import requests
class SiteAvailabilityClient:
def __init__(self, base_url, secret):
self.base_url = base_url
self.secret = secret.encode('utf-8')
def generate_signature(self, method, path, body, timestamp):
canonical_request = f"{method}\n{path}\n{body or ''}\n{timestamp}"
signature = hmac.new(
self.secret,
canonical_request.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature
def make_request(self, method, path, body=None):
timestamp = int(time.time())
body_string = json.dumps(body) if body else ''
signature = self.generate_signature(method, path, body_string, timestamp)
headers = {
'Authorization': f'HMAC-SHA256 {signature}',
'X-Timestamp': str(timestamp),
'Content-Type': 'application/json'
}
url = f"{self.base_url}{path}"
response = requests.request(
method,
url,
headers=headers,
data=body_string if body_string else None
)
response.raise_for_status()
return response.json()
def get_applications(self):
return self.make_request('GET', '/api/apps')
def update_scrape_interval(self, interval):
return self.make_request('POST', '/api/scrape-interval', {'interval': interval})
# Usage
client = SiteAvailabilityClient('http://localhost:8080', 'your-secret-key')
apps = client.get_applications()
cURL
#!/bin/bash
SECRET="your-secret-key"
BASE_URL="http://localhost:8080"
METHOD="GET"
PATH="/api/apps"
BODY=""
TIMESTAMP=$(date +%s)
# Create canonical request
CANONICAL_REQUEST="${METHOD}\n${PATH}\n${BODY}\n${TIMESTAMP}"
# Generate signature
SIGNATURE=$(echo -ne "$CANONICAL_REQUEST" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
# Make request
curl -X "$METHOD" \
-H "Authorization: HMAC-SHA256 $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "Content-Type: application/json" \
"$BASE_URL$PATH"
Security Considerations
Clock Skew Tolerance
The server accepts requests within a configurable time window to account for clock differences:
authentication:
hmac:
clock_skew_tolerance: 300 # 5 minutes
Secret Management
Best Practices:
- Use strong, randomly generated secrets (minimum 32 characters)
- Store secrets in environment variables or secure key management systems
- Rotate secrets regularly
- Never commit secrets to version control
Secret Generation:
# Generate a secure secret
openssl rand -hex 32
Replay Attack Prevention
The timestamp in the signature helps prevent replay attacks:
- Each request must include a current timestamp
- Server rejects requests outside the tolerance window
- Consider implementing nonce tracking for additional security
Authentication Errors
Invalid Signature
{
"error": {
"code": "INVALID_SIGNATURE",
"message": "HMAC signature verification failed",
"details": ["Check your secret key and signature generation"]
}
}
Timestamp Issues
{
"error": {
"code": "TIMESTAMP_ERROR",
"message": "Request timestamp outside acceptable range",
"details": [
"Current server time: 1638360300",
"Request timestamp: 1638359000"
]
}
}
Missing Headers
{
"error": {
"code": "MISSING_AUTH_HEADERS",
"message": "Required authentication headers missing",
"details": ["Authorization and X-Timestamp headers required"]
}
}
Testing Authentication
Verification Script
#!/bin/bash
# test-auth.sh
SECRET="your-secret-key"
BASE_URL="http://localhost:8080"
echo "Testing authentication..."
# Test GET request
METHOD="GET"
PATH="/api/apps"
BODY=""
TIMESTAMP=$(date +%s)
CANONICAL_REQUEST="${METHOD}\n${PATH}\n${BODY}\n${TIMESTAMP}"
SIGNATURE=$(echo -ne "$CANONICAL_REQUEST" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
echo "GET /api/apps"
curl -s -w "Status: %{http_code}\n" \
-H "Authorization: HMAC-SHA256 $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
"$BASE_URL$PATH"
# Test POST request
METHOD="POST"
PATH="/api/scrape-interval"
BODY='{"interval":"60s"}'
TIMESTAMP=$(date +%s)
CANONICAL_REQUEST="${METHOD}\n${PATH}\n${BODY}\n${TIMESTAMP}"
SIGNATURE=$(echo -ne "$CANONICAL_REQUEST" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
echo -e "\nPOST /api/scrape-interval"
curl -s -w "Status: %{http_code}\n" \
-X POST \
-H "Authorization: HMAC-SHA256 $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "Content-Type: application/json" \
-d "$BODY" \
"$BASE_URL$PATH"
Unit Tests
func TestHMACAuthentication(t *testing.T) {
secret := "test-secret"
auth := NewHMACAuth(secret)
// Test valid signature
timestamp := time.Now().Unix()
signature := generateTestSignature("GET", "/api/apps", "", timestamp, secret)
req := httptest.NewRequest("GET", "/api/apps", nil)
req.Header.Set("Authorization", fmt.Sprintf("HMAC-SHA256 %s", signature))
req.Header.Set("X-Timestamp", strconv.FormatInt(timestamp, 10))
valid, err := auth.ValidateRequest(req)
assert.NoError(t, err)
assert.True(t, valid)
// Test invalid signature
req.Header.Set("Authorization", "HMAC-SHA256 invalid-signature")
valid, err = auth.ValidateRequest(req)
assert.Error(t, err)
assert.False(t, valid)
}
Troubleshooting
Common Issues
- Clock Synchronization: Ensure client and server clocks are synchronized
- Encoding: Use UTF-8 encoding for all string operations
- Newlines: Use
\n
(LF) for canonical request formatting - Empty Body: Use empty string, not null, for requests without body
- URL Encoding: Use the exact path from the URL, not URL-encoded version
Debug Mode
Enable debug logging to troubleshoot authentication issues:
authentication:
hmac:
debug: true # Only for development
This will log canonical requests and signatures (without revealing the secret).
Remember: HMAC authentication provides strong security when implemented correctly. Always use HTTPS in production to protect against man-in-the-middle attacks.