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:
324
docs/polar-webhooks.md
Normal file
324
docs/polar-webhooks.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# Polar Webhook Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This application supports comprehensive webhook integration with Polar.sh for real-time subscription management and payment processing. The webhook system handles subscription lifecycle events, payment confirmations, and customer status updates.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Polar webhook endpoints are automatically configured based on your payment provider settings in the database. The system supports both sandbox and live environments with separate credentials.
|
||||
|
||||
#### Required Configuration
|
||||
|
||||
1. **Payment Provider Configuration** (stored in database):
|
||||
- `api_key`: Live API key
|
||||
- `webhook_secret`: Live webhook secret (stored as raw text)
|
||||
- `sandbox_api_key`: Sandbox API key (if sandbox mode enabled)
|
||||
- `sandbox_webhook_secret`: Sandbox webhook secret (if sandbox mode enabled, stored as raw text)
|
||||
- `sandbox`: Boolean flag indicating sandbox mode
|
||||
|
||||
**Important**: Store webhook secrets as **raw text** in the database. The system automatically base64 encodes the secret during signature validation as required by Polar's Standard Webhooks implementation.
|
||||
|
||||
2. **Webhook URL**: `https://your-domain.test/webhook/polar`
|
||||
|
||||
### Polar Dashboard Setup
|
||||
|
||||
1. Navigate to your Polar.sh dashboard
|
||||
2. Go to Settings → Webhooks
|
||||
3. Add webhook URL: `https://your-domain.test/webhook/polar`
|
||||
4. Select events to monitor:
|
||||
- `subscription.created`
|
||||
- `subscription.active`
|
||||
- `subscription.cancelled`
|
||||
- `subscription.paused`
|
||||
- `subscription.resumed`
|
||||
- `subscription.trial_will_end`
|
||||
- `subscription.trial_ended`
|
||||
- `customer.state_changed`
|
||||
|
||||
## Security Features
|
||||
|
||||
### Signature Validation
|
||||
|
||||
The webhook system implements Standard Webhooks specification for Polar signature validation:
|
||||
|
||||
- **Headers Expected**:
|
||||
- `webhook-id`: Unique webhook identifier
|
||||
- `webhook-signature`: HMAC-SHA256 signature in format `v1,<signature>`
|
||||
- `webhook-timestamp`: Unix timestamp of request (in seconds)
|
||||
|
||||
- **Validation Implementation**:
|
||||
- Uses built-in `App\Services\Webhooks\StandardWebhooks` class
|
||||
- Created via `App\Services\Webhooks\WebhookFactory::createPolar($secret)`
|
||||
- Follows official Standard Webhooks specification
|
||||
|
||||
- **Validation Process**:
|
||||
1. Extract required headers: `webhook-id`, `webhook-timestamp`, `webhook-signature`
|
||||
2. Verify all required headers are present
|
||||
3. Validate timestamp is within ±5 minutes (replay attack prevention)
|
||||
4. For Polar: use raw webhook secret (base64 encoded during HMAC operations)
|
||||
5. Construct signed payload: `{webhook-id}.{webhook-timestamp}.{raw-request-body}`
|
||||
6. Compute HMAC-SHA256 signature and base64 encode result
|
||||
7. Parse received signature format (`v1,<signature>`) and compare
|
||||
8. Return decoded payload on successful validation
|
||||
|
||||
- **Error Handling**:
|
||||
- `WebhookVerificationException` for validation failures
|
||||
- `WebhookSigningException` for signing errors
|
||||
- Detailed logging for debugging and monitoring
|
||||
|
||||
### Standard Webhooks Implementation
|
||||
|
||||
The application includes a built-in Standard Webhooks validation system:
|
||||
|
||||
- **Location**: `app/Services/Webhooks/`
|
||||
- **Components**:
|
||||
- `StandardWebhooks.php`: Main validation class
|
||||
- `WebhookFactory.php`: Factory for creating provider-specific validators
|
||||
- `WebhookVerificationException.php`: Validation failure exception
|
||||
- `WebhookSigningException.php`: Signing error exception
|
||||
|
||||
- **Multi-Provider Support**:
|
||||
```php
|
||||
// Polar (uses raw secret)
|
||||
$webhook = WebhookFactory::createPolar($secret);
|
||||
|
||||
// Stripe (uses whsec_ prefix)
|
||||
$webhook = WebhookFactory::createStripe($secret);
|
||||
|
||||
// Generic providers
|
||||
$webhook = WebhookFactory::create($secret, $isRaw);
|
||||
```
|
||||
|
||||
- **Benefits**:
|
||||
- Industry-standard webhook validation
|
||||
- Reusable across multiple payment providers
|
||||
- Built-in security features (timestamp tolerance, replay prevention)
|
||||
- Proper exception handling and logging
|
||||
- Testable and maintainable code
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Webhook endpoints are rate-limited to prevent abuse:
|
||||
- **Polar**: 60 requests per minute per IP
|
||||
- **Other providers**: 30-100 requests per minute depending on provider
|
||||
|
||||
Rate limit headers are included in responses:
|
||||
- `X-RateLimit-Limit`: Maximum requests allowed
|
||||
- `X-RateLimit-Remaining`: Requests remaining in current window
|
||||
|
||||
### Idempotency
|
||||
|
||||
Webhook processing is idempotent to prevent duplicate processing:
|
||||
- Each webhook has a unique ID
|
||||
- Processed webhook IDs are cached for 24 hours
|
||||
- Duplicate webhooks are ignored but return success response
|
||||
|
||||
## Supported Events
|
||||
|
||||
### Subscription Events
|
||||
|
||||
#### `subscription.created`
|
||||
- **Trigger**: New subscription created
|
||||
- **Action**: Creates or updates subscription record
|
||||
- **Data**: Customer ID, subscription ID, product details, status
|
||||
|
||||
#### `subscription.active`
|
||||
- **Trigger**: Subscription becomes active
|
||||
- **Action**: Updates subscription status to active
|
||||
- **Data**: Subscription ID, billing period dates, status
|
||||
|
||||
#### `subscription.cancelled`
|
||||
- **Trigger**: Subscription cancelled
|
||||
- **Action**: Updates subscription with cancellation details
|
||||
- **Data**: Cancellation reason, comments, effective date
|
||||
|
||||
#### `subscription.paused`
|
||||
- **Trigger**: Subscription paused
|
||||
- **Action**: Updates subscription status to paused
|
||||
- **Data**: Pause reason, pause date
|
||||
|
||||
#### `subscription.resumed`
|
||||
- **Trigger**: Paused subscription resumed
|
||||
- **Action**: Updates subscription status to active
|
||||
- **Data**: Resume reason, resume date, new billing period
|
||||
|
||||
#### `subscription.trial_will_end`
|
||||
- **Trigger**: Trial period ending soon
|
||||
- **Action**: Updates trial end date
|
||||
- **Data**: Trial end date
|
||||
|
||||
#### `subscription.trial_ended`
|
||||
- **Trigger**: Trial period ended
|
||||
- **Action**: Converts trial to active subscription
|
||||
- **Data**: Trial end date, new billing period
|
||||
|
||||
### Customer Events
|
||||
|
||||
#### `customer.state_changed`
|
||||
- **Trigger**: Customer profile or subscription state changes
|
||||
- **Action**: Updates user's Polar customer ID and syncs active subscriptions
|
||||
- **Data**: Customer details, active subscriptions list, metadata
|
||||
- **Headers**: `webhook-signature`, `webhook-timestamp`, `webhook-id`
|
||||
|
||||
## Subscription Lookup Logic
|
||||
|
||||
The webhook handler uses a sophisticated lookup system to find subscriptions:
|
||||
|
||||
1. **Primary Match**: `provider_subscription_id`
|
||||
2. **Fallback Match**: `provider_checkout_id` (for newly created subscriptions)
|
||||
3. **Customer Binding**: Uses `polar_cust_id` from user record
|
||||
|
||||
This ensures webhooks are processed correctly even when the subscription ID hasn't been populated yet.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Validation Failures
|
||||
- Invalid signatures: HTTP 400
|
||||
- Missing timestamps: HTTP 400
|
||||
- Old/future timestamps: HTTP 400
|
||||
|
||||
### Processing Errors
|
||||
- Malformed payloads: Logged and returns HTTP 200
|
||||
- Missing subscriptions: Logged and returns HTTP 200
|
||||
- Database errors: Logged and returns HTTP 200
|
||||
|
||||
### Logging
|
||||
All webhook activity is logged with:
|
||||
- Webhook ID and event type
|
||||
- Subscription match details
|
||||
- Processing errors
|
||||
- Security violations (rate limits, invalid signatures)
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Suite
|
||||
Comprehensive test suite located at `tests/Feature/Controllers/PolarWebhookTest.php` with 16 test cases covering:
|
||||
|
||||
- Standard Webhooks signature validation
|
||||
- Timestamp validation (±5 minutes tolerance)
|
||||
- Header validation (webhook-id, webhook-timestamp, webhook-signature)
|
||||
- Idempotency (duplicate webhook prevention)
|
||||
- All event handlers (subscription.*, customer.state_changed)
|
||||
- Error scenarios and edge cases
|
||||
- Sandbox mode bypass functionality
|
||||
|
||||
### Test Configuration
|
||||
Tests use database-driven configuration:
|
||||
- Creates `PaymentProvider` model with Polar configuration
|
||||
- Uses `sandbox: false` for validation tests
|
||||
- Properly sets up webhook secrets and API keys
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# Run all Polar webhook tests
|
||||
php artisan test tests/Feature/Controllers/PolarWebhookTest.php
|
||||
|
||||
# Run specific test
|
||||
php artisan test --filter="it_processes_valid_webhook_with_correct_signature"
|
||||
```
|
||||
|
||||
### Test Data
|
||||
Tests use realistic Polar webhook payloads with:
|
||||
- Proper Standard Webhooks signature generation
|
||||
- Correct header names and formats
|
||||
- Database configuration setup
|
||||
- Comprehensive error scenarios
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Webhook Validation Failing
|
||||
**Symptoms**: HTTP 400 responses, `WebhookVerificationException` in logs
|
||||
|
||||
**Solutions**:
|
||||
1. Verify webhook secret is stored as raw text in database (not base64 encoded)
|
||||
2. Check all required headers are present: `webhook-id`, `webhook-timestamp`, `webhook-signature`
|
||||
3. Ensure headers use Standard Webhooks naming (not Polar-specific headers)
|
||||
4. Verify timestamp is in seconds, not milliseconds
|
||||
5. Check that payment provider configuration has `sandbox: false` for production
|
||||
6. Enable debug logging to see detailed validation steps
|
||||
7. Test signature generation using Standard Webhooks format
|
||||
|
||||
**Debug Mode**:
|
||||
Enable detailed webhook logging by setting `LOG_LEVEL=debug` in your environment file. This will provide:
|
||||
- Detailed signature validation steps
|
||||
- Header parsing information
|
||||
- Secret encoding details
|
||||
- Payload construction information
|
||||
|
||||
#### Subscription Not Found
|
||||
**Symptoms**: Logs show "No subscription found" warnings
|
||||
|
||||
**Solutions**:
|
||||
1. Check `provider_subscription_id` in database
|
||||
2. Verify `provider_checkout_id` is set for new subscriptions
|
||||
3. Confirm user has correct `polar_cust_id`
|
||||
4. Check webhook payload contains expected customer/subscription IDs
|
||||
|
||||
#### Rate Limit Exceeded
|
||||
**Symptoms**: HTTP 429 responses
|
||||
|
||||
**Solutions**:
|
||||
1. Check if Polar is sending duplicate webhooks
|
||||
2. Verify webhook endpoint isn't being called by other services
|
||||
3. Monitor rate limit headers in responses
|
||||
|
||||
### Debug Mode
|
||||
Enable detailed webhook logging by setting `LOG_LEVEL=debug` in your environment file. This will provide:
|
||||
- Detailed signature validation steps
|
||||
- Header parsing information
|
||||
- Subscription lookup details
|
||||
- Payload parsing information
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Key Metrics to Monitor
|
||||
- Webhook success rate
|
||||
- Validation failure frequency
|
||||
- Processing errors by type
|
||||
- Rate limit violations
|
||||
- Subscription match success rate
|
||||
|
||||
### Recommended Alerts
|
||||
- High validation failure rate (>5%)
|
||||
- Rate limit violations
|
||||
- Processing errors for critical events (subscription.cancelled)
|
||||
- Webhook endpoint downtime
|
||||
|
||||
## Migration from Other Providers
|
||||
|
||||
The webhook system is designed to handle multiple payment providers. When migrating to Polar:
|
||||
|
||||
1. Update user records with `polar_cust_id`
|
||||
2. Create subscription records with `provider = 'polar'`
|
||||
3. Set proper `provider_subscription_id` and `provider_checkout_id`
|
||||
4. Test webhook processing in sandbox mode
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Webhook endpoints bypass CSRF protection but maintain signature validation
|
||||
- All webhook processing is logged for audit trails
|
||||
- Rate limiting prevents abuse and DoS attacks
|
||||
- Idempotency prevents duplicate processing
|
||||
- Timestamp validation prevents replay attacks
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Webhook processing is optimized for speed with minimal database queries
|
||||
- Cache-based idempotency checking
|
||||
- Efficient subscription lookup with fallback strategies
|
||||
- Background processing for heavy operations (if needed)
|
||||
|
||||
## Support
|
||||
|
||||
For issues with Polar webhook integration:
|
||||
|
||||
1. Check application logs for detailed error information
|
||||
2. Verify Polar dashboard configuration
|
||||
3. Test with Polar's webhook testing tools
|
||||
4. Review this documentation for common solutions
|
||||
5. Check test suite for expected behavior examples
|
||||
Reference in New Issue
Block a user