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.
This commit is contained in:
65
app/Http/Middleware/WebhookRateLimit.php
Normal file
65
app/Http/Middleware/WebhookRateLimit.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user