In this guide we'll cover how to secure your Flask application by validating incoming requests to your Twilio webhooks are, in fact, from Twilio.
With a few lines of code, we'll write a custom decorator for our Flask app that uses the Twilio Python SDK's validator utility. We can then use that decorator on our Flask views which accept Twilio webhooks to confirm that incoming requests genuinely originated from Twilio.
Let's get started!
The Twilio Python SDK includes a RequestValidator
class we can use to validate incoming requests.
We could include our request validation code as part of our Flask views, but this is a perfect opportunity to write a Python decorator. This way we can reuse our validation logic across all our views which accept incoming requests from Twilio.
Confirm incoming requests to your Flask views are genuine with this custom decorator.
_31from flask import abort, Flask, request_31from functools import wraps_31from twilio.request_validator import RequestValidator_31_31import os_31_31_31app = Flask(__name__)_31_31_31def validate_twilio_request(f):_31 """Validates that incoming requests genuinely originated from Twilio"""_31 @wraps(f)_31 def decorated_function(*args, **kwargs):_31 # Create an instance of the RequestValidator class_31 validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))_31_31 # Validate the request using its URL, POST data,_31 # and X-TWILIO-SIGNATURE header_31 request_valid = validator.validate(_31 request.url,_31 request.form,_31 request.headers.get('X-TWILIO-SIGNATURE', ''))_31_31 # Continue processing the request if it's valid, return a 403 error if_31 # it's not_31 if request_valid:_31 return f(*args, **kwargs)_31 else:_31 return abort(403)_31 return decorated_function
To validate an incoming request genuinely originated from Twilio, we first need to create an instance of the RequestValidator
class using our Twilio auth token. After that we call its validate
method, passing in the request's URL, payload, and the value of the request's X-TWILIO-SIGNATURE
header.
That method will return True if the request is valid or False if it isn't. Our decorator then either continues processing the view or returns a 403 HTTP response for inauthentic requests.
If you are passing query string parameters in the URLs used in the webhooks you are validating, you may need to take extra care to encode or decode the URL so that validation passes. Some web frameworks like Flask will sometimes automatically unescape the query string part of the request URL, causing validation to fail.
Now we're ready to apply our decorator to any view in our Flask application that handles incoming requests from Twilio.
Apply a custom Twilio request validation decorator to a Flask view used for Twilio webhooks.
_46from flask import Flask, request_46from twilio.twiml.voice_response import VoiceResponse, MessagingResponse_46_46_46app = Flask(__name__)_46_46_46@app.route('/voice', methods=['POST'])_46@validate_twilio_request_46def incoming_call():_46 """Twilio Voice URL - receives incoming calls from Twilio"""_46 # Create a new TwiML response_46 resp = VoiceResponse()_46_46 # <Say> a message to the caller_46 from_number = request.values['From']_46 body = """_46 Thanks for calling!_46_46 Your phone number is {0}. I got your call because of Twilio's webhook._46_46 Goodbye!""".format(' '.join(from_number))_46 resp.say(body)_46_46 # Return the TwiML_46 return str(resp)_46_46_46@app.route('/message', methods=['POST'])_46@validate_twilio_request_46def incoming_message():_46 """Twilio Messaging URL - receives incoming messages from Twilio"""_46 # Create a new TwiML response_46 resp = MessagingResponse()_46_46 # <Message> a text back to the person who texted us_46 body = "Your text to me was {0} characters long. Webhooks are neat :)" \_46 .format(len(request.values['Body']))_46 resp.message(body)_46_46 # Return the TwiML_46 return str(resp)_46_46_46if __name__ == '__main__':_46 app.run(debug=True)
To use the decorator with an existing view, just put @validate_twilio_request
above the view's definition. In this sample application, we use our decorator with two views: one that handles incoming phone calls and another that handles incoming text messages.
If your Twilio webhook URLs start with https\://
instead of http\://
, your request validator may fail locally when you use Ngrok or in production if your stack terminates SSL connections upstream from your app. This is because the request URL that your Flask application sees does not match the URL Twilio used to reach your application.
To fix this for local development with Ngrok, use http\://
for your webhook instead of https\://
. To fix this in your production app, your decorator will need to reconstruct the request's original URL using request headers like X-Original-Host
and X-Forwarded-Proto
, if available.
If you write tests for your Flask views those tests may fail for views where you use your Twilio request validation decorator. Any requests your test suite sends to those views will fail the decorator's validation check.
To fix this problem we recommend adding an extra check in your decorator, like so, telling it to only reject incoming requests if your app is running in production.
Use this version of the custom Flask decorator if you test your Flask views.
_28from flask import abort, current_app, request_28from functools import wraps_28from twilio.request_validator import RequestValidator_28_28import os_28_28_28def validate_twilio_request(f):_28 """Validates that incoming requests genuinely originated from Twilio"""_28 @wraps(f)_28 def decorated_function(*args, **kwargs):_28 # Create an instance of the RequestValidator class_28 validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))_28_28 # Validate the request using its URL, POST data,_28 # and X-TWILIO-SIGNATURE header_28 request_valid = validator.validate(_28 request.url,_28 request.form,_28 request.headers.get('X-TWILIO-SIGNATURE', ''))_28_28 # Continue processing the request if it's valid (or if DEBUG is True)_28 # and return a 403 error if it's not_28 if request_valid or current_app.debug:_28 return f(*args, **kwargs)_28 else:_28 return abort(403)_28 return decorated_function
It's a great idea to run automated testing against your webhooks to ensure that their signatures are secure. The following Python code can test your unique endpoints against both valid and invalid signatures.
To make this test work for you, you'll need to:
HTTPDigestAuth
to
HTTPBasicAuth
This sample test will test the validity of your webhook signature with HTTP Basic or Digest authentication.
_45# Download the twilio-python library from twilio.com/docs/python/install_45from twilio.request_validator import RequestValidator_45from requests.auth import HTTPDigestAuth_45from requests.auth import HTTPBasicAuth_45import requests_45import urllib_45import os_45_45# Your Auth Token from twilio.com/user/account saved as an environment variable_45# Remember never to hard code your auth token in code, browser Javascript, or distribute it in mobile apps_45auth_token = os.environ.get('TWILIO_AUTH_TOKEN')_45validator = RequestValidator(auth_token)_45_45# Replace this URL with your unique URL_45url = 'https://mycompany.com/myapp'_45# User credentials if required by your web server. Change to 'HTTPBasicAuth' if needed_45auth = HTTPDigestAuth('username', 'password')_45_45params = {_45 'CallSid': 'CA1234567890ABCDE',_45 'Caller': '+12349013030',_45 'Digits': '1234',_45 'From': '+12349013030',_45 'To': '+18005551212'_45}_45_45def test_url(method, url, params, valid):_45 if method == "GET":_45 url = url + '?' + urllib.parse.urlencode(params)_45 params = {}_45_45 if valid:_45 signature = validator.compute_signature(url, params)_45 else:_45 signature = validator.compute_signature("http://invalid.com", params)_45_45 headers = {'X-Twilio-Signature': signature}_45 response = requests.request(method, url, headers=headers, data=params, auth=auth)_45 print('HTTP {0} with {1} signature returned {2}'.format(method, 'valid' if valid else 'invalid', response.status_code))_45_45_45test_url('GET', url, params, True)_45test_url('GET', url, params, False)_45test_url('POST', url, params, True)_45test_url('POST', url, params, False)
Validating requests to your Twilio webhooks is a great first step for securing your Twilio application. We recommend reading over our full security documentation for more advice on protecting your app, and the Anti-Fraud Developer's Guide in particular.
To learn more about securing your Flask application in general, check out the security considerations page in the official Flask docs.