Twilio API client and TwiML generator for comprehensive telecommunications services
Request signature validation for securing webhook endpoints and ensuring requests originate from Twilio. Provides utilities for computing and validating request signatures.
Validate incoming webhooks to ensure they originated from Twilio using HMAC-SHA1 signatures.
class RequestValidator:
"""Webhook request signature validator"""
def __init__(self, auth_token: str):
"""
Initialize validator with auth token.
Args:
auth_token (str): Twilio Account Auth Token
"""
def validate(
self,
uri: str,
params: dict,
signature: str
) -> bool:
"""
Validate webhook request signature.
Args:
uri (str): Full request URI including query string
params (dict): POST body parameters or query parameters
signature (str): X-Twilio-Signature header value
Returns:
bool: True if signature is valid
"""
def compute_signature(
self,
uri: str,
params: dict
) -> str:
"""
Compute expected signature for request.
Args:
uri (str): Full request URI
params (dict): Request parameters
Returns:
str: Expected signature string
"""
def compute_body_hash(self, body: str) -> str:
"""
Compute SHA256 hash of request body.
Args:
body (str): Raw request body
Returns:
str: Base64-encoded body hash
"""Helper functions for request validation and URI manipulation.
def compare(string1: str, string2: str) -> bool:
"""
Timing-safe string comparison to prevent timing attacks.
Args:
string1 (str): First string
string2 (str): Second string
Returns:
bool: True if strings are equal
"""
def remove_port(uri: str) -> str:
"""
Remove port number from URI if it's default port.
Args:
uri (str): URI with potential port
Returns:
str: URI with default port removed
"""
def add_port(uri: str) -> str:
"""
Add default port to URI if missing.
Args:
uri (str): URI potentially missing port
Returns:
str: URI with explicit port
"""Validate incoming webhook requests in web frameworks.
from twilio.request_validator import RequestValidator
from flask import Flask, request
app = Flask(__name__)
validator = RequestValidator('your_auth_token')
@app.route('/webhook', methods=['POST'])
def webhook_handler():
# Get signature from headers
signature = request.headers.get('X-Twilio-Signature', '')
# Get full URL (important: include protocol and port)
url = request.url
# Get POST parameters
params = request.form.to_dict()
# Validate signature
if not validator.validate(url, params, signature):
return 'Forbidden', 403
# Process validated webhook
from_number = params.get('From')
body = params.get('Body')
print(f"Valid webhook from {from_number}: {body}")
return 'OK', 200from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from twilio.request_validator import RequestValidator
import json
validator = RequestValidator('your_auth_token')
@csrf_exempt
@require_POST
def twilio_webhook(request):
# Get signature
signature = request.META.get('HTTP_X_TWILIO_SIGNATURE', '')
# Build full URL
url = request.build_absolute_uri()
# Get parameters
params = {}
for key, value in request.POST.items():
params[key] = value
# Validate
if not validator.validate(url, params, signature):
return HttpResponseForbidden('Invalid signature')
# Handle webhook
call_sid = params.get('CallSid')
call_status = params.get('CallStatus')
return HttpResponse('OK')from twilio.request_validator import RequestValidator
validator = RequestValidator('your_auth_token')
# Compute signature manually
uri = 'https://example.com/webhook'
params = {
'From': '+15551234567',
'To': '+15559876543',
'Body': 'Hello World',
'MessageSid': 'SMxxxxx'
}
expected_signature = validator.compute_signature(uri, params)
print(f"Expected signature: {expected_signature}")
# Validate against actual signature
actual_signature = 'signature_from_header'
is_valid = validator.validate(uri, params, actual_signature)
print(f"Valid: {is_valid}")from twilio.request_validator import RequestValidator
from flask import Flask, request
import json
app = Flask(__name__)
validator = RequestValidator('your_auth_token')
@app.route('/webhook', methods=['POST'])
def webhook_handler():
signature = request.headers.get('X-Twilio-Signature', '')
url = request.url
# Handle form-encoded data (default)
if request.content_type == 'application/x-www-form-urlencoded':
params = request.form.to_dict()
# Handle JSON data (for some webhook types)
elif request.content_type == 'application/json':
# For JSON, validate against raw body
body = request.get_data(as_text=True)
body_hash = validator.compute_body_hash(body)
# Add body hash to empty params for validation
params = {'bodySHA256': body_hash}
else:
return 'Unsupported content type', 400
if not validator.validate(url, params, signature):
return 'Forbidden', 403
return 'OK', 200from twilio.request_validator import RequestValidator
def debug_validation(uri, params, signature, auth_token):
"""Debug webhook validation issues"""
validator = RequestValidator(auth_token)
print(f"URI: {uri}")
print(f"Params: {params}")
print(f"Signature: {signature}")
# Compute expected signature
expected = validator.compute_signature(uri, params)
print(f"Expected: {expected}")
# Check if they match
is_valid = validator.validate(uri, params, signature)
print(f"Valid: {is_valid}")
# Common issues to check
if not is_valid:
print("\nCommon issues to check:")
print("1. Ensure URI includes protocol (https://)")
print("2. Ensure URI includes correct port (if not 80/443)")
print("3. Check for URL encoding differences")
print("4. Verify auth token is correct")
print("5. Check for proxy/load balancer URI changes")
# Try with port manipulation
uri_with_port = add_port(uri)
uri_without_port = remove_port(uri)
if validator.validate(uri_with_port, params, signature):
print(f"✓ Valid with explicit port: {uri_with_port}")
elif validator.validate(uri_without_port, params, signature):
print(f"✓ Valid without port: {uri_without_port}")
# Usage
debug_validation(
uri='https://example.com/webhook',
params={'From': '+15551234567', 'Body': 'Test'},
signature='signature_from_header',
auth_token='your_auth_token'
)from twilio.request_validator import RequestValidator
from functools import wraps
import os
# Initialize validator with environment variable
validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))
def validate_twilio_request(f):
"""Decorator for automatic webhook validation"""
@wraps(f)
def decorated_function(*args, **kwargs):
from flask import request, abort
signature = request.headers.get('X-Twilio-Signature', '')
url = request.url
params = request.form.to_dict()
if not validator.validate(url, params, signature):
abort(403) # Forbidden
return f(*args, **kwargs)
return decorated_function
# Usage
@app.route('/voice-webhook', methods=['POST'])
@validate_twilio_request
def handle_voice():
# This code only runs for valid Twilio requests
from twilio.twiml.voice_response import VoiceResponse
response = VoiceResponse()
response.say("Hello from validated webhook!")
return str(response)
@app.route('/sms-webhook', methods=['POST'])
@validate_twilio_request
def handle_sms():
from twilio.twiml.messaging_response import MessagingResponse
body = request.form.get('Body', '').strip()
response = MessagingResponse()
response.message(f"You said: {body}")
return str(response)Install with Tessl CLI
npx tessl i tessl/pypi-twilio