Causes:
- Payload order wrong (must be:
METHOD\nPATH\nBODY\nTIMESTAMP\nNONCE) - Body mismatch (no pretty-print, empty =
""notnull) - Path missing query params (
/api/users?page=1not/api/users) - Wrong secret or algorithm
$payload = implode("\n", [$method, $path, $body, $timestamp, $nonce]);Causes:
- Clock drift > tolerance (default 300s)
- Using local time instead of Unix timestamp
Fix: Sync with NTP: timedatectl set-ntp true
Cause: Reusing nonce on retry.
Fix: Generate new nonce for each request:
$nonce = bin2hex(random_bytes(16));Check:
php artisan tinker
>>> ApiCredential::where('client_id', 'prod_xxx')->first();
>>> $cred->is_active; // Must be trueCheck:
>>> $cred->expires_at;
>>> $cred->isExpired();Production credentials only work when APP_ENV=production.
Fix: Use correct credentials or disable enforcement:
'enforce_environment' => false, // Not recommendedReset:
>>> app(RateLimiterInterface::class)->reset('prod_xxx');Clear:
redis-cli DEL hmac:ip_failures:192.168.1.100redis-cli ping # Should return PONGphp artisan vendor:publish --tag=hmac-migrations
php artisan migrateAPP_KEY changed after credentials created. Regenerate credentials.
Event::listen(AuthenticationFailed::class, function ($event) {
Log::debug('HMAC failed', [
'reason' => $event->reason->value,
'client_id' => $event->clientId,
]);
});Test signature:
$service = app(SignatureService::class);
$payload = new SignaturePayload('POST', '/api/test', '{}', time(), 'nonce');
$expected = $service->generate($payload, 'secret');