Files
zemailnator/app/Http/Middleware/WebhookRateLimit.php
idevakk 289baa1286 feat(payments): implement standard webhooks validation system
Add comprehensive webhook validation and processing system with Polar.sh integration:

  - Create built-in Standard Webhooks package following official specification
  - Implement HMAC-SHA256 signature validation with base64 encoding
  - Add webhook factory for multi-provider support (Polar, Stripe, generic)
  - Replace custom Polar webhook validation with Standard Webhooks implementation
  - Add proper exception handling with custom WebhookVerificationException
  - Support sandbox mode bypass for development environments
  - Update Polar provider to use database-driven configuration
  - Enhance webhook test suite with proper Standard Webhooks format
  - Add PaymentProvider model HasFactory trait for testing
  - Implement timestamp tolerance checking (±5 minutes) for replay protection
  - Support multiple signature versions and proper header validation

  This provides a secure, reusable webhook validation system that can be extended
  to other payment providers while maintaining full compliance with Standard
  Webhooks specification.

  BREAKING CHANGE: Polar webhook validation now uses Standard Webhooks format
  with headers 'webhook-id', 'webhook-timestamp', 'webhook-signature' instead of
  previous Polar-specific headers.
2025-12-06 22:49:54 -08:00

66 lines
2.2 KiB
PHP

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class WebhookRateLimit
{
/**
* Handle an incoming request.
*
* @param Closure(Request):Response $next
*/
public function handle(Request $request, Closure $next): Response
{
$provider = $request->route('provider');
$clientIp = $request->ip();
// Rate limits per provider (requests per minute)
$rateLimits = [
'polar' => 60, // Polar: 60 requests per minute
'stripe' => 100, // Stripe: 100 requests per minute
'lemon_squeezy' => 60, // Lemon Squeezy: 60 requests per minute
'oxapay' => 30, // OxaPay: 30 requests per minute
'crypto' => 20, // Crypto: 20 requests per minute
];
$rateLimit = $rateLimits[$provider] ?? 30; // Default: 30 requests per minute
// Cache key for rate limiting
$key = "webhook_rate_limit:{$provider}:{$clientIp}";
// Use Laravel's Cache atomic increment for rate limiting
$current = Cache::increment($key, 1, now()->addMinutes(1));
// Check if rate limit exceeded
if ($current > $rateLimit) {
// Log rate limit violation
\Log::warning('Webhook rate limit exceeded', [
'provider' => $provider,
'ip' => $clientIp,
'current' => $current,
'limit' => $rateLimit,
'user_agent' => $request->userAgent(),
'request_size' => strlen($request->getContent()),
]);
return response()->json([
'error' => 'Rate limit exceeded',
'message' => 'Too many webhook requests. Please try again later.',
'retry_after' => 60,
], 429)->header('Retry-After', 60);
}
// Add rate limit headers
$response = $next($request);
$response->headers->set('X-RateLimit-Limit', $rateLimit);
$response->headers->set('X-RateLimit-Remaining', max(0, $rateLimit - $current));
return $response;
}
}