Files
musadaq-saas/app/Middleware/HmacMiddleware.php

63 lines
2.1 KiB
PHP

<?php
/**
* HMAC Request Signature Middleware
*
* Verifies that incoming requests are signed with a shared secret,
* preventing replay attacks and ensuring request integrity.
*
* Client must send:
* X-Timestamp: Unix timestamp (seconds)
* X-HMAC-Signature: HMAC-SHA256(timestamp + "." + raw_body, HMAC_SECRET_KEY)
*/
declare(strict_types=1);
namespace App\Middleware;
use App\Core\Security;
final class HmacMiddleware
{
/**
* @param int $maxAgeSeconds Max age for replay attack window (default: 5 minutes)
*/
public static function verify(int $maxAgeSeconds = 300): void
{
$headers = getallheaders();
$signature = $headers['X-HMAC-Signature'] ?? $headers['x-hmac-signature'] ?? '';
$timestamp = $headers['X-Timestamp'] ?? $headers['x-timestamp'] ?? '';
// 1. Ensure both headers are present
if (empty($signature) || empty($timestamp)) {
json_error('Missing HMAC signature or timestamp', 401);
}
// 2. Validate timestamp is numeric
if (!ctype_digit((string)$timestamp)) {
json_error('Invalid timestamp format', 401);
}
// 3. Replay attack prevention — reject stale requests
$age = abs(time() - (int)$timestamp);
if ($age > $maxAgeSeconds) {
json_error('Request expired. Check your system clock.', 401);
}
// 4. Build the expected signature
$body = file_get_contents('php://input');
$payload = $timestamp . '.' . $body;
$secret = env('HMAC_SECRET_KEY');
if (!$secret || strlen($secret) < 32) {
error_log('FATAL: HMAC_SECRET_KEY is missing or too short in .env');
json_error('Server configuration error', 500);
}
// 5. Verify using constant-time comparison (prevents timing attacks)
if (!Security::verifySignature($payload, $signature, $secret)) {
error_log("HMAC verification failed for " . ($_SERVER['REQUEST_URI'] ?? ''));
json_error('Invalid request signature', 401);
}
}
}