0
# Webhooks & Validation
1
2
Request signature validation for securing webhook endpoints and ensuring requests originate from Twilio. Provides utilities for computing and validating request signatures.
3
4
## Capabilities
5
6
### Request Validation
7
8
Validate incoming webhooks to ensure they originated from Twilio using HMAC-SHA1 signatures.
9
10
```python { .api }
11
class RequestValidator:
12
"""Webhook request signature validator"""
13
14
def __init__(self, auth_token: str):
15
"""
16
Initialize validator with auth token.
17
18
Args:
19
auth_token (str): Twilio Account Auth Token
20
"""
21
22
def validate(
23
self,
24
uri: str,
25
params: dict,
26
signature: str
27
) -> bool:
28
"""
29
Validate webhook request signature.
30
31
Args:
32
uri (str): Full request URI including query string
33
params (dict): POST body parameters or query parameters
34
signature (str): X-Twilio-Signature header value
35
36
Returns:
37
bool: True if signature is valid
38
"""
39
40
def compute_signature(
41
self,
42
uri: str,
43
params: dict
44
) -> str:
45
"""
46
Compute expected signature for request.
47
48
Args:
49
uri (str): Full request URI
50
params (dict): Request parameters
51
52
Returns:
53
str: Expected signature string
54
"""
55
56
def compute_body_hash(self, body: str) -> str:
57
"""
58
Compute SHA256 hash of request body.
59
60
Args:
61
body (str): Raw request body
62
63
Returns:
64
str: Base64-encoded body hash
65
"""
66
```
67
68
### Utility Functions
69
70
Helper functions for request validation and URI manipulation.
71
72
```python { .api }
73
def compare(string1: str, string2: str) -> bool:
74
"""
75
Timing-safe string comparison to prevent timing attacks.
76
77
Args:
78
string1 (str): First string
79
string2 (str): Second string
80
81
Returns:
82
bool: True if strings are equal
83
"""
84
85
def remove_port(uri: str) -> str:
86
"""
87
Remove port number from URI if it's default port.
88
89
Args:
90
uri (str): URI with potential port
91
92
Returns:
93
str: URI with default port removed
94
"""
95
96
def add_port(uri: str) -> str:
97
"""
98
Add default port to URI if missing.
99
100
Args:
101
uri (str): URI potentially missing port
102
103
Returns:
104
str: URI with explicit port
105
"""
106
```
107
108
## Webhook Validation Examples
109
110
### Basic Validation
111
112
Validate incoming webhook requests in web frameworks.
113
114
```python
115
from twilio.request_validator import RequestValidator
116
from flask import Flask, request
117
118
app = Flask(__name__)
119
validator = RequestValidator('your_auth_token')
120
121
@app.route('/webhook', methods=['POST'])
122
def webhook_handler():
123
# Get signature from headers
124
signature = request.headers.get('X-Twilio-Signature', '')
125
126
# Get full URL (important: include protocol and port)
127
url = request.url
128
129
# Get POST parameters
130
params = request.form.to_dict()
131
132
# Validate signature
133
if not validator.validate(url, params, signature):
134
return 'Forbidden', 403
135
136
# Process validated webhook
137
from_number = params.get('From')
138
body = params.get('Body')
139
140
print(f"Valid webhook from {from_number}: {body}")
141
return 'OK', 200
142
```
143
144
### Django Validation
145
146
```python
147
from django.http import HttpResponse, HttpResponseForbidden
148
from django.views.decorators.csrf import csrf_exempt
149
from django.views.decorators.http import require_POST
150
from twilio.request_validator import RequestValidator
151
import json
152
153
validator = RequestValidator('your_auth_token')
154
155
@csrf_exempt
156
@require_POST
157
def twilio_webhook(request):
158
# Get signature
159
signature = request.META.get('HTTP_X_TWILIO_SIGNATURE', '')
160
161
# Build full URL
162
url = request.build_absolute_uri()
163
164
# Get parameters
165
params = {}
166
for key, value in request.POST.items():
167
params[key] = value
168
169
# Validate
170
if not validator.validate(url, params, signature):
171
return HttpResponseForbidden('Invalid signature')
172
173
# Handle webhook
174
call_sid = params.get('CallSid')
175
call_status = params.get('CallStatus')
176
177
return HttpResponse('OK')
178
```
179
180
### Manual Signature Computation
181
182
```python
183
from twilio.request_validator import RequestValidator
184
185
validator = RequestValidator('your_auth_token')
186
187
# Compute signature manually
188
uri = 'https://example.com/webhook'
189
params = {
190
'From': '+15551234567',
191
'To': '+15559876543',
192
'Body': 'Hello World',
193
'MessageSid': 'SMxxxxx'
194
}
195
196
expected_signature = validator.compute_signature(uri, params)
197
print(f"Expected signature: {expected_signature}")
198
199
# Validate against actual signature
200
actual_signature = 'signature_from_header'
201
is_valid = validator.validate(uri, params, actual_signature)
202
print(f"Valid: {is_valid}")
203
```
204
205
### Handling Different Content Types
206
207
```python
208
from twilio.request_validator import RequestValidator
209
from flask import Flask, request
210
import json
211
212
app = Flask(__name__)
213
validator = RequestValidator('your_auth_token')
214
215
@app.route('/webhook', methods=['POST'])
216
def webhook_handler():
217
signature = request.headers.get('X-Twilio-Signature', '')
218
url = request.url
219
220
# Handle form-encoded data (default)
221
if request.content_type == 'application/x-www-form-urlencoded':
222
params = request.form.to_dict()
223
224
# Handle JSON data (for some webhook types)
225
elif request.content_type == 'application/json':
226
# For JSON, validate against raw body
227
body = request.get_data(as_text=True)
228
body_hash = validator.compute_body_hash(body)
229
230
# Add body hash to empty params for validation
231
params = {'bodySHA256': body_hash}
232
233
else:
234
return 'Unsupported content type', 400
235
236
if not validator.validate(url, params, signature):
237
return 'Forbidden', 403
238
239
return 'OK', 200
240
```
241
242
### Debugging Validation Issues
243
244
```python
245
from twilio.request_validator import RequestValidator
246
247
def debug_validation(uri, params, signature, auth_token):
248
"""Debug webhook validation issues"""
249
validator = RequestValidator(auth_token)
250
251
print(f"URI: {uri}")
252
print(f"Params: {params}")
253
print(f"Signature: {signature}")
254
255
# Compute expected signature
256
expected = validator.compute_signature(uri, params)
257
print(f"Expected: {expected}")
258
259
# Check if they match
260
is_valid = validator.validate(uri, params, signature)
261
print(f"Valid: {is_valid}")
262
263
# Common issues to check
264
if not is_valid:
265
print("\nCommon issues to check:")
266
print("1. Ensure URI includes protocol (https://)")
267
print("2. Ensure URI includes correct port (if not 80/443)")
268
print("3. Check for URL encoding differences")
269
print("4. Verify auth token is correct")
270
print("5. Check for proxy/load balancer URI changes")
271
272
# Try with port manipulation
273
uri_with_port = add_port(uri)
274
uri_without_port = remove_port(uri)
275
276
if validator.validate(uri_with_port, params, signature):
277
print(f"✓ Valid with explicit port: {uri_with_port}")
278
elif validator.validate(uri_without_port, params, signature):
279
print(f"✓ Valid without port: {uri_without_port}")
280
281
# Usage
282
debug_validation(
283
uri='https://example.com/webhook',
284
params={'From': '+15551234567', 'Body': 'Test'},
285
signature='signature_from_header',
286
auth_token='your_auth_token'
287
)
288
```
289
290
### Production Validation Patterns
291
292
```python
293
from twilio.request_validator import RequestValidator
294
from functools import wraps
295
import os
296
297
# Initialize validator with environment variable
298
validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))
299
300
def validate_twilio_request(f):
301
"""Decorator for automatic webhook validation"""
302
@wraps(f)
303
def decorated_function(*args, **kwargs):
304
from flask import request, abort
305
306
signature = request.headers.get('X-Twilio-Signature', '')
307
url = request.url
308
params = request.form.to_dict()
309
310
if not validator.validate(url, params, signature):
311
abort(403) # Forbidden
312
313
return f(*args, **kwargs)
314
return decorated_function
315
316
# Usage
317
@app.route('/voice-webhook', methods=['POST'])
318
@validate_twilio_request
319
def handle_voice():
320
# This code only runs for valid Twilio requests
321
from twilio.twiml.voice_response import VoiceResponse
322
323
response = VoiceResponse()
324
response.say("Hello from validated webhook!")
325
return str(response)
326
327
@app.route('/sms-webhook', methods=['POST'])
328
@validate_twilio_request
329
def handle_sms():
330
from twilio.twiml.messaging_response import MessagingResponse
331
332
body = request.form.get('Body', '').strip()
333
334
response = MessagingResponse()
335
response.message(f"You said: {body}")
336
return str(response)
337
```
338
339
## Security Best Practices
340
341
1. **Always validate signatures** in production webhooks
342
2. **Use HTTPS** for all webhook URLs
343
3. **Store auth tokens securely** using environment variables
344
4. **Log validation failures** for monitoring
345
5. **Handle URL encoding consistently** across your application
346
6. **Consider rate limiting** webhook endpoints
347
7. **Validate request origin** using signatures, not IP addresses
348
8. **Use timing-safe comparison** (provided by the validator)
349
9. **Test validation** with Twilio's webhook testing tools
350
10. **Monitor webhook health** and response times