Skip to contentSkip to navigationSkip to topbar
Rate this Page:

Secure your Flask App by Validating Incoming Twilio Requests


In this guide we'll cover how to secure your Flask(link takes you to an external page) 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!


Create a custom decorator

create-a-custom-decorator page anchor

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(link takes you to an external page). This way we can reuse our validation logic across all our views which accept incoming requests from Twilio.

Custom decorator for Flask apps to validate Twilio requests

custom-decorator-for-flask-apps-to-validate-twilio-requests page anchor

Confirm incoming requests to your Flask views are genuine with this custom decorator.


_31
from flask import abort, Flask, request
_31
from functools import wraps
_31
from twilio.request_validator import RequestValidator
_31
_31
import os
_31
_31
_31
app = Flask(__name__)
_31
_31
_31
def 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.

(warning)

Warning

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.


Use the Decorator with our Twilio Webhooks

use-the-decorator-with-our-twilio-webhooks page anchor

Now we're ready to apply our decorator to any view in our Flask application that handles incoming requests from Twilio.

Apply the request validation decorator to a Flask view

apply-the-request-validation-decorator-to-a-flask-view page anchor

Apply a custom Twilio request validation decorator to a Flask view used for Twilio webhooks.


_46
from flask import Flask, request
_46
from twilio.twiml.voice_response import VoiceResponse, MessagingResponse
_46
_46
_46
app = Flask(__name__)
_46
_46
_46
@app.route('/voice', methods=['POST'])
_46
@validate_twilio_request
_46
def 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
_46
def 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
_46
if __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.

(warning)

Warning

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.


Disable Request Validation During Testing

disable-request-validation-during-testing page anchor

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.

An improved Flask request validation decorator, useful for testing

an-improved-flask-request-validation-decorator-useful-for-testing page anchor

Use this version of the custom Flask decorator if you test your Flask views.


_28
from flask import abort, current_app, request
_28
from functools import wraps
_28
from twilio.request_validator import RequestValidator
_28
_28
import os
_28
_28
_28
def 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


Test the validity of your webhook signature

test-the-validity-of-your-webhook-signature page anchor
(information)

Info

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:

  1. Set your Auth Token(link takes you to an external page) as an environment variable
  2. Set the URL to the endpoint you want to test
  3. If testing BasicAuth, change HTTPDigestAuth to HTTPBasicAuth

Test the validity of your webhook signature

test-the-validity-of-your-webhook-signature-1 page anchor

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
_45
from twilio.request_validator import RequestValidator
_45
from requests.auth import HTTPDigestAuth
_45
from requests.auth import HTTPBasicAuth
_45
import requests
_45
import urllib
_45
import 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
_45
auth_token = os.environ.get('TWILIO_AUTH_TOKEN')
_45
validator = RequestValidator(auth_token)
_45
_45
# Replace this URL with your unique URL
_45
url = 'https://mycompany.com/myapp'
_45
# User credentials if required by your web server. Change to 'HTTPBasicAuth' if needed
_45
auth = HTTPDigestAuth('username', 'password')
_45
_45
params = {
_45
'CallSid': 'CA1234567890ABCDE',
_45
'Caller': '+12349013030',
_45
'Digits': '1234',
_45
'From': '+12349013030',
_45
'To': '+18005551212'
_45
}
_45
_45
def 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
_45
test_url('GET', url, params, True)
_45
test_url('GET', url, params, False)
_45
test_url('POST', url, params, True)
_45
test_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(link takes you to an external page).


Rate this Page: