zai_payment 2.9.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/zai_payment/version.rb +1 -1
- metadata +6 -43
- data/.yardopts +0 -3
- data/IMPLEMENTATION_SUMMARY.md +0 -183
- data/RESPONSE_FORMAT_CORRECTION.md +0 -75
- data/Rakefile +0 -12
- data/badges/.gitkeep +0 -2
- data/badges/coverage.json +0 -1
- data/changelog.md +0 -750
- data/code_of_conduct.md +0 -132
- data/contributing.md +0 -383
- data/docs/architecture.md +0 -232
- data/docs/authentication.md +0 -647
- data/docs/bank_accounts.md +0 -496
- data/docs/batch_transactions.md +0 -340
- data/docs/bpay_accounts.md +0 -519
- data/docs/direct_api_usage.md +0 -489
- data/docs/items.md +0 -1241
- data/docs/pay_ids.md +0 -777
- data/docs/readme.md +0 -111
- data/docs/token_auths.md +0 -523
- data/docs/user_id_field.md +0 -284
- data/docs/user_quick_reference.md +0 -230
- data/docs/users.md +0 -750
- data/docs/virtual_accounts.md +0 -916
- data/docs/wallet_accounts.md +0 -493
- data/docs/webhook_security_quickstart.md +0 -136
- data/docs/webhook_signature.md +0 -244
- data/docs/webhooks.md +0 -417
- data/examples/bank_accounts.md +0 -637
- data/examples/batch_transactions.md +0 -450
- data/examples/bpay_accounts.md +0 -642
- data/examples/items.md +0 -2713
- data/examples/pay_ids.md +0 -871
- data/examples/rails_card_payment.md +0 -1252
- data/examples/token_auths.md +0 -687
- data/examples/users.md +0 -765
- data/examples/virtual_accounts.md +0 -1530
- data/examples/wallet_accounts.md +0 -733
- data/examples/webhooks.md +0 -635
- data/readme.md +0 -357
- data/sig/zai_payment.rbs +0 -4
data/docs/webhook_signature.md
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
# Webhook Signature Verification Implementation
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This document summarizes the implementation of webhook signature verification for the Zai Payment Ruby gem, following Zai's security specifications.
|
|
6
|
-
|
|
7
|
-
## What Was Implemented
|
|
8
|
-
|
|
9
|
-
### 1. Core Functionality
|
|
10
|
-
|
|
11
|
-
#### `create_secret_key(secret_key:)`
|
|
12
|
-
- Creates and registers a secret key with Zai for webhook signature generation
|
|
13
|
-
- **Validation**:
|
|
14
|
-
- Minimum 32 bytes
|
|
15
|
-
- ASCII characters only
|
|
16
|
-
- **Endpoint**: `POST /webhooks/secret_key`
|
|
17
|
-
- **Returns**: Response object with registered secret key
|
|
18
|
-
|
|
19
|
-
#### `verify_signature(payload:, signature_header:, secret_key:, tolerance: 300)`
|
|
20
|
-
- Verifies that a webhook request came from Zai
|
|
21
|
-
- **Features**:
|
|
22
|
-
- HMAC SHA256 signature verification
|
|
23
|
-
- Timestamp validation (prevents replay attacks)
|
|
24
|
-
- Constant-time comparison (prevents timing attacks)
|
|
25
|
-
- Support for multiple signatures in header
|
|
26
|
-
- Configurable tolerance window (default: 5 minutes)
|
|
27
|
-
- **Returns**: `true` if valid, `false` if invalid
|
|
28
|
-
- **Raises**: `ValidationError` for malformed headers or expired timestamps
|
|
29
|
-
|
|
30
|
-
#### `generate_signature(payload, secret_key, timestamp = Time.now.to_i)`
|
|
31
|
-
- Utility method to generate signatures
|
|
32
|
-
- Useful for testing and webhook simulation
|
|
33
|
-
- Uses HMAC SHA256 with base64url encoding (no padding)
|
|
34
|
-
- **Returns**: Base64url-encoded signature string
|
|
35
|
-
|
|
36
|
-
### 2. Security Best Practices
|
|
37
|
-
|
|
38
|
-
✅ **HMAC SHA256**: Industry-standard cryptographic hashing
|
|
39
|
-
✅ **Constant-time comparison**: Prevents timing attacks
|
|
40
|
-
✅ **Timestamp validation**: Prevents replay attacks
|
|
41
|
-
✅ **Base64 URL-safe encoding**: Compatible with HTTP headers
|
|
42
|
-
✅ **Configurable tolerance**: Flexible for network latency
|
|
43
|
-
✅ **Multi-signature support**: Handles key rotation scenarios
|
|
44
|
-
|
|
45
|
-
### 3. Test Coverage
|
|
46
|
-
|
|
47
|
-
**Total Tests**: 95 (all passing ✅)
|
|
48
|
-
**New Tests**: 56 test cases for webhook signature verification
|
|
49
|
-
|
|
50
|
-
Test categories:
|
|
51
|
-
- ✅ Secret key creation (valid, invalid, missing)
|
|
52
|
-
- ✅ Signature generation (known values, default timestamp)
|
|
53
|
-
- ✅ Signature verification (valid, invalid, expired)
|
|
54
|
-
- ✅ Header parsing (malformed, missing components)
|
|
55
|
-
- ✅ Multiple signatures handling
|
|
56
|
-
- ✅ Edge cases and error scenarios
|
|
57
|
-
|
|
58
|
-
**RSpec Compliance**: All test blocks follow the requirement of max 2 examples per `it` block.
|
|
59
|
-
|
|
60
|
-
### 4. Documentation
|
|
61
|
-
|
|
62
|
-
#### Updated Files:
|
|
63
|
-
1. **`docs/webhooks.md`** (170+ lines added)
|
|
64
|
-
- Complete security section
|
|
65
|
-
- Step-by-step setup guide
|
|
66
|
-
- Rails controller examples
|
|
67
|
-
- Troubleshooting guide
|
|
68
|
-
- Security best practices
|
|
69
|
-
- References to official Zai documentation
|
|
70
|
-
|
|
71
|
-
2. **`examples/webhooks.md`** (400+ lines added)
|
|
72
|
-
- Complete workflow from setup to testing
|
|
73
|
-
- Rails controller implementation
|
|
74
|
-
- RSpec test examples
|
|
75
|
-
- Sinatra example
|
|
76
|
-
- Rack middleware example
|
|
77
|
-
- Background job processing pattern
|
|
78
|
-
- Idempotency pattern
|
|
79
|
-
|
|
80
|
-
3. **`readme.md`** (40+ lines added)
|
|
81
|
-
- Quick start guide
|
|
82
|
-
- Security features highlights
|
|
83
|
-
- Simple usage example
|
|
84
|
-
|
|
85
|
-
### 5. Implementation Details
|
|
86
|
-
|
|
87
|
-
#### File Structure:
|
|
88
|
-
```
|
|
89
|
-
lib/zai_payment/resources/webhook.rb
|
|
90
|
-
├── Public Methods
|
|
91
|
-
│ ├── create_secret_key(secret_key:)
|
|
92
|
-
│ ├── verify_signature(payload:, signature_header:, secret_key:, tolerance:)
|
|
93
|
-
│ └── generate_signature(payload, secret_key, timestamp)
|
|
94
|
-
└── Private Methods
|
|
95
|
-
├── validate_secret_key!(secret_key)
|
|
96
|
-
├── parse_signature_header(header)
|
|
97
|
-
├── verify_timestamp!(timestamp, tolerance)
|
|
98
|
-
└── secure_compare(a, b)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
#### Algorithm Implementation:
|
|
102
|
-
|
|
103
|
-
1. **Signature Generation**:
|
|
104
|
-
```ruby
|
|
105
|
-
signed_payload = "#{timestamp}.#{payload}"
|
|
106
|
-
digest = OpenSSL::Digest.new('sha256')
|
|
107
|
-
hash = OpenSSL::HMAC.digest(digest, secret_key, signed_payload)
|
|
108
|
-
signature = Base64.urlsafe_encode64(hash, padding: false)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
2. **Verification Process**:
|
|
112
|
-
- Parse header: `t=timestamp,v=signature`
|
|
113
|
-
- Validate timestamp is within tolerance
|
|
114
|
-
- Generate expected signature
|
|
115
|
-
- Compare using constant-time comparison
|
|
116
|
-
- Return true if any signature matches
|
|
117
|
-
|
|
118
|
-
## Usage Examples
|
|
119
|
-
|
|
120
|
-
### Basic Setup
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
require 'securerandom'
|
|
124
|
-
|
|
125
|
-
# 1. Generate secret key
|
|
126
|
-
secret_key = SecureRandom.alphanumeric(32)
|
|
127
|
-
|
|
128
|
-
# 2. Register with Zai
|
|
129
|
-
ZaiPayment.webhooks.create_secret_key(secret_key: secret_key)
|
|
130
|
-
|
|
131
|
-
# 3. Store securely
|
|
132
|
-
ENV['ZAI_WEBHOOK_SECRET'] = secret_key
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Rails Controller
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
class WebhooksController < ApplicationController
|
|
139
|
-
skip_before_action :verify_authenticity_token
|
|
140
|
-
|
|
141
|
-
def zai_webhook
|
|
142
|
-
payload = request.body.read
|
|
143
|
-
signature_header = request.headers['Webhooks-signature']
|
|
144
|
-
|
|
145
|
-
if ZaiPayment.webhooks.verify_signature(
|
|
146
|
-
payload: payload,
|
|
147
|
-
signature_header: signature_header,
|
|
148
|
-
secret_key: ENV['ZAI_WEBHOOK_SECRET']
|
|
149
|
-
)
|
|
150
|
-
process_webhook(JSON.parse(payload))
|
|
151
|
-
render json: { status: 'success' }
|
|
152
|
-
else
|
|
153
|
-
render json: { error: 'Invalid signature' }, status: :unauthorized
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Testing
|
|
160
|
-
|
|
161
|
-
```ruby
|
|
162
|
-
RSpec.describe WebhooksController do
|
|
163
|
-
let(:secret_key) { SecureRandom.alphanumeric(32) }
|
|
164
|
-
let(:payload) { { event: 'transaction.updated' }.to_json }
|
|
165
|
-
let(:timestamp) { Time.now.to_i }
|
|
166
|
-
|
|
167
|
-
it 'accepts valid webhooks' do
|
|
168
|
-
signature = ZaiPayment::Resources::Webhook.new.generate_signature(
|
|
169
|
-
payload, secret_key, timestamp
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
request.headers['Webhooks-signature'] = "t=#{timestamp},v=#{signature}"
|
|
173
|
-
post :zai_webhook, body: payload
|
|
174
|
-
|
|
175
|
-
expect(response).to have_http_status(:ok)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## API Reference
|
|
181
|
-
|
|
182
|
-
### Method Signatures
|
|
183
|
-
|
|
184
|
-
```ruby
|
|
185
|
-
# Create secret key
|
|
186
|
-
ZaiPayment.webhooks.create_secret_key(
|
|
187
|
-
secret_key: String # Required, min 32 bytes, ASCII only
|
|
188
|
-
) # => Response
|
|
189
|
-
|
|
190
|
-
# Verify signature
|
|
191
|
-
ZaiPayment.webhooks.verify_signature(
|
|
192
|
-
payload: String, # Required, raw request body
|
|
193
|
-
signature_header: String, # Required, 'Webhooks-signature' header
|
|
194
|
-
secret_key: String, # Required, your secret key
|
|
195
|
-
tolerance: Integer # Optional, default: 300 seconds
|
|
196
|
-
) # => Boolean
|
|
197
|
-
|
|
198
|
-
# Generate signature
|
|
199
|
-
ZaiPayment.webhooks.generate_signature(
|
|
200
|
-
payload, # String, request body
|
|
201
|
-
secret_key, # String, your secret key
|
|
202
|
-
timestamp = Time.now.to_i # Integer, Unix timestamp
|
|
203
|
-
) # => String (base64url-encoded signature)
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## Standards Compliance
|
|
207
|
-
|
|
208
|
-
✅ **Zai API Specification**: Follows [official documentation](https://developer.hellozai.com/docs/verify-webhook-signatures)
|
|
209
|
-
✅ **RFC 2104**: HMAC implementation
|
|
210
|
-
✅ **RFC 4648**: Base64url encoding
|
|
211
|
-
✅ **OWASP Best Practices**: Timing attack prevention
|
|
212
|
-
|
|
213
|
-
## Testing
|
|
214
|
-
|
|
215
|
-
Run all tests:
|
|
216
|
-
```bash
|
|
217
|
-
bundle exec rspec
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Run webhook tests only:
|
|
221
|
-
```bash
|
|
222
|
-
bundle exec rspec spec/zai_payment/resources/webhook_spec.rb
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## References
|
|
226
|
-
|
|
227
|
-
- [Zai Webhook Signature Documentation](https://developer.hellozai.com/docs/verify-webhook-signatures)
|
|
228
|
-
- [Create Secret Key API](https://developer.hellozai.com/reference/createsecretkey)
|
|
229
|
-
- [Ruby OpenSSL Documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/openssl/rdoc/OpenSSL/HMAC.html)
|
|
230
|
-
|
|
231
|
-
## Next Steps
|
|
232
|
-
|
|
233
|
-
1. ✅ Implementation complete
|
|
234
|
-
2. ✅ Tests passing (95/95)
|
|
235
|
-
3. ✅ Documentation complete
|
|
236
|
-
4. 🔄 Ready for code review
|
|
237
|
-
5. 📦 Ready for release
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
**Implementation Date**: October 22, 2025
|
|
242
|
-
**Test Coverage**: 100%
|
|
243
|
-
**Standards**: OWASP, RFC 2104, RFC 4648
|
|
244
|
-
|
data/docs/webhooks.md
DELETED
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
# Zai Payment Webhook Implementation
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
This document provides a summary of the webhook implementation in the zai_payment gem.
|
|
5
|
-
|
|
6
|
-
## Architecture
|
|
7
|
-
|
|
8
|
-
### Core Components
|
|
9
|
-
|
|
10
|
-
1. **Client** (`lib/zai_payment/client.rb`)
|
|
11
|
-
- Base HTTP client for making API requests
|
|
12
|
-
- Handles authentication automatically via TokenProvider
|
|
13
|
-
- Supports GET, POST, PATCH, DELETE methods
|
|
14
|
-
- Manages connection with proper headers and JSON encoding/decoding
|
|
15
|
-
|
|
16
|
-
2. **Response** (`lib/zai_payment/response.rb`)
|
|
17
|
-
- Wraps Faraday responses
|
|
18
|
-
- Provides convenient methods: `success?`, `client_error?`, `server_error?`
|
|
19
|
-
- Automatically raises appropriate errors based on HTTP status
|
|
20
|
-
- Extracts data and metadata from response body
|
|
21
|
-
|
|
22
|
-
3. **Webhook Resource** (`lib/zai_payment/resources/webhook.rb`)
|
|
23
|
-
- Implements all CRUD operations for webhooks
|
|
24
|
-
- Full input validation
|
|
25
|
-
- Clean, documented API
|
|
26
|
-
|
|
27
|
-
4. **Enhanced Error Handling** (`lib/zai_payment/errors.rb`)
|
|
28
|
-
- Specific error classes for different scenarios
|
|
29
|
-
- Makes debugging and error handling easier
|
|
30
|
-
|
|
31
|
-
## API Methods
|
|
32
|
-
|
|
33
|
-
### List Webhooks
|
|
34
|
-
```ruby
|
|
35
|
-
ZaiPayment.webhooks.list(limit: 10, offset: 0)
|
|
36
|
-
```
|
|
37
|
-
- Returns paginated list of webhooks
|
|
38
|
-
- Response includes `data` (array of webhooks) and `meta` (pagination info)
|
|
39
|
-
|
|
40
|
-
### Show Webhook
|
|
41
|
-
```ruby
|
|
42
|
-
ZaiPayment.webhooks.show(webhook_id)
|
|
43
|
-
```
|
|
44
|
-
- Returns details of a specific webhook
|
|
45
|
-
- Raises `NotFoundError` if webhook doesn't exist
|
|
46
|
-
|
|
47
|
-
### Create Webhook
|
|
48
|
-
```ruby
|
|
49
|
-
ZaiPayment.webhooks.create(
|
|
50
|
-
url: 'https://example.com/webhook',
|
|
51
|
-
object_type: 'transactions',
|
|
52
|
-
enabled: true,
|
|
53
|
-
description: 'Optional description'
|
|
54
|
-
)
|
|
55
|
-
```
|
|
56
|
-
- Validates URL format
|
|
57
|
-
- Validates required fields
|
|
58
|
-
- Returns created webhook with ID
|
|
59
|
-
|
|
60
|
-
### Update Webhook
|
|
61
|
-
```ruby
|
|
62
|
-
ZaiPayment.webhooks.update(
|
|
63
|
-
webhook_id,
|
|
64
|
-
url: 'https://example.com/new-webhook',
|
|
65
|
-
enabled: false
|
|
66
|
-
)
|
|
67
|
-
```
|
|
68
|
-
- All fields are optional
|
|
69
|
-
- Only updates provided fields
|
|
70
|
-
- Validates URL format if URL is provided
|
|
71
|
-
|
|
72
|
-
### Delete Webhook
|
|
73
|
-
```ruby
|
|
74
|
-
ZaiPayment.webhooks.delete(webhook_id)
|
|
75
|
-
```
|
|
76
|
-
- Permanently deletes the webhook
|
|
77
|
-
- Returns 204 No Content on success
|
|
78
|
-
|
|
79
|
-
## Error Handling
|
|
80
|
-
|
|
81
|
-
The gem provides specific error classes:
|
|
82
|
-
|
|
83
|
-
| Error Class | HTTP Status | Description |
|
|
84
|
-
|------------|-------------|-------------|
|
|
85
|
-
| `ValidationError` | 400, 422 | Invalid input data |
|
|
86
|
-
| `UnauthorizedError` | 401 | Authentication failed |
|
|
87
|
-
| `ForbiddenError` | 403 | Access denied |
|
|
88
|
-
| `NotFoundError` | 404 | Resource not found |
|
|
89
|
-
| `RateLimitError` | 429 | Too many requests |
|
|
90
|
-
| `ServerError` | 5xx | Server-side error |
|
|
91
|
-
| `TimeoutError` | - | Request timeout |
|
|
92
|
-
| `ConnectionError` | - | Connection failed |
|
|
93
|
-
|
|
94
|
-
Example:
|
|
95
|
-
```ruby
|
|
96
|
-
begin
|
|
97
|
-
response = ZaiPayment.webhooks.create(...)
|
|
98
|
-
rescue ZaiPayment::Errors::ValidationError => e
|
|
99
|
-
puts "Validation failed: #{e.message}"
|
|
100
|
-
rescue ZaiPayment::Errors::UnauthorizedError => e
|
|
101
|
-
puts "Authentication failed: #{e.message}"
|
|
102
|
-
end
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Best Practices Implemented
|
|
106
|
-
|
|
107
|
-
1. **Single Responsibility**: Each class has a clear, focused purpose
|
|
108
|
-
2. **DRY (Don't Repeat Yourself)**: Client and Response classes are reusable
|
|
109
|
-
3. **Error Handling**: Comprehensive error handling with specific error classes
|
|
110
|
-
4. **Input Validation**: All inputs are validated before making API calls
|
|
111
|
-
5. **Documentation**: Inline documentation with examples
|
|
112
|
-
6. **Testing**: Comprehensive test coverage using RSpec
|
|
113
|
-
7. **Thread Safety**: TokenProvider uses mutex for thread-safe token refresh
|
|
114
|
-
8. **Configuration**: Centralized configuration management
|
|
115
|
-
9. **RESTful Design**: Follows REST principles for resource management
|
|
116
|
-
10. **Response Wrapping**: Consistent response format across all methods
|
|
117
|
-
|
|
118
|
-
## Usage Examples
|
|
119
|
-
|
|
120
|
-
See `examples/webhooks.rb` for complete examples including:
|
|
121
|
-
- Basic CRUD operations
|
|
122
|
-
- Pagination
|
|
123
|
-
- Error handling
|
|
124
|
-
- Custom client instances
|
|
125
|
-
|
|
126
|
-
## Testing
|
|
127
|
-
|
|
128
|
-
Run the webhook tests:
|
|
129
|
-
```bash
|
|
130
|
-
bundle exec rspec spec/zai_payment/resources/webhook_spec.rb
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
The test suite covers:
|
|
134
|
-
- All CRUD operations
|
|
135
|
-
- Success and error scenarios
|
|
136
|
-
- Input validation
|
|
137
|
-
- Error handling
|
|
138
|
-
- Edge cases
|
|
139
|
-
|
|
140
|
-
## Future Enhancements
|
|
141
|
-
|
|
142
|
-
Potential improvements for future versions:
|
|
143
|
-
1. Webhook job management (list jobs, show job details)
|
|
144
|
-
2. ~~Webhook signature verification~~ ✅ **Implemented**
|
|
145
|
-
3. Webhook retry logic
|
|
146
|
-
4. Bulk operations
|
|
147
|
-
5. Async webhook operations
|
|
148
|
-
|
|
149
|
-
## Webhook Security: Signature Verification
|
|
150
|
-
|
|
151
|
-
### Overview
|
|
152
|
-
|
|
153
|
-
Webhook signature verification ensures that webhook requests truly come from Zai and haven't been tampered with. This protection guards against:
|
|
154
|
-
- **Man-in-the-middle attacks**: Verify the sender is Zai
|
|
155
|
-
- **Replay attacks**: Timestamp verification prevents old webhooks from being reused
|
|
156
|
-
- **Data tampering**: HMAC ensures the payload hasn't been modified
|
|
157
|
-
|
|
158
|
-
### Setup
|
|
159
|
-
|
|
160
|
-
#### Step 1: Generate and Store a Secret Key
|
|
161
|
-
|
|
162
|
-
First, create a secret key that will be shared between you and Zai:
|
|
163
|
-
|
|
164
|
-
```ruby
|
|
165
|
-
require 'securerandom'
|
|
166
|
-
|
|
167
|
-
# Generate a cryptographically secure secret key (at least 32 bytes)
|
|
168
|
-
secret_key = SecureRandom.alphanumeric(32)
|
|
169
|
-
|
|
170
|
-
# Store this securely in your environment variables
|
|
171
|
-
# DO NOT commit this to version control!
|
|
172
|
-
ENV['ZAI_WEBHOOK_SECRET'] = secret_key
|
|
173
|
-
|
|
174
|
-
# Register the secret key with Zai
|
|
175
|
-
response = ZaiPayment.webhooks.create_secret_key(secret_key: secret_key)
|
|
176
|
-
|
|
177
|
-
if response.success?
|
|
178
|
-
puts "Secret key registered successfully!"
|
|
179
|
-
end
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
**Important Security Notes:**
|
|
183
|
-
- Store the secret key in environment variables or a secure vault (e.g., AWS Secrets Manager, HashiCorp Vault)
|
|
184
|
-
- Never commit the secret key to version control
|
|
185
|
-
- Rotate the secret key periodically
|
|
186
|
-
- Use at least 32 bytes for the secret key
|
|
187
|
-
|
|
188
|
-
#### Step 2: Verify Webhook Signatures
|
|
189
|
-
|
|
190
|
-
In your webhook endpoint, verify each incoming request:
|
|
191
|
-
|
|
192
|
-
```ruby
|
|
193
|
-
# Rails example
|
|
194
|
-
class WebhooksController < ApplicationController
|
|
195
|
-
skip_before_action :verify_authenticity_token
|
|
196
|
-
|
|
197
|
-
def zai_webhook
|
|
198
|
-
payload = request.body.read
|
|
199
|
-
signature_header = request.headers['Webhooks-signature']
|
|
200
|
-
secret_key = ENV['ZAI_WEBHOOK_SECRET']
|
|
201
|
-
|
|
202
|
-
begin
|
|
203
|
-
# Verify the signature
|
|
204
|
-
if ZaiPayment.webhooks.verify_signature(
|
|
205
|
-
payload: payload,
|
|
206
|
-
signature_header: signature_header,
|
|
207
|
-
secret_key: secret_key,
|
|
208
|
-
tolerance: 300 # 5 minutes
|
|
209
|
-
)
|
|
210
|
-
# Signature is valid, process the webhook
|
|
211
|
-
webhook_data = JSON.parse(payload)
|
|
212
|
-
process_webhook(webhook_data)
|
|
213
|
-
|
|
214
|
-
render json: { status: 'success' }, status: :ok
|
|
215
|
-
else
|
|
216
|
-
# Invalid signature
|
|
217
|
-
render json: { error: 'Invalid signature' }, status: :unauthorized
|
|
218
|
-
end
|
|
219
|
-
rescue ZaiPayment::Errors::ValidationError => e
|
|
220
|
-
# Signature verification failed (e.g., timestamp too old)
|
|
221
|
-
Rails.logger.error "Webhook signature verification failed: #{e.message}"
|
|
222
|
-
render json: { error: e.message }, status: :unauthorized
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
private
|
|
227
|
-
|
|
228
|
-
def process_webhook(data)
|
|
229
|
-
# Your webhook processing logic here
|
|
230
|
-
Rails.logger.info "Processing webhook: #{data['event']}"
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### How It Works
|
|
236
|
-
|
|
237
|
-
The verification process follows these steps:
|
|
238
|
-
|
|
239
|
-
1. **Extract Components**: Parse the `Webhooks-signature` header to get timestamp and signature(s)
|
|
240
|
-
- Header format: `t=1257894000,v=signature1,v=signature2`
|
|
241
|
-
|
|
242
|
-
2. **Verify Timestamp**: Check that the webhook isn't too old (prevents replay attacks)
|
|
243
|
-
- Default tolerance: 300 seconds (5 minutes)
|
|
244
|
-
- Configurable via the `tolerance` parameter
|
|
245
|
-
|
|
246
|
-
3. **Generate Expected Signature**: Create HMAC SHA256 signature
|
|
247
|
-
- Signed payload: `timestamp.request_body`
|
|
248
|
-
- Uses base64url encoding (URL-safe, no padding)
|
|
249
|
-
|
|
250
|
-
4. **Compare Signatures**: Use constant-time comparison to prevent timing attacks
|
|
251
|
-
- Returns `true` if any signature in the header matches
|
|
252
|
-
|
|
253
|
-
### Advanced Examples
|
|
254
|
-
|
|
255
|
-
#### Custom Tolerance Window
|
|
256
|
-
|
|
257
|
-
```ruby
|
|
258
|
-
# Allow webhooks up to 10 minutes old
|
|
259
|
-
ZaiPayment.webhooks.verify_signature(
|
|
260
|
-
payload: payload,
|
|
261
|
-
signature_header: signature_header,
|
|
262
|
-
secret_key: secret_key,
|
|
263
|
-
tolerance: 600 # 10 minutes
|
|
264
|
-
)
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
#### Generate Signatures for Testing
|
|
268
|
-
|
|
269
|
-
```ruby
|
|
270
|
-
# Generate a signature for testing your webhook endpoint
|
|
271
|
-
payload = '{"event": "transaction.updated", "id": "txn_123"}'
|
|
272
|
-
secret_key = ENV['ZAI_WEBHOOK_SECRET']
|
|
273
|
-
timestamp = Time.now.to_i
|
|
274
|
-
|
|
275
|
-
signature = ZaiPayment.webhooks.generate_signature(payload, secret_key, timestamp)
|
|
276
|
-
signature_header = "t=#{timestamp},v=#{signature}"
|
|
277
|
-
|
|
278
|
-
# Now use this in your test request
|
|
279
|
-
# This is useful for integration tests
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
#### Handling Multiple Signatures
|
|
283
|
-
|
|
284
|
-
Zai may include multiple signatures in the header (e.g., during key rotation):
|
|
285
|
-
|
|
286
|
-
```ruby
|
|
287
|
-
# The verify_signature method automatically handles multiple signatures
|
|
288
|
-
# It returns true if ANY signature matches
|
|
289
|
-
signature_header = "t=1257894000,v=old_sig,v=new_sig"
|
|
290
|
-
result = ZaiPayment.webhooks.verify_signature(
|
|
291
|
-
payload: payload,
|
|
292
|
-
signature_header: signature_header,
|
|
293
|
-
secret_key: secret_key
|
|
294
|
-
)
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Testing Your Implementation
|
|
298
|
-
|
|
299
|
-
Create a test to ensure your webhook endpoint properly validates signatures:
|
|
300
|
-
|
|
301
|
-
```ruby
|
|
302
|
-
require 'rails_helper'
|
|
303
|
-
|
|
304
|
-
RSpec.describe WebhooksController, type: :controller do
|
|
305
|
-
let(:secret_key) { SecureRandom.alphanumeric(32) }
|
|
306
|
-
let(:payload) { { event: 'transaction.updated', id: 'txn_123' }.to_json }
|
|
307
|
-
let(:timestamp) { Time.now.to_i }
|
|
308
|
-
|
|
309
|
-
before do
|
|
310
|
-
ENV['ZAI_WEBHOOK_SECRET'] = secret_key
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
describe 'POST #zai_webhook' do
|
|
314
|
-
context 'with valid signature' do
|
|
315
|
-
it 'processes the webhook' do
|
|
316
|
-
signature = ZaiPayment::Resources::Webhook.new.generate_signature(
|
|
317
|
-
payload, secret_key, timestamp
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
request.headers['Webhooks-signature'] = "t=#{timestamp},v=#{signature}"
|
|
321
|
-
post :zai_webhook, body: payload
|
|
322
|
-
|
|
323
|
-
expect(response).to have_http_status(:ok)
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
context 'with invalid signature' do
|
|
328
|
-
it 'rejects the webhook' do
|
|
329
|
-
request.headers['Webhooks-signature'] = "t=#{timestamp},v=invalid_sig"
|
|
330
|
-
post :zai_webhook, body: payload
|
|
331
|
-
|
|
332
|
-
expect(response).to have_http_status(:unauthorized)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Troubleshooting
|
|
340
|
-
|
|
341
|
-
#### Common Issues
|
|
342
|
-
|
|
343
|
-
1. **"Invalid signature header: missing or invalid timestamp"**
|
|
344
|
-
- Ensure the header format is correct: `t=timestamp,v=signature`
|
|
345
|
-
- Check that timestamp is a valid Unix timestamp
|
|
346
|
-
|
|
347
|
-
2. **"Webhook timestamp is outside tolerance"**
|
|
348
|
-
- Check your server's clock synchronization (use NTP)
|
|
349
|
-
- Increase the tolerance if network latency is high
|
|
350
|
-
- Log the timestamp difference to diagnose timing issues
|
|
351
|
-
|
|
352
|
-
3. **Signature doesn't match**
|
|
353
|
-
- Verify you're using the raw request body (not parsed JSON)
|
|
354
|
-
- Ensure the secret key matches what you registered with Zai
|
|
355
|
-
- Check for any character encoding issues
|
|
356
|
-
|
|
357
|
-
#### Debugging Tips
|
|
358
|
-
|
|
359
|
-
```ruby
|
|
360
|
-
# Enable detailed logging for debugging
|
|
361
|
-
def verify_webhook_with_logging(payload, signature_header, secret_key)
|
|
362
|
-
webhook = ZaiPayment::Resources::Webhook.new
|
|
363
|
-
|
|
364
|
-
begin
|
|
365
|
-
# Extract timestamp and signature
|
|
366
|
-
timestamp = signature_header.match(/t=(\d+)/)[1].to_i
|
|
367
|
-
signature = signature_header.match(/v=([^,]+)/)[1]
|
|
368
|
-
|
|
369
|
-
# Log details
|
|
370
|
-
Rails.logger.debug "Webhook timestamp: #{timestamp}"
|
|
371
|
-
Rails.logger.debug "Current time: #{Time.now.to_i}"
|
|
372
|
-
Rails.logger.debug "Time difference: #{Time.now.to_i - timestamp}s"
|
|
373
|
-
Rails.logger.debug "Payload length: #{payload.bytesize} bytes"
|
|
374
|
-
|
|
375
|
-
# Generate expected signature for comparison
|
|
376
|
-
expected = webhook.generate_signature(payload, secret_key, timestamp)
|
|
377
|
-
Rails.logger.debug "Expected signature: #{expected[0..10]}..."
|
|
378
|
-
Rails.logger.debug "Received signature: #{signature[0..10]}..."
|
|
379
|
-
|
|
380
|
-
# Verify
|
|
381
|
-
webhook.verify_signature(
|
|
382
|
-
payload: payload,
|
|
383
|
-
signature_header: signature_header,
|
|
384
|
-
secret_key: secret_key
|
|
385
|
-
)
|
|
386
|
-
rescue => e
|
|
387
|
-
Rails.logger.error "Verification failed: #{e.message}"
|
|
388
|
-
false
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
### Security Best Practices
|
|
394
|
-
|
|
395
|
-
1. **Always Verify Signatures**: Never process webhooks without verification in production
|
|
396
|
-
2. **Use HTTPS**: Ensure your webhook endpoint uses HTTPS
|
|
397
|
-
3. **Implement Rate Limiting**: Protect against DoS attacks
|
|
398
|
-
4. **Log Failed Attempts**: Monitor for suspicious activity
|
|
399
|
-
5. **Rotate Secrets**: Periodically update your secret key
|
|
400
|
-
6. **Use Environment Variables**: Never hardcode secret keys
|
|
401
|
-
7. **Validate Payload**: After verifying the signature, validate the payload structure
|
|
402
|
-
8. **Idempotency**: Design webhook handlers to be idempotent (safe to replay)
|
|
403
|
-
|
|
404
|
-
### References
|
|
405
|
-
|
|
406
|
-
- [Zai Webhook Signature Documentation](https://developer.hellozai.com/docs/verify-webhook-signatures)
|
|
407
|
-
- [Create Secret Key API](https://developer.hellozai.com/reference/createsecretkey)
|
|
408
|
-
|
|
409
|
-
## API Reference
|
|
410
|
-
|
|
411
|
-
For the official Zai API documentation, see:
|
|
412
|
-
- [List Webhooks](https://developer.hellozai.com/reference/getallwebhooks)
|
|
413
|
-
- [Show Webhook](https://developer.hellozai.com/reference/getwebhookbyid)
|
|
414
|
-
- [Create Webhook](https://developer.hellozai.com/reference/createwebhook)
|
|
415
|
-
- [Update Webhook](https://developer.hellozai.com/reference/updatewebhook)
|
|
416
|
-
- [Delete Webhook](https://developer.hellozai.com/reference/deletewebhookbyid)
|
|
417
|
-
|