zai_payment 1.0.2 โ 1.2.0
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/CHANGELOG.md +81 -1
- data/IMPLEMENTATION.md +201 -0
- data/README.md +81 -13
- data/docs/ARCHITECTURE.md +232 -0
- data/docs/AUTHENTICATION.md +647 -0
- data/docs/README.md +81 -0
- data/docs/WEBHOOKS.md +417 -0
- data/docs/WEBHOOK_SECURITY_QUICKSTART.md +141 -0
- data/docs/WEBHOOK_SIGNATURE.md +244 -0
- data/examples/webhooks.md +635 -0
- data/lib/zai_payment/client.rb +116 -0
- data/lib/zai_payment/config.rb +2 -0
- data/lib/zai_payment/errors.rb +19 -0
- data/lib/zai_payment/resources/webhook.rb +331 -0
- data/lib/zai_payment/response.rb +77 -0
- data/lib/zai_payment/version.rb +1 -1
- data/lib/zai_payment.rb +10 -0
- metadata +40 -1
data/docs/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Documentation Index
|
|
2
|
+
|
|
3
|
+
Welcome to the Zai Payment Ruby gem documentation. This guide will help you find the information you need.
|
|
4
|
+
|
|
5
|
+
## ๐ Getting Started
|
|
6
|
+
|
|
7
|
+
**New to the gem?** Start here:
|
|
8
|
+
1. [Main README](../README.md) - Installation and basic configuration
|
|
9
|
+
2. [Authentication Guide](AUTHENTICATION.md) - Get tokens with two approaches (short & long way)
|
|
10
|
+
3. [Webhook Quick Start](WEBHOOK_SECURITY_QUICKSTART.md) - Set up secure webhooks in 5 minutes
|
|
11
|
+
4. [Webhook Examples](../examples/webhooks.md) - Complete usage examples
|
|
12
|
+
|
|
13
|
+
## ๐๏ธ Architecture & Design
|
|
14
|
+
|
|
15
|
+
- [**ARCHITECTURE.md**](ARCHITECTURE.md) - System architecture and design principles
|
|
16
|
+
- [**AUTHENTICATION.md**](AUTHENTICATION.md) - OAuth2 implementation, token management, two approaches
|
|
17
|
+
- [**WEBHOOKS.md**](WEBHOOKS.md) - Webhook implementation details, best practices, and patterns
|
|
18
|
+
|
|
19
|
+
## ๐ Security Guides
|
|
20
|
+
|
|
21
|
+
- [**WEBHOOK_SECURITY_QUICKSTART.md**](WEBHOOK_SECURITY_QUICKSTART.md) - Quick 5-minute security setup guide
|
|
22
|
+
- [**WEBHOOK_SIGNATURE.md**](WEBHOOK_SIGNATURE.md) - Detailed signature verification implementation
|
|
23
|
+
|
|
24
|
+
## ๐ Examples
|
|
25
|
+
|
|
26
|
+
- [**Webhook Examples**](../examples/webhooks.md) - Comprehensive webhook usage examples including:
|
|
27
|
+
- Basic CRUD operations
|
|
28
|
+
- Rails controller implementation
|
|
29
|
+
- Sinatra example
|
|
30
|
+
- Rack middleware
|
|
31
|
+
- Background job processing
|
|
32
|
+
- Idempotency patterns
|
|
33
|
+
|
|
34
|
+
## ๐ Quick Links
|
|
35
|
+
|
|
36
|
+
### Authentication
|
|
37
|
+
- **Getting Started**: [Authentication Guide](AUTHENTICATION.md)
|
|
38
|
+
- **Short Way**: `ZaiPayment.token` (one-liner)
|
|
39
|
+
- **Long Way**: `TokenProvider.new(config: config).bearer_token` (full control)
|
|
40
|
+
|
|
41
|
+
### Webhooks
|
|
42
|
+
- **Setup**: [Quick Start Guide](WEBHOOK_SECURITY_QUICKSTART.md)
|
|
43
|
+
- **Examples**: [Complete Examples](../examples/webhooks.md)
|
|
44
|
+
- **Details**: [Technical Documentation](WEBHOOKS.md)
|
|
45
|
+
- **Security**: [Signature Verification](WEBHOOK_SIGNATURE.md)
|
|
46
|
+
|
|
47
|
+
### External Resources
|
|
48
|
+
- [Zai Developer Portal](https://developer.hellozai.com/)
|
|
49
|
+
- [Zai API Reference](https://developer.hellozai.com/reference)
|
|
50
|
+
- [Webhook Signature Docs](https://developer.hellozai.com/docs/verify-webhook-signatures)
|
|
51
|
+
|
|
52
|
+
## ๐ Documentation Structure
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
docs/
|
|
56
|
+
โโโ README.md # This file - documentation index
|
|
57
|
+
โโโ AUTHENTICATION.md # OAuth2 authentication guide (NEW!)
|
|
58
|
+
โโโ ARCHITECTURE.md # System architecture
|
|
59
|
+
โโโ WEBHOOKS.md # Webhook technical docs
|
|
60
|
+
โโโ WEBHOOK_SECURITY_QUICKSTART.md # Quick security setup
|
|
61
|
+
โโโ WEBHOOK_SIGNATURE.md # Signature implementation
|
|
62
|
+
|
|
63
|
+
examples/
|
|
64
|
+
โโโ webhooks.md # Complete webhook examples
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## ๐ก Tips
|
|
68
|
+
|
|
69
|
+
- **Getting tokens?** Check [AUTHENTICATION.md](AUTHENTICATION.md) for both approaches
|
|
70
|
+
- **Looking for code examples?** Check [examples/webhooks.md](../examples/webhooks.md)
|
|
71
|
+
- **Need quick setup?** See [WEBHOOK_SECURITY_QUICKSTART.md](WEBHOOK_SECURITY_QUICKSTART.md)
|
|
72
|
+
- **Want to understand the design?** Read [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
73
|
+
- **Security details?** Review [WEBHOOK_SIGNATURE.md](WEBHOOK_SIGNATURE.md)
|
|
74
|
+
|
|
75
|
+
## ๐ Need Help?
|
|
76
|
+
|
|
77
|
+
1. Check the relevant documentation section above
|
|
78
|
+
2. Review the [examples](../examples/webhooks.md)
|
|
79
|
+
3. Consult the [Zai API documentation](https://developer.hellozai.com/)
|
|
80
|
+
4. Open an issue on GitHub
|
|
81
|
+
|
data/docs/WEBHOOKS.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Quick Start: Webhook Security
|
|
2
|
+
|
|
3
|
+
## ๐ 5-Minute Setup
|
|
4
|
+
|
|
5
|
+
### Step 1: Generate & Register Secret Key (One Time)
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'securerandom'
|
|
9
|
+
|
|
10
|
+
secret_key = SecureRandom.alphanumeric(32)
|
|
11
|
+
ZaiPayment.webhooks.create_secret_key(secret_key: secret_key)
|
|
12
|
+
|
|
13
|
+
# Save to your .env file
|
|
14
|
+
# ZAI_WEBHOOK_SECRET=your_generated_secret_key
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Step 2: Add Verification to Your Webhook Endpoint
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# app/controllers/webhooks_controller.rb
|
|
21
|
+
class WebhooksController < ApplicationController
|
|
22
|
+
skip_before_action :verify_authenticity_token
|
|
23
|
+
|
|
24
|
+
def zai_webhook
|
|
25
|
+
payload = request.body.read
|
|
26
|
+
signature = request.headers['Webhooks-signature']
|
|
27
|
+
|
|
28
|
+
# โ
Verify signature
|
|
29
|
+
unless verify_webhook(payload, signature)
|
|
30
|
+
return render json: { error: 'Invalid signature' }, status: :unauthorized
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ๐ Process your webhook
|
|
34
|
+
webhook_data = JSON.parse(payload)
|
|
35
|
+
handle_webhook(webhook_data)
|
|
36
|
+
|
|
37
|
+
render json: { status: 'success' }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def verify_webhook(payload, signature)
|
|
43
|
+
ZaiPayment.webhooks.verify_signature(
|
|
44
|
+
payload: payload,
|
|
45
|
+
signature_header: signature,
|
|
46
|
+
secret_key: ENV['ZAI_WEBHOOK_SECRET'],
|
|
47
|
+
tolerance: 300 # 5 minutes
|
|
48
|
+
)
|
|
49
|
+
rescue ZaiPayment::Errors::ValidationError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def handle_webhook(data)
|
|
54
|
+
case data['event']
|
|
55
|
+
when 'transaction.created'
|
|
56
|
+
# Your logic here
|
|
57
|
+
when 'transaction.completed'
|
|
58
|
+
# Your logic here
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 3: Add Route
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# config/routes.rb
|
|
68
|
+
post '/webhooks/zai', to: 'webhooks#zai_webhook'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## ๐งช Testing
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# spec/controllers/webhooks_controller_spec.rb
|
|
75
|
+
RSpec.describe WebhooksController do
|
|
76
|
+
let(:secret_key) { ENV['ZAI_WEBHOOK_SECRET'] }
|
|
77
|
+
let(:payload) { { event: 'transaction.updated' }.to_json }
|
|
78
|
+
|
|
79
|
+
it 'accepts valid webhooks' do
|
|
80
|
+
timestamp = Time.now.to_i
|
|
81
|
+
signature = ZaiPayment::Resources::Webhook.new.generate_signature(
|
|
82
|
+
payload, secret_key, timestamp
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
request.headers['Webhooks-signature'] = "t=#{timestamp},v=#{signature}"
|
|
86
|
+
post :zai_webhook, body: payload
|
|
87
|
+
|
|
88
|
+
expect(response).to have_http_status(:ok)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'rejects invalid signatures' do
|
|
92
|
+
request.headers['Webhooks-signature'] = "t=#{Time.now.to_i},v=bad_signature"
|
|
93
|
+
post :zai_webhook, body: payload
|
|
94
|
+
|
|
95
|
+
expect(response).to have_http_status(:unauthorized)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## ๐ Security Checklist
|
|
101
|
+
|
|
102
|
+
- โ
Secret key is at least 32 bytes
|
|
103
|
+
- โ
Secret key stored in environment variables (not in code)
|
|
104
|
+
- โ
Using HTTPS for webhook endpoint
|
|
105
|
+
- โ
Signature verification before processing
|
|
106
|
+
- โ
Timestamp tolerance configured appropriately
|
|
107
|
+
- โ
Error logging for failed verifications
|
|
108
|
+
- โ
Tests cover both valid and invalid scenarios
|
|
109
|
+
|
|
110
|
+
## ๐ Common Issues
|
|
111
|
+
|
|
112
|
+
### "Invalid signature header: missing or invalid timestamp"
|
|
113
|
+
**Fix**: Ensure header format is `t=timestamp,v=signature`
|
|
114
|
+
|
|
115
|
+
### "Webhook timestamp is outside tolerance"
|
|
116
|
+
**Fix**: Check server clock synchronization or increase tolerance
|
|
117
|
+
|
|
118
|
+
### Signature doesn't match
|
|
119
|
+
**Fix**:
|
|
120
|
+
- Use raw request body (don't parse it first)
|
|
121
|
+
- Verify secret key matches what was registered
|
|
122
|
+
- Check for encoding issues
|
|
123
|
+
|
|
124
|
+
## ๐ Full Documentation
|
|
125
|
+
|
|
126
|
+
- [Complete Setup Guide](docs/WEBHOOKS.md#webhook-security-signature-verification)
|
|
127
|
+
- [More Examples](examples/webhooks.md#webhook-security-complete-setup-guide)
|
|
128
|
+
- [Zai Official Docs](https://developer.hellozai.com/docs/verify-webhook-signatures)
|
|
129
|
+
|
|
130
|
+
## ๐ก Pro Tips
|
|
131
|
+
|
|
132
|
+
1. **Use Background Jobs**: Process webhooks asynchronously for better performance
|
|
133
|
+
2. **Implement Idempotency**: Check if webhook was already processed
|
|
134
|
+
3. **Add Rate Limiting**: Protect against DoS attacks
|
|
135
|
+
4. **Log Everything**: Monitor for suspicious activity
|
|
136
|
+
5. **Test Replay Attacks**: Ensure old webhooks are rejected
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
**Need Help?** See the [full implementation guide](WEBHOOK_SIGNATURE_IMPLEMENTATION.md)
|
|
141
|
+
|